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)]
33pub struct RelativeVolatilityIndex {
34 pub period: usize,
35 pub scalar: f64,
36 pub ma_type: MovingAverageType,
37 pub value: f64,
38 pub initialized: bool,
39 prices: ArrayDeque<f64, 1024, Wrapping>,
40 ma: Box<dyn MovingAverage + Send + 'static>,
41 pos_ma: Box<dyn MovingAverage + Send + 'static>,
42 neg_ma: Box<dyn MovingAverage + Send + 'static>,
43 previous_close: f64,
44 std: f64,
45 has_inputs: bool,
46}
47
48impl Display for RelativeVolatilityIndex {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 write!(
51 f,
52 "{}({},{},{})",
53 self.name(),
54 self.period,
55 self.scalar,
56 self.ma_type,
57 )
58 }
59}
60
61impl Indicator for RelativeVolatilityIndex {
62 fn name(&self) -> String {
63 stringify!(RelativeVolatilityIndex).to_string()
64 }
65
66 fn has_inputs(&self) -> bool {
67 self.has_inputs
68 }
69
70 fn initialized(&self) -> bool {
71 self.initialized
72 }
73
74 fn handle_bar(&mut self, bar: &Bar) {
75 self.update_raw((&bar.close).into());
76 }
77
78 fn reset(&mut self) {
79 self.previous_close = 0.0;
80 self.value = 0.0;
81 self.has_inputs = false;
82 self.initialized = false;
83 self.std = 0.0;
84 self.prices.clear();
85 self.ma.reset();
86 self.pos_ma.reset();
87 self.neg_ma.reset();
88 }
89}
90
91impl RelativeVolatilityIndex {
92 #[must_use]
101 pub fn new(period: usize, scalar: Option<f64>, ma_type: Option<MovingAverageType>) -> Self {
102 assert!(
103 period <= 1024,
104 "period {period} exceeds maximum capacity of price deque"
105 );
106
107 Self {
108 period,
109 scalar: scalar.unwrap_or(100.0),
110 ma_type: ma_type.unwrap_or(MovingAverageType::Simple),
111 value: 0.0,
112 initialized: false,
113 prices: ArrayDeque::new(),
114 ma: MovingAverageFactory::create(ma_type.unwrap_or(MovingAverageType::Simple), period),
115 pos_ma: MovingAverageFactory::create(
116 ma_type.unwrap_or(MovingAverageType::Simple),
117 period,
118 ),
119 neg_ma: MovingAverageFactory::create(
120 ma_type.unwrap_or(MovingAverageType::Simple),
121 period,
122 ),
123 previous_close: 0.0,
124 std: 0.0,
125 has_inputs: false,
126 }
127 }
128
129 pub fn update_raw(&mut self, close: f64) {
130 self.prices.push_back(close);
131 self.ma.update_raw(close);
132
133 if self.prices.is_empty() {
134 self.std = 0.0;
135 } else {
136 let mean = self.ma.value();
137 let mut var_sum = 0.0;
138 for &price in &self.prices {
139 let diff = price - mean;
140 var_sum += diff * diff;
141 }
142 self.std = (var_sum / self.prices.len() as f64).sqrt();
143 self.std = self.std * (self.period as f64).sqrt() / ((self.period - 1) as f64).sqrt();
144 }
145
146 if self.ma.initialized() {
147 if close > self.previous_close {
148 self.pos_ma.update_raw(self.std);
149 self.neg_ma.update_raw(0.0);
150 } else if close < self.previous_close {
151 self.pos_ma.update_raw(0.0);
152 self.neg_ma.update_raw(self.std);
153 } else {
154 self.pos_ma.update_raw(0.0);
155 self.neg_ma.update_raw(0.0);
156 }
157
158 self.value = self.scalar * self.pos_ma.value();
159 self.value /= self.pos_ma.value() + self.neg_ma.value();
160 }
161
162 self.previous_close = close;
163
164 if !self.initialized {
165 self.has_inputs = true;
166 if self.pos_ma.initialized() {
167 self.initialized = true;
168 }
169 }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use rstest::rstest;
176
177 use super::*;
178 use crate::stubs::rvi_10;
179
180 #[rstest]
181 fn test_name_returns_expected_string(rvi_10: RelativeVolatilityIndex) {
182 assert_eq!(rvi_10.name(), "RelativeVolatilityIndex");
183 }
184
185 #[rstest]
186 fn test_str_repr_returns_expected_string(rvi_10: RelativeVolatilityIndex) {
187 assert_eq!(format!("{rvi_10}"), "RelativeVolatilityIndex(10,10,SIMPLE)");
188 }
189
190 #[rstest]
191 fn test_period_returns_expected_value(rvi_10: RelativeVolatilityIndex) {
192 assert_eq!(rvi_10.period, 10);
193 assert_eq!(rvi_10.scalar, 10.0);
194 assert_eq!(rvi_10.ma_type, MovingAverageType::Simple);
195 }
196
197 #[rstest]
198 fn test_initialized_without_inputs_returns_false(rvi_10: RelativeVolatilityIndex) {
199 assert!(!rvi_10.initialized());
200 }
201
202 #[rstest]
203 fn test_value_with_all_higher_inputs_returns_expected_value(
204 mut rvi_10: RelativeVolatilityIndex,
205 ) {
206 let close_values = [
207 105.25, 107.50, 109.75, 112.00, 114.25, 116.50, 118.75, 121.00, 123.25, 125.50, 127.75,
208 130.00, 132.25, 134.50, 136.75, 139.00, 141.25, 143.50, 145.75, 148.00, 150.25, 152.50,
209 154.75, 157.00, 159.25, 161.50, 163.75, 166.00, 168.25, 170.50,
210 ];
211
212 for close in close_values {
213 rvi_10.update_raw(close);
214 }
215
216 assert!(rvi_10.initialized());
217 assert_eq!(rvi_10.value, 10.0);
218 }
219
220 #[rstest]
221 fn test_reset_successfully_returns_indicator_to_fresh_state(
222 mut rvi_10: RelativeVolatilityIndex,
223 ) {
224 rvi_10.update_raw(1.00020);
225 rvi_10.update_raw(1.00030);
226 rvi_10.update_raw(1.00070);
227
228 rvi_10.reset();
229
230 assert!(!rvi_10.initialized());
231 assert_eq!(rvi_10.value, 0.0);
232 assert!(!rvi_10.initialized);
233 assert!(!rvi_10.has_inputs);
234 assert_eq!(rvi_10.std, 0.0);
235 assert_eq!(rvi_10.prices.len(), 0);
236 assert_eq!(rvi_10.ma.value(), 0.0);
237 assert_eq!(rvi_10.pos_ma.value(), 0.0);
238 assert_eq!(rvi_10.neg_ma.value(), 0.0);
239 }
240}