nautilus_indicators/volatility/
rvi.rs1use std::{
17 collections::VecDeque,
18 fmt::{Debug, Display},
19};
20
21use nautilus_core::correctness::FAILED;
22use nautilus_model::data::{Bar, QuoteTick, TradeTick};
23
24use crate::{
25 average::{MovingAverageFactory, MovingAverageType},
26 indicator::{Indicator, MovingAverage},
27 support::{MAX_PERIOD, ShiftedMoments},
28};
29
30#[repr(C)]
32#[derive(Debug)]
33#[cfg_attr(
34 feature = "python",
35 pyo3::pyclass(module = "nautilus_trader.indicators", unsendable)
36)]
37#[cfg_attr(
38 feature = "python",
39 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
40)]
41pub struct RelativeVolatilityIndex {
42 pub period: usize,
43 pub scalar: f64,
44 pub ma_type: MovingAverageType,
45 pub value: f64,
46 pub initialized: bool,
47 prices: VecDeque<f64>,
48 moments: ShiftedMoments,
49 pos_ma: Box<dyn MovingAverage + Send + 'static>,
50 neg_ma: Box<dyn MovingAverage + Send + 'static>,
51 previous_close: f64,
52 has_inputs: bool,
53}
54
55impl Display for RelativeVolatilityIndex {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(
58 f,
59 "{}({},{},{})",
60 self.name(),
61 self.period,
62 self.scalar,
63 self.ma_type,
64 )
65 }
66}
67
68impl Indicator for RelativeVolatilityIndex {
69 fn name(&self) -> String {
70 stringify!(RelativeVolatilityIndex).to_string()
71 }
72
73 fn has_inputs(&self) -> bool {
74 self.has_inputs
75 }
76
77 fn initialized(&self) -> bool {
78 self.initialized
79 }
80
81 fn handle_quote(&mut self, _quote: &QuoteTick) -> anyhow::Result<()> {
82 Ok(())
83 }
84
85 fn handle_trade(&mut self, _trade: &TradeTick) {}
86
87 fn handle_bar(&mut self, bar: &Bar) {
88 self.update_raw((&bar.close).into());
89 }
90
91 fn reset(&mut self) {
92 self.previous_close = 0.0;
93 self.value = 0.0;
94 self.has_inputs = false;
95 self.initialized = false;
96 self.prices.clear();
97 self.moments.reset();
98 self.pos_ma.reset();
99 self.neg_ma.reset();
100 }
101}
102
103impl RelativeVolatilityIndex {
104 #[must_use]
113 pub fn new(period: usize, scalar: Option<f64>, ma_type: Option<MovingAverageType>) -> Self {
114 Self::new_checked(period, scalar, ma_type).expect(FAILED)
115 }
116
117 pub(crate) fn new_checked(
118 period: usize,
119 scalar: Option<f64>,
120 ma_type: Option<MovingAverageType>,
121 ) -> anyhow::Result<Self> {
122 anyhow::ensure!(
123 (2..=MAX_PERIOD).contains(&period),
124 "period must be in 2..={MAX_PERIOD}"
125 );
126 let scalar = scalar.unwrap_or(100.0);
127 anyhow::ensure!(
128 scalar.is_finite() && (0.0..=100.0).contains(&scalar),
129 "scalar must be finite and in 0..=100"
130 );
131 Ok(Self {
132 period,
133 scalar,
134 ma_type: ma_type.unwrap_or(MovingAverageType::Wilder),
135 value: 0.0,
136 initialized: false,
137 prices: VecDeque::with_capacity(period),
138 moments: ShiftedMoments::new(),
139 pos_ma: MovingAverageFactory::create(
140 ma_type.unwrap_or(MovingAverageType::Wilder),
141 period,
142 ),
143 neg_ma: MovingAverageFactory::create(
144 ma_type.unwrap_or(MovingAverageType::Wilder),
145 period,
146 ),
147 previous_close: 0.0,
148 has_inputs: false,
149 })
150 }
151
152 pub fn update_raw(&mut self, close: f64) {
153 if !close.is_finite() {
154 return;
155 }
156
157 if self.prices.len() == self.period
158 && let Some(old) = self.prices.pop_front()
159 {
160 self.moments.evict(old);
161 }
162 self.prices.push_back(close);
163 self.moments.push(close);
164 if self.moments.needs_reseed(self.period) {
165 self.moments.reseed(self.prices.iter());
166 }
167 self.has_inputs = true;
168
169 if self.prices.len() < self.period {
170 self.previous_close = close;
171 return;
172 }
173
174 let std_dev = self.moments.std_dev(self.period);
175 self.pos_ma.update_raw(if close > self.previous_close {
176 std_dev
177 } else {
178 0.0
179 });
180 self.neg_ma.update_raw(if close < self.previous_close {
181 std_dev
182 } else {
183 0.0
184 });
185 self.previous_close = close;
186
187 if !self.pos_ma.initialized() || !self.neg_ma.initialized() {
188 return;
189 }
190 let denominator = self.pos_ma.value() + self.neg_ma.value();
191 self.value = if denominator == 0.0 {
192 self.scalar * 0.5
193 } else {
194 self.scalar * self.pos_ma.value() / denominator
195 };
196 self.initialized = true;
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use rstest::rstest;
203
204 use super::*;
205 use crate::{stubs::rvi_10, testing::assert_approx_equal};
206
207 #[rstest]
208 fn test_name_returns_expected_string(rvi_10: RelativeVolatilityIndex) {
209 assert_eq!(rvi_10.name(), "RelativeVolatilityIndex");
210 }
211
212 #[rstest]
213 fn test_str_repr_returns_expected_string(rvi_10: RelativeVolatilityIndex) {
214 assert_eq!(format!("{rvi_10}"), "RelativeVolatilityIndex(10,10,SIMPLE)");
215 }
216
217 #[rstest]
218 fn test_period_returns_expected_value(rvi_10: RelativeVolatilityIndex) {
219 assert_eq!(rvi_10.period, 10);
220 assert_eq!(rvi_10.scalar, 10.0);
221 assert_eq!(rvi_10.ma_type, MovingAverageType::Simple);
222 }
223
224 #[rstest]
225 fn test_initialized_without_inputs_returns_false(rvi_10: RelativeVolatilityIndex) {
226 assert!(!rvi_10.initialized());
227 }
228
229 #[rstest]
230 fn test_value_with_all_higher_inputs_returns_expected_value(
231 mut rvi_10: RelativeVolatilityIndex,
232 ) {
233 let close_values = [
234 105.25, 107.50, 109.75, 112.00, 114.25, 116.50, 118.75, 121.00, 123.25, 125.50, 127.75,
235 130.00, 132.25, 134.50, 136.75, 139.00, 141.25, 143.50, 145.75, 148.00, 150.25, 152.50,
236 154.75, 157.00, 159.25, 161.50, 163.75, 166.00, 168.25, 170.50,
237 ];
238
239 for close in close_values {
240 rvi_10.update_raw(close);
241 }
242
243 assert!(rvi_10.initialized());
244 assert_eq!(rvi_10.value, 10.0);
245 }
246
247 #[rstest]
248 fn test_prices_window_bounded_to_period(mut rvi_10: RelativeVolatilityIndex) {
249 for i in 0..50 {
253 rvi_10.update_raw(100.0 + f64::from(i));
254 }
255
256 assert!(rvi_10.initialized());
257 assert_eq!(rvi_10.prices.len(), 10);
258 assert_eq!(rvi_10.value, 10.0);
259 }
260
261 #[rstest]
262 fn test_reset_successfully_returns_indicator_to_fresh_state(
263 mut rvi_10: RelativeVolatilityIndex,
264 ) {
265 rvi_10.update_raw(1.00020);
266 rvi_10.update_raw(1.00030);
267 rvi_10.update_raw(1.00070);
268
269 rvi_10.reset();
270
271 assert!(!rvi_10.initialized());
272 assert_eq!(rvi_10.value, 0.0);
273 assert!(!rvi_10.initialized);
274 assert!(!rvi_10.has_inputs);
275 assert_eq!(rvi_10.prices.len(), 0);
276 assert_eq!(rvi_10.moments.mean(rvi_10.period), 0.0);
277 assert_eq!(rvi_10.pos_ma.value(), 0.0);
278 assert_eq!(rvi_10.neg_ma.value(), 0.0);
279 }
280
281 #[rstest]
282 fn test_value_is_midpoint_for_flat_prices(mut rvi_10: RelativeVolatilityIndex) {
283 for _ in 0..30 {
284 rvi_10.update_raw(100.0);
285 }
286
287 assert!(rvi_10.initialized());
288 assert_eq!(rvi_10.value, 5.0);
289 }
290
291 #[rstest]
292 fn test_value_matches_wilder_smoothed_population_deviation() {
293 let mut rvi = RelativeVolatilityIndex::new(3, Some(100.0), None);
296 for close in [10.0, 12.0, 11.0, 13.0, 12.0] {
297 rvi.update_raw(close);
298 }
299 assert!(rvi.initialized());
300 assert_approx_equal(rvi.value, 100.0 / 3.0);
301
302 rvi.update_raw(15.0);
303 let s = (2.0_f64 / 3.0).sqrt();
304 let t = 14.0_f64.sqrt() / 3.0;
305 let pos = (2.0 * s / 3.0 + t) / 3.0;
306 let neg = 4.0 * s / 9.0;
307 assert_approx_equal(rvi.value, 100.0 * pos / (pos + neg));
308 }
309}