quant_indicators/
variance_ratio.rs1use quant_primitives::Candle;
19use rust_decimal::Decimal;
20
21use crate::error::IndicatorError;
22use crate::indicator::Indicator;
23use crate::series::Series;
24
25#[derive(Debug, Clone)]
30pub struct VarianceRatio {
31 lag: usize,
32 name: String,
33}
34
35impl VarianceRatio {
36 pub fn new(lag: usize) -> Result<Self, IndicatorError> {
42 if lag < 2 {
43 return Err(IndicatorError::InvalidParameter {
44 message: format!("VarianceRatio lag must be >= 2, got {}", lag),
45 });
46 }
47 Ok(Self {
48 lag,
49 name: format!("VR({})", lag),
50 })
51 }
52
53 pub fn compute_ratio(&self, candles: &[Candle]) -> Result<Decimal, IndicatorError> {
60 let min_required = self.lag + 2;
61 if candles.len() < min_required {
62 return Err(IndicatorError::InsufficientData {
63 required: min_required,
64 actual: candles.len(),
65 });
66 }
67
68 let log_prices = Self::log_prices(candles);
69 Self::vr_from_log_prices(&log_prices, self.lag)
70 }
71
72 pub fn rolling(
76 &self,
77 candles: &[Candle],
78 window: usize,
79 ) -> Result<Vec<(usize, Decimal)>, IndicatorError> {
80 let min_required = window;
81 if candles.len() < min_required {
82 return Err(IndicatorError::InsufficientData {
83 required: min_required,
84 actual: candles.len(),
85 });
86 }
87
88 let log_prices = Self::log_prices(candles);
89 let mut results = Vec::new();
90
91 for end in window..=log_prices.len() {
92 let slice = &log_prices[end - window..end];
93 match Self::vr_from_log_prices(slice, self.lag) {
94 Ok(vr) => results.push((end - 1, vr)),
95 Err(_) => continue,
96 }
97 }
98
99 if results.is_empty() {
100 return Err(IndicatorError::InsufficientData {
101 required: window,
102 actual: candles.len(),
103 });
104 }
105
106 Ok(results)
107 }
108
109 fn log_prices(candles: &[Candle]) -> Vec<Decimal> {
111 candles.iter().map(|c| decimal_ln(c.close())).collect()
112 }
113
114 fn vr_from_log_prices(log_prices: &[Decimal], lag: usize) -> Result<Decimal, IndicatorError> {
116 if log_prices.len() < lag + 2 {
117 return Err(IndicatorError::InsufficientData {
118 required: lag + 2,
119 actual: log_prices.len(),
120 });
121 }
122
123 let returns_1: Vec<Decimal> = log_prices.windows(2).map(|w| w[1] - w[0]).collect();
125
126 let var1 = variance(&returns_1);
128
129 if var1.is_zero() {
130 return Err(IndicatorError::InsufficientData {
131 required: lag + 2,
132 actual: log_prices.len(),
133 });
134 }
135
136 let diffs_q: Vec<Decimal> = log_prices.windows(lag + 1).map(|w| w[lag] - w[0]).collect();
138
139 let var_q = variance(&diffs_q);
141 let q_dec = Decimal::from(lag as i64);
142 let var_q_scaled = var_q / q_dec;
143
144 Ok(var_q_scaled / var1)
145 }
146}
147
148impl Indicator for VarianceRatio {
149 fn name(&self) -> &str {
150 &self.name
151 }
152
153 fn warmup_period(&self) -> usize {
154 self.lag + 2
155 }
156
157 fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
158 let vr = self.compute_ratio(candles)?;
159 let last = candles.last().ok_or(IndicatorError::InsufficientData {
160 required: 1,
161 actual: 0,
162 })?;
163 Ok(Series::new(vec![(last.timestamp(), vr)]))
164 }
165}
166
167fn variance(data: &[Decimal]) -> Decimal {
169 if data.is_empty() {
170 return Decimal::ZERO;
171 }
172
173 let n = Decimal::from(data.len() as i64);
174 let mean = data.iter().copied().sum::<Decimal>() / n;
175 let sum_sq: Decimal = data.iter().map(|x| (*x - mean) * (*x - mean)).sum();
176 sum_sq / n
177}
178
179fn decimal_ln(x: Decimal) -> Decimal {
185 if x <= Decimal::ZERO {
186 return Decimal::ZERO;
187 }
188
189 if x == Decimal::ONE {
190 return Decimal::ZERO;
191 }
192
193 use rust_decimal::prelude::ToPrimitive;
197 let x_f64 = x.to_f64().unwrap_or(1.0);
198 let ln_f64 = x_f64.ln();
199 Decimal::from_f64_retain(ln_f64).unwrap_or(Decimal::ZERO)
200}
201
202#[cfg(test)]
203#[path = "variance_ratio_tests.rs"]
204mod tests;