nautilus_indicators/volatility/
rvi.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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::{
17    collections::VecDeque,
18    fmt::{Debug, Display},
19};
20
21use nautilus_model::data::Bar;
22
23use crate::{
24    average::{MovingAverageFactory, MovingAverageType},
25    indicator::{Indicator, MovingAverage},
26    momentum::bb::fast_std_with_mean,
27};
28
29/// An indicator which calculates a Average True Range (ATR) across a rolling window.
30#[repr(C)]
31#[derive(Debug)]
32#[cfg_attr(
33    feature = "python",
34    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators", unsendable)
35)]
36pub struct RelativeVolatilityIndex {
37    pub period: usize,
38    pub scalar: f64,
39    pub ma_type: MovingAverageType,
40    pub value: f64,
41    pub initialized: bool,
42    prices: VecDeque<f64>,
43    ma: Box<dyn MovingAverage + Send + 'static>,
44    pos_ma: Box<dyn MovingAverage + Send + 'static>,
45    neg_ma: Box<dyn MovingAverage + Send + 'static>,
46    previous_close: f64,
47    std: f64,
48    has_inputs: bool,
49}
50
51impl Display for RelativeVolatilityIndex {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        write!(
54            f,
55            "{}({},{},{})",
56            self.name(),
57            self.period,
58            self.scalar,
59            self.ma_type,
60        )
61    }
62}
63
64impl Indicator for RelativeVolatilityIndex {
65    fn name(&self) -> String {
66        stringify!(RelativeVolatilityIndex).to_string()
67    }
68
69    fn has_inputs(&self) -> bool {
70        self.has_inputs
71    }
72
73    fn initialized(&self) -> bool {
74        self.initialized
75    }
76
77    fn handle_bar(&mut self, bar: &Bar) {
78        self.update_raw((&bar.close).into());
79    }
80
81    fn reset(&mut self) {
82        self.previous_close = 0.0;
83        self.value = 0.0;
84        self.has_inputs = false;
85        self.initialized = false;
86        self.std = 0.0;
87        self.prices.clear();
88        self.ma.reset();
89        self.pos_ma.reset();
90        self.neg_ma.reset();
91    }
92}
93
94impl RelativeVolatilityIndex {
95    /// Creates a new [`RelativeVolatilityIndex`] instance.
96    #[must_use]
97    pub fn new(period: usize, scalar: Option<f64>, ma_type: Option<MovingAverageType>) -> Self {
98        Self {
99            period,
100            scalar: scalar.unwrap_or(100.0),
101            ma_type: ma_type.unwrap_or(MovingAverageType::Simple),
102            value: 0.0,
103            initialized: false,
104            prices: VecDeque::with_capacity(period),
105            ma: MovingAverageFactory::create(ma_type.unwrap_or(MovingAverageType::Simple), period),
106            pos_ma: MovingAverageFactory::create(
107                ma_type.unwrap_or(MovingAverageType::Simple),
108                period,
109            ),
110            neg_ma: MovingAverageFactory::create(
111                ma_type.unwrap_or(MovingAverageType::Simple),
112                period,
113            ),
114            previous_close: 0.0,
115            std: 0.0,
116            has_inputs: false,
117        }
118    }
119
120    pub fn update_raw(&mut self, close: f64) {
121        self.prices.push_back(close);
122        self.ma.update_raw(close);
123
124        self.std = fast_std_with_mean(self.prices.clone(), self.ma.value());
125        self.std = self.std * (self.period as f64).sqrt() / ((self.period - 1) as f64).sqrt();
126
127        if self.ma.initialized() {
128            if close > self.previous_close {
129                self.pos_ma.update_raw(self.std);
130                self.neg_ma.update_raw(0.0);
131            } else if close < self.previous_close {
132                self.pos_ma.update_raw(0.0);
133                self.neg_ma.update_raw(self.std);
134            } else {
135                self.pos_ma.update_raw(0.0);
136                self.neg_ma.update_raw(0.0);
137            }
138
139            self.value = self.scalar * self.pos_ma.value();
140            self.value /= self.pos_ma.value() + self.neg_ma.value();
141        }
142
143        self.previous_close = close;
144
145        // Initialization logic
146        if !self.initialized {
147            self.has_inputs = true;
148            if self.pos_ma.initialized() {
149                self.initialized = true;
150            }
151        }
152    }
153}
154
155////////////////////////////////////////////////////////////////////////////////
156// Tests
157////////////////////////////////////////////////////////////////////////////////
158#[cfg(test)]
159mod tests {
160    use rstest::rstest;
161
162    use super::*;
163    use crate::stubs::rvi_10;
164
165    #[rstest]
166    fn test_name_returns_expected_string(rvi_10: RelativeVolatilityIndex) {
167        assert_eq!(rvi_10.name(), "RelativeVolatilityIndex");
168    }
169
170    #[rstest]
171    fn test_str_repr_returns_expected_string(rvi_10: RelativeVolatilityIndex) {
172        assert_eq!(format!("{rvi_10}"), "RelativeVolatilityIndex(10,10,SIMPLE)");
173    }
174
175    #[rstest]
176    fn test_period_returns_expected_value(rvi_10: RelativeVolatilityIndex) {
177        assert_eq!(rvi_10.period, 10);
178        assert_eq!(rvi_10.scalar, 10.0);
179        assert_eq!(rvi_10.ma_type, MovingAverageType::Simple);
180    }
181
182    #[rstest]
183    fn test_initialized_without_inputs_returns_false(rvi_10: RelativeVolatilityIndex) {
184        assert!(!rvi_10.initialized());
185    }
186
187    #[rstest]
188    fn test_value_with_all_higher_inputs_returns_expected_value(
189        mut rvi_10: RelativeVolatilityIndex,
190    ) {
191        let close_values = [
192            105.25, 107.50, 109.75, 112.00, 114.25, 116.50, 118.75, 121.00, 123.25, 125.50, 127.75,
193            130.00, 132.25, 134.50, 136.75, 139.00, 141.25, 143.50, 145.75, 148.00, 150.25, 152.50,
194            154.75, 157.00, 159.25, 161.50, 163.75, 166.00, 168.25, 170.50,
195        ];
196
197        for close in close_values {
198            rvi_10.update_raw(close);
199        }
200
201        assert!(rvi_10.initialized());
202        assert_eq!(rvi_10.value, 10.0);
203    }
204
205    #[rstest]
206    fn test_reset_successfully_returns_indicator_to_fresh_state(
207        mut rvi_10: RelativeVolatilityIndex,
208    ) {
209        rvi_10.update_raw(1.00020);
210        rvi_10.update_raw(1.00030);
211        rvi_10.update_raw(1.00070);
212
213        rvi_10.reset();
214
215        assert!(!rvi_10.initialized());
216        assert_eq!(rvi_10.value, 0.0);
217        assert!(!rvi_10.initialized);
218        assert!(!rvi_10.has_inputs);
219        assert_eq!(rvi_10.std, 0.0);
220        assert_eq!(rvi_10.prices.len(), 0);
221        assert_eq!(rvi_10.ma.value(), 0.0);
222        assert_eq!(rvi_10.pos_ma.value(), 0.0);
223        assert_eq!(rvi_10.neg_ma.value(), 0.0);
224    }
225}