nautilus_indicators/volatility/
rvi.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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/// An indicator which calculates a Average True Range (ATR) across a rolling window.
27#[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    /// Creates a new [`RelativeVolatilityIndex`] instance.
93    ///
94    /// # Panics
95    ///
96    /// This function panics if:
97    /// - `period` is not in the range of 1 to 1024 (inclusive).
98    /// - `scalar` is not in the range of 0.0 to 100.0 (inclusive).
99    /// - `ma_type` is not a valid [`MovingAverageType`].
100    #[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////////////////////////////////////////////////////////////////////////////////
174// Tests
175////////////////////////////////////////////////////////////////////////////////
176#[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}