Skip to main content

nautilus_indicators/volatility/
rvi.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.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 Relative Volatility Index (RVI) across a rolling window.
27#[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    /// Creates a new [`RelativeVolatilityIndex`] instance.
97    ///
98    /// # Panics
99    ///
100    /// This function panics if:
101    /// - `period` is not in the range of 1 to 1024 (inclusive).
102    /// - `scalar` is not in the range of 0.0 to 100.0 (inclusive).
103    /// - `ma_type` is not a valid [`MovingAverageType`].
104    #[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}