nautilus_indicators/momentum/
cmo.rs1use std::fmt::Display;
17
18use nautilus_model::data::{Bar, QuoteTick, TradeTick};
19
20use crate::{
21 average::{MovingAverageFactory, MovingAverageType},
22 indicator::{Indicator, MovingAverage},
23};
24
25#[repr(C)]
26#[derive(Debug)]
27#[cfg_attr(
28 feature = "python",
29 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators", unsendable)
30)]
31pub struct ChandeMomentumOscillator {
32 pub period: usize,
33 pub ma_type: MovingAverageType,
34 pub value: f64,
35 pub count: usize,
36 pub initialized: bool,
37 previous_close: f64,
38 average_gain: Box<dyn MovingAverage + Send + 'static>,
39 average_loss: Box<dyn MovingAverage + Send + 'static>,
40 has_inputs: bool,
41}
42
43impl Display for ChandeMomentumOscillator {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(f, "{}({})", self.name(), self.period)
46 }
47}
48
49impl Indicator for ChandeMomentumOscillator {
50 fn name(&self) -> String {
51 stringify!(ChandeMomentumOscillator).to_string()
52 }
53
54 fn has_inputs(&self) -> bool {
55 self.has_inputs
56 }
57
58 fn initialized(&self) -> bool {
59 self.initialized
60 }
61
62 fn handle_quote(&mut self, _quote: &QuoteTick) {
63 }
65
66 fn handle_trade(&mut self, _trade: &TradeTick) {
67 }
69
70 fn handle_bar(&mut self, bar: &Bar) {
71 self.update_raw((&bar.close).into());
72 }
73
74 fn reset(&mut self) {
75 self.value = 0.0;
76 self.count = 0;
77 self.has_inputs = false;
78 self.initialized = false;
79 self.previous_close = 0.0;
80 }
81}
82
83impl ChandeMomentumOscillator {
84 #[must_use]
86 pub fn new(period: usize, ma_type: Option<MovingAverageType>) -> Self {
87 Self {
88 period,
89 ma_type: ma_type.unwrap_or(MovingAverageType::Wilder),
90 average_gain: MovingAverageFactory::create(MovingAverageType::Wilder, period),
91 average_loss: MovingAverageFactory::create(MovingAverageType::Wilder, period),
92 previous_close: 0.0,
93 value: 0.0,
94 count: 0,
95 initialized: false,
96 has_inputs: false,
97 }
98 }
99
100 pub fn update_raw(&mut self, close: f64) {
101 if !self.has_inputs {
102 self.previous_close = close;
103 self.has_inputs = true;
104 }
105
106 let gain: f64 = close - self.previous_close;
107 if gain > 0.0 {
108 self.average_gain.update_raw(gain);
109 self.average_loss.update_raw(0.0);
110 } else if gain < 0.0 {
111 self.average_gain.update_raw(0.0);
112 self.average_loss.update_raw(-gain);
113 } else {
114 self.average_gain.update_raw(0.0);
115 self.average_loss.update_raw(0.0);
116 }
117
118 if !self.initialized && self.average_gain.initialized() && self.average_loss.initialized() {
119 self.initialized = true;
120 }
121 if self.initialized {
122 let divisor = self.average_gain.value() + self.average_loss.value();
123 if divisor == 0.0 {
124 self.value = 0.0;
125 } else {
126 self.value =
127 100.0 * (self.average_gain.value() - self.average_loss.value()) / divisor;
128 }
129 }
130 self.previous_close = close;
131 }
132}
133
134#[cfg(test)]
138mod tests {
139 use nautilus_model::data::{Bar, QuoteTick};
140 use rstest::rstest;
141
142 use crate::{indicator::Indicator, momentum::cmo::ChandeMomentumOscillator, stubs::*};
143
144 #[rstest]
145 fn test_cmo_initialized(cmo_10: ChandeMomentumOscillator) {
146 let display_str = format!("{cmo_10}");
147 assert_eq!(display_str, "ChandeMomentumOscillator(10)");
148 assert_eq!(cmo_10.period, 10);
149 assert!(!cmo_10.initialized);
150 }
151
152 #[rstest]
153 fn test_initialized_with_required_inputs_returns_true(mut cmo_10: ChandeMomentumOscillator) {
154 for i in 0..12 {
155 cmo_10.update_raw(f64::from(i));
156 }
157 assert!(cmo_10.initialized);
158 }
159
160 #[rstest]
161 fn test_value_all_higher_inputs_returns_expected_value(mut cmo_10: ChandeMomentumOscillator) {
162 cmo_10.update_raw(109.93);
163 cmo_10.update_raw(110.0);
164 cmo_10.update_raw(109.77);
165 cmo_10.update_raw(109.96);
166 cmo_10.update_raw(110.29);
167 cmo_10.update_raw(110.53);
168 cmo_10.update_raw(110.27);
169 cmo_10.update_raw(110.21);
170 cmo_10.update_raw(110.06);
171 cmo_10.update_raw(110.19);
172 cmo_10.update_raw(109.83);
173 cmo_10.update_raw(109.9);
174 cmo_10.update_raw(110.0);
175 cmo_10.update_raw(110.03);
176 cmo_10.update_raw(110.13);
177 cmo_10.update_raw(109.95);
178 cmo_10.update_raw(109.75);
179 cmo_10.update_raw(110.15);
180 cmo_10.update_raw(109.9);
181 cmo_10.update_raw(110.04);
182 assert_eq!(cmo_10.value, 2.089_629_456_238_705_4);
183 }
184
185 #[rstest]
186 fn test_value_with_one_input_returns_expected_value(mut cmo_10: ChandeMomentumOscillator) {
187 cmo_10.update_raw(1.00000);
188 assert_eq!(cmo_10.value, 0.0);
189 }
190
191 #[rstest]
192 fn test_reset(mut cmo_10: ChandeMomentumOscillator) {
193 cmo_10.update_raw(1.00020);
194 cmo_10.update_raw(1.00030);
195 cmo_10.update_raw(1.00050);
196 cmo_10.reset();
197 assert!(!cmo_10.initialized());
198 assert_eq!(cmo_10.count, 0);
199 assert_eq!(cmo_10.value, 0.0);
200 assert_eq!(cmo_10.previous_close, 0.0);
201 }
202
203 #[rstest]
204 fn test_handle_quote_tick(mut cmo_10: ChandeMomentumOscillator, stub_quote: QuoteTick) {
205 cmo_10.handle_quote(&stub_quote);
206 assert_eq!(cmo_10.count, 0);
207 assert_eq!(cmo_10.value, 0.0);
208 }
209
210 #[rstest]
211 fn test_handle_bar(mut cmo_10: ChandeMomentumOscillator, bar_ethusdt_binance_minute_bid: Bar) {
212 cmo_10.handle_bar(&bar_ethusdt_binance_minute_bid);
213 assert_eq!(cmo_10.count, 0);
214 assert_eq!(cmo_10.value, 0.0);
215 }
216}