nautilus_indicators/volatility/
rvi.rs1use std::fmt::{Debug, Display};
17
18use arraydeque::{ArrayDeque, Wrapping};
19use nautilus_model::data::Bar;
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.core.nautilus_pyo3.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_bar(&mut self, bar: &Bar) {
79 self.update_raw((&bar.close).into());
80 }
81
82 fn reset(&mut self) {
83 self.previous_close = 0.0;
84 self.value = 0.0;
85 self.has_inputs = false;
86 self.initialized = false;
87 self.std = 0.0;
88 self.prices.clear();
89 self.ma.reset();
90 self.pos_ma.reset();
91 self.neg_ma.reset();
92 }
93}
94
95impl RelativeVolatilityIndex {
96 #[must_use]
105 pub fn new(period: usize, scalar: Option<f64>, ma_type: Option<MovingAverageType>) -> Self {
106 assert!(
107 period <= 1024,
108 "period {period} exceeds maximum capacity of price deque"
109 );
110
111 Self {
112 period,
113 scalar: scalar.unwrap_or(100.0),
114 ma_type: ma_type.unwrap_or(MovingAverageType::Simple),
115 value: 0.0,
116 initialized: false,
117 prices: ArrayDeque::new(),
118 ma: MovingAverageFactory::create(ma_type.unwrap_or(MovingAverageType::Simple), period),
119 pos_ma: MovingAverageFactory::create(
120 ma_type.unwrap_or(MovingAverageType::Simple),
121 period,
122 ),
123 neg_ma: MovingAverageFactory::create(
124 ma_type.unwrap_or(MovingAverageType::Simple),
125 period,
126 ),
127 previous_close: 0.0,
128 std: 0.0,
129 has_inputs: false,
130 }
131 }
132
133 pub fn update_raw(&mut self, close: f64) {
134 self.prices.push_back(close);
135 self.ma.update_raw(close);
136
137 if self.prices.is_empty() {
138 self.std = 0.0;
139 } else {
140 let mean = self.ma.value();
141 let mut var_sum = 0.0;
142
143 for &price in &self.prices {
144 let diff = price - mean;
145 var_sum += diff * diff;
146 }
147 self.std = (var_sum / self.prices.len() as f64).sqrt();
148 self.std = self.std * (self.period as f64).sqrt() / ((self.period - 1) as f64).sqrt();
149 }
150
151 if self.ma.initialized() {
152 if close > self.previous_close {
153 self.pos_ma.update_raw(self.std);
154 self.neg_ma.update_raw(0.0);
155 } else if close < self.previous_close {
156 self.pos_ma.update_raw(0.0);
157 self.neg_ma.update_raw(self.std);
158 } else {
159 self.pos_ma.update_raw(0.0);
160 self.neg_ma.update_raw(0.0);
161 }
162
163 self.value = self.scalar * self.pos_ma.value();
164 self.value /= self.pos_ma.value() + self.neg_ma.value();
165 }
166
167 self.previous_close = close;
168
169 if !self.initialized {
170 self.has_inputs = true;
171
172 if self.pos_ma.initialized() {
173 self.initialized = true;
174 }
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use rstest::rstest;
182
183 use super::*;
184 use crate::stubs::rvi_10;
185
186 #[rstest]
187 fn test_name_returns_expected_string(rvi_10: RelativeVolatilityIndex) {
188 assert_eq!(rvi_10.name(), "RelativeVolatilityIndex");
189 }
190
191 #[rstest]
192 fn test_str_repr_returns_expected_string(rvi_10: RelativeVolatilityIndex) {
193 assert_eq!(format!("{rvi_10}"), "RelativeVolatilityIndex(10,10,SIMPLE)");
194 }
195
196 #[rstest]
197 fn test_period_returns_expected_value(rvi_10: RelativeVolatilityIndex) {
198 assert_eq!(rvi_10.period, 10);
199 assert_eq!(rvi_10.scalar, 10.0);
200 assert_eq!(rvi_10.ma_type, MovingAverageType::Simple);
201 }
202
203 #[rstest]
204 fn test_initialized_without_inputs_returns_false(rvi_10: RelativeVolatilityIndex) {
205 assert!(!rvi_10.initialized());
206 }
207
208 #[rstest]
209 fn test_value_with_all_higher_inputs_returns_expected_value(
210 mut rvi_10: RelativeVolatilityIndex,
211 ) {
212 let close_values = [
213 105.25, 107.50, 109.75, 112.00, 114.25, 116.50, 118.75, 121.00, 123.25, 125.50, 127.75,
214 130.00, 132.25, 134.50, 136.75, 139.00, 141.25, 143.50, 145.75, 148.00, 150.25, 152.50,
215 154.75, 157.00, 159.25, 161.50, 163.75, 166.00, 168.25, 170.50,
216 ];
217
218 for close in close_values {
219 rvi_10.update_raw(close);
220 }
221
222 assert!(rvi_10.initialized());
223 assert_eq!(rvi_10.value, 10.0);
224 }
225
226 #[rstest]
227 fn test_reset_successfully_returns_indicator_to_fresh_state(
228 mut rvi_10: RelativeVolatilityIndex,
229 ) {
230 rvi_10.update_raw(1.00020);
231 rvi_10.update_raw(1.00030);
232 rvi_10.update_raw(1.00070);
233
234 rvi_10.reset();
235
236 assert!(!rvi_10.initialized());
237 assert_eq!(rvi_10.value, 0.0);
238 assert!(!rvi_10.initialized);
239 assert!(!rvi_10.has_inputs);
240 assert_eq!(rvi_10.std, 0.0);
241 assert_eq!(rvi_10.prices.len(), 0);
242 assert_eq!(rvi_10.ma.value(), 0.0);
243 assert_eq!(rvi_10.pos_ma.value(), 0.0);
244 assert_eq!(rvi_10.neg_ma.value(), 0.0);
245 }
246}