nautilus_indicators/volatility/
rvi.rs1use std::fmt::{Debug, Display};
17
18use arraydeque::{ArrayDeque, Wrapping};
19use nautilus_model::data::{Bar, QuoteTick, TradeTick};
20
21use crate::{
22 average::{MovingAverageFactory, MovingAverageType},
23 indicator::{Indicator, MovingAverage},
24};
25
26#[repr(C)]
28#[derive(Debug)]
29#[cfg_attr(
30 feature = "python",
31 pyo3::pyclass(module = "nautilus_trader.indicators", unsendable)
32)]
33#[cfg_attr(
34 feature = "python",
35 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
36)]
37pub struct RelativeVolatilityIndex {
38 pub period: usize,
39 pub scalar: f64,
40 pub ma_type: MovingAverageType,
41 pub value: f64,
42 pub initialized: bool,
43 prices: ArrayDeque<f64, 1024, Wrapping>,
44 ma: Box<dyn MovingAverage + Send + 'static>,
45 pos_ma: Box<dyn MovingAverage + Send + 'static>,
46 neg_ma: Box<dyn MovingAverage + Send + 'static>,
47 previous_close: f64,
48 std: f64,
49 has_inputs: bool,
50}
51
52impl Display for RelativeVolatilityIndex {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(
55 f,
56 "{}({},{},{})",
57 self.name(),
58 self.period,
59 self.scalar,
60 self.ma_type,
61 )
62 }
63}
64
65impl Indicator for RelativeVolatilityIndex {
66 fn name(&self) -> String {
67 stringify!(RelativeVolatilityIndex).to_string()
68 }
69
70 fn has_inputs(&self) -> bool {
71 self.has_inputs
72 }
73
74 fn initialized(&self) -> bool {
75 self.initialized
76 }
77
78 fn handle_quote(&mut self, _quote: &QuoteTick) -> anyhow::Result<()> {
79 Ok(())
80 }
81
82 fn handle_trade(&mut self, _trade: &TradeTick) {}
83
84 fn handle_bar(&mut self, bar: &Bar) {
85 self.update_raw((&bar.close).into());
86 }
87
88 fn reset(&mut self) {
89 self.previous_close = 0.0;
90 self.value = 0.0;
91 self.has_inputs = false;
92 self.initialized = false;
93 self.std = 0.0;
94 self.prices.clear();
95 self.ma.reset();
96 self.pos_ma.reset();
97 self.neg_ma.reset();
98 }
99}
100
101impl RelativeVolatilityIndex {
102 #[must_use]
111 pub fn new(period: usize, scalar: Option<f64>, ma_type: Option<MovingAverageType>) -> Self {
112 assert!(
113 period <= 1024,
114 "period {period} exceeds maximum capacity of price deque"
115 );
116
117 Self {
118 period,
119 scalar: scalar.unwrap_or(100.0),
120 ma_type: ma_type.unwrap_or(MovingAverageType::Simple),
121 value: 0.0,
122 initialized: false,
123 prices: ArrayDeque::new(),
124 ma: MovingAverageFactory::create(ma_type.unwrap_or(MovingAverageType::Simple), period),
125 pos_ma: MovingAverageFactory::create(
126 ma_type.unwrap_or(MovingAverageType::Simple),
127 period,
128 ),
129 neg_ma: MovingAverageFactory::create(
130 ma_type.unwrap_or(MovingAverageType::Simple),
131 period,
132 ),
133 previous_close: 0.0,
134 std: 0.0,
135 has_inputs: false,
136 }
137 }
138
139 pub fn update_raw(&mut self, close: f64) {
140 if self.prices.len() == self.period {
144 self.prices.pop_front();
145 }
146
147 self.prices.push_back(close);
148 self.ma.update_raw(close);
149
150 if self.prices.is_empty() {
151 self.std = 0.0;
152 } else {
153 let mean = self.ma.value();
154 let mut var_sum = 0.0;
155
156 for &price in &self.prices {
157 let diff = price - mean;
158 var_sum += diff * diff;
159 }
160 self.std = (var_sum / self.prices.len() as f64).sqrt();
161 self.std = self.std * (self.period as f64).sqrt() / ((self.period - 1) as f64).sqrt();
162 }
163
164 if self.ma.initialized() {
165 if close > self.previous_close {
166 self.pos_ma.update_raw(self.std);
167 self.neg_ma.update_raw(0.0);
168 } else if close < self.previous_close {
169 self.pos_ma.update_raw(0.0);
170 self.neg_ma.update_raw(self.std);
171 } else {
172 self.pos_ma.update_raw(0.0);
173 self.neg_ma.update_raw(0.0);
174 }
175
176 self.value = self.scalar * self.pos_ma.value();
177 self.value /= self.pos_ma.value() + self.neg_ma.value();
178 }
179
180 self.previous_close = close;
181
182 if !self.initialized {
183 self.has_inputs = true;
184
185 if self.pos_ma.initialized() {
186 self.initialized = true;
187 }
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use rstest::rstest;
195
196 use super::*;
197 use crate::stubs::rvi_10;
198
199 #[rstest]
200 fn test_name_returns_expected_string(rvi_10: RelativeVolatilityIndex) {
201 assert_eq!(rvi_10.name(), "RelativeVolatilityIndex");
202 }
203
204 #[rstest]
205 fn test_str_repr_returns_expected_string(rvi_10: RelativeVolatilityIndex) {
206 assert_eq!(format!("{rvi_10}"), "RelativeVolatilityIndex(10,10,SIMPLE)");
207 }
208
209 #[rstest]
210 fn test_period_returns_expected_value(rvi_10: RelativeVolatilityIndex) {
211 assert_eq!(rvi_10.period, 10);
212 assert_eq!(rvi_10.scalar, 10.0);
213 assert_eq!(rvi_10.ma_type, MovingAverageType::Simple);
214 }
215
216 #[rstest]
217 fn test_initialized_without_inputs_returns_false(rvi_10: RelativeVolatilityIndex) {
218 assert!(!rvi_10.initialized());
219 }
220
221 #[rstest]
222 fn test_value_with_all_higher_inputs_returns_expected_value(
223 mut rvi_10: RelativeVolatilityIndex,
224 ) {
225 let close_values = [
226 105.25, 107.50, 109.75, 112.00, 114.25, 116.50, 118.75, 121.00, 123.25, 125.50, 127.75,
227 130.00, 132.25, 134.50, 136.75, 139.00, 141.25, 143.50, 145.75, 148.00, 150.25, 152.50,
228 154.75, 157.00, 159.25, 161.50, 163.75, 166.00, 168.25, 170.50,
229 ];
230
231 for close in close_values {
232 rvi_10.update_raw(close);
233 }
234
235 assert!(rvi_10.initialized());
236 assert_eq!(rvi_10.value, 10.0);
237 }
238
239 #[rstest]
240 fn test_prices_window_bounded_to_period(mut rvi_10: RelativeVolatilityIndex) {
241 for i in 0..50 {
245 rvi_10.update_raw(100.0 + f64::from(i));
246 }
247
248 assert!(rvi_10.initialized());
249 assert_eq!(rvi_10.prices.len(), 10);
250 assert_eq!(rvi_10.value, 10.0);
251 }
252
253 #[rstest]
254 fn test_reset_successfully_returns_indicator_to_fresh_state(
255 mut rvi_10: RelativeVolatilityIndex,
256 ) {
257 rvi_10.update_raw(1.00020);
258 rvi_10.update_raw(1.00030);
259 rvi_10.update_raw(1.00070);
260
261 rvi_10.reset();
262
263 assert!(!rvi_10.initialized());
264 assert_eq!(rvi_10.value, 0.0);
265 assert!(!rvi_10.initialized);
266 assert!(!rvi_10.has_inputs);
267 assert_eq!(rvi_10.std, 0.0);
268 assert_eq!(rvi_10.prices.len(), 0);
269 assert_eq!(rvi_10.ma.value(), 0.0);
270 assert_eq!(rvi_10.pos_ma.value(), 0.0);
271 assert_eq!(rvi_10.neg_ma.value(), 0.0);
272 }
273}