schwab_cli/agent/
volatility.rs1use anyhow::{Context, Result};
4use schwab_market_data::MarketDataApi;
5
6pub fn realized_vol_annualized_pct(closes: &[f64], lookback: usize) -> f64 {
8 if closes.len() <= lookback + 1 || lookback == 0 {
9 return 0.0;
10 }
11 let mut returns = Vec::new();
12 let start = closes.len().saturating_sub(lookback + 1);
13 for i in start + 1..closes.len() {
14 let prev = closes[i - 1];
15 if prev > 0.0 && closes[i] > 0.0 {
16 returns.push((closes[i] / prev).ln());
17 }
18 }
19 if returns.is_empty() {
20 return 0.0;
21 }
22 let mean = returns.iter().sum::<f64>() / returns.len() as f64;
23 let var = returns
24 .iter()
25 .map(|r| (r - mean).powi(2))
26 .sum::<f64>()
27 / returns.len() as f64;
28 (var.sqrt() * (252.0_f64).sqrt()) * 100.0
29}
30
31pub async fn fetch_realized_vol_pct(
33 market: &MarketDataApi,
34 symbol: &str,
35 lookback: usize,
36) -> Result<Option<f64>> {
37 let sym = symbol.trim().to_uppercase();
38 let history = market
39 .price_history()
40 .get(
41 &sym,
42 Some("year"),
43 Some(1),
44 Some("daily"),
45 None,
46 None,
47 None,
48 None,
49 Some(true),
50 )
51 .await
52 .with_context(|| format!("price history for realized vol {sym}"))?;
53
54 let closes: Vec<f64> = history
55 .get("candles")
56 .and_then(|v| v.as_array())
57 .map(|arr| {
58 arr.iter()
59 .filter_map(|c| c.get("close").and_then(|v| v.as_f64()))
60 .collect()
61 })
62 .unwrap_or_default();
63
64 let rv = realized_vol_annualized_pct(&closes, lookback);
65 if rv > 0.0 {
66 Ok(Some(rv))
67 } else {
68 Ok(None)
69 }
70}
71
72pub fn iv_rv_ratio(chain_iv_pct: Option<f64>, realized_vol_pct: Option<f64>) -> Option<f64> {
74 match (chain_iv_pct, realized_vol_pct) {
75 (Some(iv), Some(rv)) if iv > 0.0 && rv > 0.0 => Some(iv / rv),
76 _ => None,
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn realized_vol_positive_on_trending_series() {
86 let mut closes = Vec::new();
87 let mut px = 100.0;
88 for i in 0..40 {
89 px *= if i % 2 == 0 { 1.01 } else { 0.99 };
90 closes.push(px);
91 }
92 let rv = realized_vol_annualized_pct(&closes, 20);
93 assert!(rv > 5.0, "rv={rv}");
94 }
95
96 #[test]
97 fn iv_rv_ratio_none_when_missing() {
98 assert!(iv_rv_ratio(None, Some(15.0)).is_none());
99 assert!(iv_rv_ratio(Some(20.0), None).is_none());
100 assert!((iv_rv_ratio(Some(22.0), Some(20.0)).unwrap() - 1.1).abs() < 1e-9);
101 }
102}