nautilus_indicators/momentum/
roc.rs1use std::{collections::VecDeque, fmt::Display};
17
18use nautilus_core::correctness::FAILED;
19use nautilus_model::data::{Bar, QuoteTick, TradeTick};
20
21use crate::{indicator::Indicator, support::MAX_PERIOD};
22
23#[repr(C)]
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
26#[cfg_attr(
27 feature = "python",
28 pyo3::pyclass(
29 frozen,
30 eq,
31 eq_int,
32 module = "nautilus_trader.indicators",
33 from_py_object
34 )
35)]
36#[cfg_attr(
37 feature = "python",
38 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.indicators")
39)]
40pub enum RateOfChangeMode {
41 #[default]
43 Percentage,
44 Fraction,
46 Ratio,
48 RatioPercent,
50 Log,
52}
53
54#[repr(C)]
56#[derive(Debug)]
57#[cfg_attr(
58 feature = "python",
59 pyo3::pyclass(module = "nautilus_trader.indicators")
60)]
61#[cfg_attr(
62 feature = "python",
63 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
64)]
65pub struct RateOfChange {
66 pub period: usize,
67 pub use_log: bool,
68 pub mode: RateOfChangeMode,
69 pub value: f64,
70 pub initialized: bool,
71 has_inputs: bool,
72 prices: VecDeque<f64>,
73}
74
75impl Display for RateOfChange {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 write!(f, "{}({})", self.name(), self.period)
78 }
79}
80
81impl Indicator for RateOfChange {
82 fn name(&self) -> String {
83 stringify!(RateOfChange).to_string()
84 }
85
86 fn has_inputs(&self) -> bool {
87 self.has_inputs
88 }
89
90 fn initialized(&self) -> bool {
91 self.initialized
92 }
93
94 fn handle_quote(&mut self, _quote: &QuoteTick) -> anyhow::Result<()> {
95 Ok(())
96 }
97
98 fn handle_trade(&mut self, _trade: &TradeTick) {}
99
100 fn handle_bar(&mut self, bar: &Bar) {
101 self.update_raw((&bar.close).into());
102 }
103
104 fn reset(&mut self) {
105 self.prices.clear();
106 self.value = 0.0;
107 self.has_inputs = false;
108 self.initialized = false;
109 }
110}
111
112impl RateOfChange {
113 #[must_use]
119 pub fn new(period: usize, use_log: Option<bool>) -> Self {
120 let mode = if use_log.unwrap_or(false) {
121 RateOfChangeMode::Log
122 } else {
123 RateOfChangeMode::Percentage
124 };
125 Self::new_checked(period, mode).expect(FAILED)
126 }
127
128 pub(crate) fn new_checked(period: usize, mode: RateOfChangeMode) -> anyhow::Result<Self> {
129 anyhow::ensure!(
130 (1..=MAX_PERIOD).contains(&period),
131 "period must be in 1..={MAX_PERIOD}"
132 );
133 Ok(Self {
134 period,
135 use_log: mode == RateOfChangeMode::Log,
136 mode,
137 value: 0.0,
138 prices: VecDeque::with_capacity(period + 1),
139 has_inputs: false,
140 initialized: false,
141 })
142 }
143
144 #[must_use]
150 pub fn new_with_mode(period: usize, mode: RateOfChangeMode) -> Self {
151 Self::new_checked(period, mode).expect(FAILED)
152 }
153
154 pub fn update_raw(&mut self, price: f64) {
155 if !price.is_finite() || (self.mode == RateOfChangeMode::Log && price <= 0.0) {
156 return;
157 }
158 if self.prices.len() == self.period + 1 {
161 let _ = self.prices.pop_front();
162 }
163 self.prices.push_back(price);
164
165 if !self.initialized {
166 self.has_inputs = true;
167
168 if self.prices.len() > self.period {
169 self.initialized = true;
170 }
171 }
172
173 if !self.initialized {
174 return;
175 }
176
177 if let Some(first) = self.prices.front() {
178 if *first == 0.0 {
179 self.value = 0.0;
180 return;
181 }
182 let ratio = price / first;
183 self.value = match self.mode {
184 RateOfChangeMode::Percentage => 100.0 * (ratio - 1.0),
185 RateOfChangeMode::Fraction => ratio - 1.0,
186 RateOfChangeMode::Ratio => ratio,
187 RateOfChangeMode::RatioPercent => 100.0 * ratio,
188 RateOfChangeMode::Log => (price / first).ln(),
189 };
190 }
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use rstest::rstest;
197
198 use super::*;
199 use crate::{stubs::roc_10, testing::assert_approx_equal};
200
201 #[rstest]
202 fn test_name_returns_expected_string(roc_10: RateOfChange) {
203 assert_eq!(roc_10.name(), "RateOfChange");
204 }
205
206 #[rstest]
207 fn test_str_repr_returns_expected_string(roc_10: RateOfChange) {
208 assert_eq!(format!("{roc_10}"), "RateOfChange(10)");
209 }
210
211 #[rstest]
212 fn test_period_returns_expected_value(roc_10: RateOfChange) {
213 assert_eq!(roc_10.period, 10);
214 assert!(roc_10.use_log);
215 }
216
217 #[rstest]
218 fn test_initialized_without_inputs_returns_false(roc_10: RateOfChange) {
219 assert!(!roc_10.initialized());
220 }
221
222 #[rstest]
223 fn test_value_with_all_higher_inputs_returns_expected_value(mut roc_10: RateOfChange) {
224 let close_values = [
225 0.95, 1.95, 2.95, 3.95, 4.95, 5.95, 6.95, 7.95, 8.95, 9.95, 10.05, 10.15, 10.25, 11.05,
226 11.45,
227 ];
228
229 for close in &close_values {
230 roc_10.update_raw(*close);
231 }
232
233 assert!(roc_10.initialized());
234 assert_approx_equal(roc_10.value, 0.838_602_153_419_649_5);
236 }
237
238 #[rstest]
239 fn test_reset_successfully_returns_indicator_to_fresh_state(mut roc_10: RateOfChange) {
240 roc_10.update_raw(1.00020);
241 roc_10.update_raw(1.00030);
242 roc_10.update_raw(1.00070);
243
244 roc_10.reset();
245
246 assert!(!roc_10.initialized());
247 assert!(!roc_10.has_inputs);
248 assert_eq!(roc_10.value, 0.0);
249 }
250
251 #[rstest]
252 fn test_value_respects_period_window() {
253 let mut roc = RateOfChange::new(3, Some(false));
254
255 roc.update_raw(100.0);
256 roc.update_raw(1.0);
257 roc.update_raw(2.0);
258 roc.update_raw(3.0);
259 roc.update_raw(4.0);
260
261 assert_eq!(roc.value, 300.0);
263 }
264}