nautilus_indicators/momentum/
rsi.rs1use 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#[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 #[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 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}