kestrel_chartkit/indicator/
relative_volatility.rs1use std::collections::VecDeque;
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6use crate::model::Bar;
7
8use super::smoothing::Rma;
9use super::{Indicator, IndicatorOutput};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13#[cfg_attr(
14 feature = "serde",
15 derive(Serialize, Deserialize),
16 serde(rename_all = "snake_case")
17)]
18pub enum RelativeVolatilityVariant {
19 #[default]
21 Close,
22 HighLow,
25}
26
27#[derive(Debug, Clone)]
59pub struct RelativeVolatilityIndex {
60 stdev_len: usize,
61 smooth_len: usize,
62 variant: RelativeVolatilityVariant,
63 close: DirectionalDeviation,
64 high: DirectionalDeviation,
65 low: DirectionalDeviation,
66}
67
68#[derive(Debug, Clone)]
70struct DirectionalDeviation {
71 len: usize,
72 window: VecDeque<f64>,
73 previous: Option<f64>,
74 up: Rma,
75 down: Rma,
76}
77
78impl DirectionalDeviation {
79 fn new(len: usize, smooth_len: usize) -> Self {
80 Self {
81 len,
82 window: VecDeque::with_capacity(len),
83 previous: None,
84 up: Rma::new(smooth_len),
85 down: Rma::new(smooth_len),
86 }
87 }
88
89 fn update(&mut self, value: f64) -> Option<f64> {
90 self.window.push_back(value);
91 if self.window.len() > self.len {
92 self.window.pop_front();
93 }
94 let previous = self.previous.replace(value);
95 if self.window.len() < self.len {
96 return None;
97 }
98
99 let mean = self.window.iter().sum::<f64>() / self.len as f64;
102 let variance = self
103 .window
104 .iter()
105 .map(|entry| {
106 let diff = entry - mean;
107 diff * diff
108 })
109 .sum::<f64>()
110 / self.len as f64;
111 let deviation = variance.sqrt();
112
113 let previous = previous?;
114 let (up, down) = if value > previous {
115 (deviation, 0.0)
116 } else if value < previous {
117 (0.0, deviation)
118 } else {
119 (0.0, 0.0)
120 };
121
122 let up_avg = self.up.update(up);
126 let down_avg = self.down.update(down);
127 let (up_avg, down_avg) = (up_avg?, down_avg?);
128 let total = up_avg + down_avg;
129 Some(if total > 0.0 {
130 100.0 * up_avg / total
131 } else {
132 50.0
133 })
134 }
135
136 fn reset(&mut self) {
137 self.window.clear();
138 self.previous = None;
139 self.up.reset();
140 self.down.reset();
141 }
142}
143
144impl RelativeVolatilityIndex {
145 pub fn new(stdev_len: usize, smooth_len: usize, variant: RelativeVolatilityVariant) -> Self {
146 let stdev_len = stdev_len.max(2);
147 let smooth_len = smooth_len.max(1);
148 Self {
149 stdev_len,
150 smooth_len,
151 variant,
152 close: DirectionalDeviation::new(stdev_len, smooth_len),
153 high: DirectionalDeviation::new(stdev_len, smooth_len),
154 low: DirectionalDeviation::new(stdev_len, smooth_len),
155 }
156 }
157
158 pub fn with_defaults() -> Self {
159 Self::new(10, 14, RelativeVolatilityVariant::Close)
160 }
161
162 pub fn variant(&self) -> RelativeVolatilityVariant {
163 self.variant
164 }
165}
166
167impl Indicator for RelativeVolatilityIndex {
168 fn name(&self) -> &str {
169 "relative_volatility"
170 }
171
172 fn warmup_period(&self) -> usize {
173 self.stdev_len + self.smooth_len
174 }
175
176 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
177 match self.variant {
178 RelativeVolatilityVariant::Close => {
179 self.close.update(bar.close).map(IndicatorOutput::new)
180 }
181 RelativeVolatilityVariant::HighLow => {
182 let high = self.high.update(bar.high);
185 let low = self.low.update(bar.low);
186 match (high, low) {
187 (Some(high), Some(low)) => Some(IndicatorOutput::new((high + low) / 2.0)),
188 _ => None,
189 }
190 }
191 }
192 }
193
194 fn reset(&mut self) {
195 self.close.reset();
196 self.high.reset();
197 self.low.reset();
198 }
199}