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 = "posei_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)]
177mod tests {
178 use rstest::rstest;
179
180 use super::*;
181 use crate::stubs::rvi_10;
182
183 #[rstest]
184 fn test_name_returns_expected_string(rvi_10: RelativeVolatilityIndex) {
185 assert_eq!(rvi_10.name(), "RelativeVolatilityIndex");
186 }
187
188 #[rstest]
189 fn test_str_repr_returns_expected_string(rvi_10: RelativeVolatilityIndex) {
190 assert_eq!(format!("{rvi_10}"), "RelativeVolatilityIndex(10,10,SIMPLE)");
191 }
192
193 #[rstest]
194 fn test_period_returns_expected_value(rvi_10: RelativeVolatilityIndex) {
195 assert_eq!(rvi_10.period, 10);
196 assert_eq!(rvi_10.scalar, 10.0);
197 assert_eq!(rvi_10.ma_type, MovingAverageType::Simple);
198 }
199
200 #[rstest]
201 fn test_initialized_without_inputs_returns_false(rvi_10: RelativeVolatilityIndex) {
202 assert!(!rvi_10.initialized());
203 }
204
205 #[rstest]
206 fn test_value_with_all_higher_inputs_returns_expected_value(
207 mut rvi_10: RelativeVolatilityIndex,
208 ) {
209 let close_values = [
210 105.25, 107.50, 109.75, 112.00, 114.25, 116.50, 118.75, 121.00, 123.25, 125.50, 127.75,
211 130.00, 132.25, 134.50, 136.75, 139.00, 141.25, 143.50, 145.75, 148.00, 150.25, 152.50,
212 154.75, 157.00, 159.25, 161.50, 163.75, 166.00, 168.25, 170.50,
213 ];
214
215 for close in close_values {
216 rvi_10.update_raw(close);
217 }
218
219 assert!(rvi_10.initialized());
220 assert_eq!(rvi_10.value, 10.0);
221 }
222
223 #[rstest]
224 fn test_reset_successfully_returns_indicator_to_fresh_state(
225 mut rvi_10: RelativeVolatilityIndex,
226 ) {
227 rvi_10.update_raw(1.00020);
228 rvi_10.update_raw(1.00030);
229 rvi_10.update_raw(1.00070);
230
231 rvi_10.reset();
232
233 assert!(!rvi_10.initialized());
234 assert_eq!(rvi_10.value, 0.0);
235 assert!(!rvi_10.initialized);
236 assert!(!rvi_10.has_inputs);
237 assert_eq!(rvi_10.std, 0.0);
238 assert_eq!(rvi_10.prices.len(), 0);
239 assert_eq!(rvi_10.ma.value(), 0.0);
240 assert_eq!(rvi_10.pos_ma.value(), 0.0);
241 assert_eq!(rvi_10.neg_ma.value(), 0.0);
242 }
243}