Skip to main content

nautilus_indicators/momentum/
rsi.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 nautilus_core::correctness::FAILED;
19use nautilus_model::{
20    data::{Bar, QuoteTick, TradeTick},
21    enums::PriceType,
22};
23
24use crate::{
25    average::{MovingAverageFactory, MovingAverageType},
26    indicator::{Indicator, MovingAverage},
27    support::MAX_PERIOD,
28};
29
30/// An indicator which calculates a relative strength index (RSI) across a rolling window.
31#[repr(C)]
32#[derive(Debug)]
33#[cfg_attr(
34    feature = "python",
35    pyo3::pyclass(module = "nautilus_trader.indicators", unsendable)
36)]
37#[cfg_attr(
38    feature = "python",
39    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
40)]
41pub struct RelativeStrengthIndex {
42    pub period: usize,
43    pub ma_type: MovingAverageType,
44    pub value: f64,
45    pub count: usize,
46    pub initialized: bool,
47    has_inputs: bool,
48    last_value: f64,
49    average_gain: Box<dyn MovingAverage + Send + 'static>,
50    average_loss: Box<dyn MovingAverage + Send + 'static>,
51    rsi_max: f64,
52}
53
54impl Display for RelativeStrengthIndex {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        write!(f, "{}({},{})", self.name(), self.period, self.ma_type)
57    }
58}
59
60impl Indicator for RelativeStrengthIndex {
61    fn name(&self) -> String {
62        stringify!(RelativeStrengthIndex).to_string()
63    }
64
65    fn has_inputs(&self) -> bool {
66        self.has_inputs
67    }
68
69    fn initialized(&self) -> bool {
70        self.initialized
71    }
72
73    fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
74        self.update_raw(quote.extract_price(PriceType::Mid)?.into());
75        Ok(())
76    }
77
78    fn handle_trade(&mut self, trade: &TradeTick) {
79        self.update_raw((trade.price).into());
80    }
81
82    fn handle_bar(&mut self, bar: &Bar) {
83        self.update_raw((&bar.close).into());
84    }
85
86    fn reset(&mut self) {
87        self.value = 0.0;
88        self.last_value = 0.0;
89        self.count = 0;
90        self.has_inputs = false;
91        self.initialized = false;
92        self.average_gain.reset();
93        self.average_loss.reset();
94    }
95}
96
97impl RelativeStrengthIndex {
98    /// Creates a new [`RelativeStrengthIndex`] instance.
99    ///
100    /// # Panics
101    ///
102    /// Panics if `period` is zero or exceeds 16,777,216.
103    #[must_use]
104    pub fn new(period: usize, ma_type: Option<MovingAverageType>) -> Self {
105        Self::new_checked(period, ma_type).expect(FAILED)
106    }
107
108    pub(crate) fn new_checked(
109        period: usize,
110        ma_type: Option<MovingAverageType>,
111    ) -> anyhow::Result<Self> {
112        anyhow::ensure!(
113            (1..=MAX_PERIOD).contains(&period),
114            "period must be in 1..={MAX_PERIOD}"
115        );
116        let ma_type = ma_type.unwrap_or(MovingAverageType::Wilder);
117        Ok(Self {
118            period,
119            ma_type,
120            value: 0.0,
121            last_value: 0.0,
122            count: 0,
123            has_inputs: false,
124            average_gain: MovingAverageFactory::create(ma_type, period),
125            average_loss: MovingAverageFactory::create(ma_type, period),
126            rsi_max: 100.0,
127            initialized: false,
128        })
129    }
130
131    pub fn update_raw(&mut self, value: f64) {
132        if !value.is_finite() {
133            return;
134        }
135
136        self.count += 1;
137        self.has_inputs = true;
138
139        if self.count == 1 {
140            self.last_value = value;
141            return;
142        }
143
144        let change = value - self.last_value;
145        self.last_value = value;
146        self.average_gain.update_raw(change.max(0.0));
147        self.average_loss.update_raw((-change).max(0.0));
148        if !self.average_gain.initialized() || !self.average_loss.initialized() {
149            return;
150        }
151
152        let average_gain = self.average_gain.value();
153        let average_loss = self.average_loss.value();
154        let total = average_gain + average_loss;
155        self.value = if total == 0.0 {
156            50.0
157        } else {
158            self.rsi_max * (average_gain / total)
159        };
160        self.initialized = true;
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use nautilus_model::data::{Bar, QuoteTick, TradeTick};
167    use rstest::rstest;
168
169    use crate::{
170        average::MovingAverageType, indicator::Indicator, momentum::rsi::RelativeStrengthIndex,
171        stubs::*, testing::assert_approx_equal,
172    };
173
174    #[rstest]
175    fn test_rsi_initialized(rsi_10: RelativeStrengthIndex) {
176        let display_str = format!("{rsi_10}");
177        assert_eq!(display_str, "RelativeStrengthIndex(10,EXPONENTIAL)");
178        assert_eq!(rsi_10.period, 10);
179        assert!(!rsi_10.initialized);
180    }
181
182    #[rstest]
183    fn test_initialized_with_required_inputs_returns_true(mut rsi_10: RelativeStrengthIndex) {
184        for i in 0..12 {
185            rsi_10.update_raw(f64::from(i));
186        }
187        assert!(rsi_10.initialized);
188    }
189
190    #[rstest]
191    fn test_value_with_one_input_returns_expected_value(mut rsi_10: RelativeStrengthIndex) {
192        rsi_10.update_raw(1.0);
193        assert_eq!(rsi_10.value, 0.0);
194        assert!(!rsi_10.initialized());
195    }
196
197    #[rstest]
198    fn test_value_all_higher_inputs_returns_expected_value() {
199        let mut rsi_10 = RelativeStrengthIndex::new(3, None);
200        for value in [1.0, 2.0, 3.0, 4.0] {
201            rsi_10.update_raw(value);
202        }
203
204        assert_eq!(rsi_10.value, 100.0);
205        assert!(rsi_10.initialized());
206    }
207
208    #[rstest]
209    fn test_value_with_all_lower_inputs_returns_expected_value() {
210        let mut rsi_10 = RelativeStrengthIndex::new(3, None);
211        for value in [4.0, 3.0, 2.0, 1.0] {
212            rsi_10.update_raw(value);
213        }
214
215        assert_eq!(rsi_10.value, 0.0);
216        assert!(rsi_10.initialized());
217    }
218
219    #[rstest]
220    fn test_value_with_various_input_returns_expected_value() {
221        let mut rsi_10 = RelativeStrengthIndex::new(3, None);
222        for value in [1.0, 2.0, 1.0, 3.0] {
223            rsi_10.update_raw(value);
224        }
225
226        assert_eq!(rsi_10.value, 75.0);
227    }
228
229    #[rstest]
230    fn test_value_at_returns_expected_value() {
231        let mut rsi_10 = RelativeStrengthIndex::new(3, None);
232        for value in [1.0, 2.0, 1.0, 3.0, 2.0] {
233            rsi_10.update_raw(value);
234        }
235
236        assert_approx_equal(rsi_10.value, 54.545_454_545_454_55);
237    }
238
239    #[rstest]
240    fn test_reset(mut rsi_10: RelativeStrengthIndex) {
241        rsi_10.update_raw(1.0);
242        rsi_10.update_raw(2.0);
243        rsi_10.reset();
244        assert!(!rsi_10.initialized());
245        assert_eq!(rsi_10.count, 0);
246    }
247
248    #[rstest]
249    fn test_reset_resets_inner_mas(mut rsi_10: RelativeStrengthIndex) {
250        rsi_10.update_raw(1.0);
251        rsi_10.update_raw(2.0);
252        rsi_10.reset();
253        assert_eq!(rsi_10.average_gain.count(), 0);
254        assert_eq!(rsi_10.average_loss.count(), 0);
255    }
256
257    #[rstest]
258    fn test_handle_quote_tick(mut rsi_10: RelativeStrengthIndex, stub_quote: QuoteTick) {
259        rsi_10.handle_quote(&stub_quote).unwrap();
260        assert!(rsi_10.has_inputs());
261        assert_eq!(rsi_10.count, 1);
262        assert_eq!(rsi_10.value, 0.0);
263        assert!(!rsi_10.initialized());
264    }
265
266    #[rstest]
267    fn test_handle_trade_tick(mut rsi_10: RelativeStrengthIndex, stub_trade: TradeTick) {
268        rsi_10.handle_trade(&stub_trade);
269        assert!(rsi_10.has_inputs());
270        assert_eq!(rsi_10.count, 1);
271        assert_eq!(rsi_10.value, 0.0);
272        assert!(!rsi_10.initialized());
273    }
274
275    #[rstest]
276    fn test_handle_bar(mut rsi_10: RelativeStrengthIndex, bar_ethusdt_binance_minute_bid: Bar) {
277        rsi_10.handle_bar(&bar_ethusdt_binance_minute_bid);
278        assert!(rsi_10.has_inputs());
279        assert_eq!(rsi_10.count, 1);
280        assert_eq!(rsi_10.value, 0.0);
281        assert!(!rsi_10.initialized());
282    }
283
284    #[rstest]
285    fn test_constant_inputs_initializes_and_value_max() {
286        let mut rsi = RelativeStrengthIndex::new(3, None);
287        for _ in 0..4 {
288            rsi.update_raw(42.0);
289        }
290
291        assert!(rsi.initialized());
292        assert_eq!(rsi.value, 50.0);
293    }
294
295    #[rstest]
296    fn test_reset_resets_has_inputs_and_value(mut rsi_10: RelativeStrengthIndex) {
297        rsi_10.update_raw(1.0);
298        rsi_10.reset();
299        assert!(!rsi_10.has_inputs());
300        assert_eq!(rsi_10.value, 0.0);
301    }
302
303    // Feeds `values` through a fresh RSI of the given `ma_type` and returns the final value.
304    fn run_rsi(values: &[f64], period: usize, ma_type: MovingAverageType) -> f64 {
305        let mut rsi = RelativeStrengthIndex::new(period, Some(ma_type));
306        for &v in values {
307            rsi.update_raw(v);
308        }
309        rsi.value
310    }
311
312    #[rstest]
313    fn test_ma_type_is_plumbed_into_inner_averages() {
314        let prices = [1.0, 2.0, 1.0, 3.0, 2.0, 4.0];
315        let simple = run_rsi(&prices, 3, MovingAverageType::Simple);
316        let wilder = run_rsi(&prices, 3, MovingAverageType::Wilder);
317
318        assert_ne!(simple, wilder);
319    }
320
321    #[rstest]
322    fn test_recovers_below_max_after_losses() {
323        let mut rsi = RelativeStrengthIndex::new(3, None);
324        for value in [1.0, 2.0, 3.0, 4.0, 3.0] {
325            rsi.update_raw(value);
326        }
327
328        assert!(rsi.value < 100.0);
329        assert!(rsi.value > 0.0);
330    }
331
332    #[rstest]
333    fn test_wilder_smoothed_series() {
334        let mut rsi = RelativeStrengthIndex::new(3, None);
335        let inputs = [1.0, 2.0, 1.0, 3.0, 2.0];
336        let expected = [0.0, 0.0, 0.0, 75.0, 54.545_454_545_454_55];
337        for (input, expected) in inputs.into_iter().zip(expected) {
338            rsi.update_raw(input);
339            assert_approx_equal(rsi.value, expected);
340        }
341    }
342}