nautilus_indicators/momentum/
macd.rs1use std::fmt::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 MovingAverageConvergenceDivergence {
42 pub fast_period: usize,
43 pub slow_period: usize,
44 pub signal_period: usize,
45 pub ma_type: MovingAverageType,
46 pub count: usize,
47 pub price_type: PriceType,
48 pub value: f64,
49 pub signal: f64,
50 pub histogram: f64,
51 pub initialized: bool,
52 has_inputs: bool,
53 fast_ma: Box<dyn MovingAverage + Send + 'static>,
54 slow_ma: Box<dyn MovingAverage + Send + 'static>,
55 signal_ma: Box<dyn MovingAverage + Send + 'static>,
56}
57
58impl Display for MovingAverageConvergenceDivergence {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 write!(
61 f,
62 "{}({},{},{},{},{})",
63 self.name(),
64 self.fast_period,
65 self.slow_period,
66 self.signal_period,
67 self.ma_type,
68 self.price_type
69 )
70 }
71}
72
73impl Indicator for MovingAverageConvergenceDivergence {
74 fn name(&self) -> String {
75 stringify!(MovingAverageConvergenceDivergence).to_string()
76 }
77
78 fn has_inputs(&self) -> bool {
79 self.has_inputs
80 }
81
82 fn initialized(&self) -> bool {
83 self.initialized
84 }
85
86 fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
87 self.update_raw(quote.extract_price(self.price_type)?.into());
88 Ok(())
89 }
90
91 fn handle_trade(&mut self, trade: &TradeTick) {
92 self.update_raw((&trade.price).into());
93 }
94
95 fn handle_bar(&mut self, bar: &Bar) {
96 self.update_raw((&bar.close).into());
97 }
98
99 fn reset(&mut self) {
100 self.value = 0.0;
101 self.signal = 0.0;
102 self.histogram = 0.0;
103 self.count = 0;
104 self.fast_ma.reset();
105 self.slow_ma.reset();
106 self.signal_ma.reset();
107 self.has_inputs = false;
108 self.initialized = false;
109 }
110}
111
112impl MovingAverageConvergenceDivergence {
113 #[must_use]
123 pub fn new(
124 fast_period: usize,
125 slow_period: usize,
126 signal_period: Option<usize>,
127 ma_type: Option<MovingAverageType>,
128 price_type: Option<PriceType>,
129 ) -> Self {
130 Self::new_checked(fast_period, slow_period, signal_period, ma_type, price_type)
131 .expect(FAILED)
132 }
133
134 pub(crate) fn new_checked(
135 fast_period: usize,
136 slow_period: usize,
137 signal_period: Option<usize>,
138 ma_type: Option<MovingAverageType>,
139 price_type: Option<PriceType>,
140 ) -> anyhow::Result<Self> {
141 anyhow::ensure!(
142 fast_period <= MAX_PERIOD,
143 "fast_period cannot exceed {MAX_PERIOD}"
144 );
145 anyhow::ensure!(
146 slow_period <= MAX_PERIOD,
147 "slow_period cannot exceed {MAX_PERIOD}"
148 );
149 anyhow::ensure!(
150 fast_period > 0 && fast_period < slow_period,
151 "MovingAverageConvergenceDivergence: fast_period must be > 0 and < slow_period (received fast {fast_period}, slow {slow_period})"
152 );
153 let signal_period = signal_period.unwrap_or(9);
154 anyhow::ensure!(
155 signal_period <= MAX_PERIOD,
156 "signal_period cannot exceed {MAX_PERIOD}"
157 );
158 anyhow::ensure!(
159 signal_period > 0,
160 "MovingAverageConvergenceDivergence: signal_period must be > 0 (received {signal_period})"
161 );
162 let ma_type = ma_type.unwrap_or(MovingAverageType::Exponential);
163
164 Ok(Self {
165 fast_period,
166 slow_period,
167 signal_period,
168 ma_type,
169 price_type: price_type.unwrap_or(PriceType::Last),
170 value: 0.0,
171 signal: 0.0,
172 histogram: 0.0,
173 count: 0,
174 initialized: false,
175 has_inputs: false,
176 fast_ma: MovingAverageFactory::create(ma_type, fast_period),
177 slow_ma: MovingAverageFactory::create(ma_type, slow_period),
178 signal_ma: MovingAverageFactory::create(ma_type, signal_period),
179 })
180 }
181}
182
183impl MovingAverage for MovingAverageConvergenceDivergence {
184 fn value(&self) -> f64 {
185 self.value
186 }
187
188 fn count(&self) -> usize {
189 self.count
190 }
191
192 fn update_raw(&mut self, close: f64) {
193 if !close.is_finite() {
194 return;
195 }
196 self.fast_ma.update_raw(close);
197 self.slow_ma.update_raw(close);
198 self.count += 1;
199
200 self.has_inputs = true;
201
202 if self.fast_ma.initialized() && self.slow_ma.initialized() {
203 let value = self.fast_ma.value() - self.slow_ma.value();
204 self.signal_ma.update_raw(value);
205 self.initialized = self.signal_ma.initialized();
206 if self.initialized {
207 self.value = value;
208 self.signal = self.signal_ma.value();
209 self.histogram = self.value - self.signal;
210 }
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use nautilus_model::data::{Bar, QuoteTick, TradeTick};
218 use rstest::rstest;
219
220 use crate::{
221 average::MovingAverageType,
222 indicator::{Indicator, MovingAverage},
223 momentum::macd::MovingAverageConvergenceDivergence,
224 stubs::*,
225 };
226
227 #[rstest]
228 fn test_macd_initialized(macd_10: MovingAverageConvergenceDivergence) {
229 let display_st = format!("{macd_10}");
230 assert_eq!(
231 display_st,
232 "MovingAverageConvergenceDivergence(8,10,9,SIMPLE,BID)"
233 );
234 assert_eq!(macd_10.fast_period, 8);
235 assert_eq!(macd_10.slow_period, 10);
236 assert_eq!(macd_10.signal_period, 9);
237 assert!(!macd_10.initialized());
238 assert!(!macd_10.has_inputs());
239 }
240
241 #[rstest]
242 fn test_initialized_with_required_input(mut macd_10: MovingAverageConvergenceDivergence) {
243 for i in 1..18 {
245 macd_10.update_raw(f64::from(i));
246 assert!(!macd_10.initialized);
247 }
248 macd_10.update_raw(18.0);
249 assert!(macd_10.initialized);
250 }
251
252 #[rstest]
253 #[case(MovingAverageType::Simple, 3)]
254 #[case(MovingAverageType::Exponential, 3)]
255 #[case(MovingAverageType::DoubleExponential, 5)]
256 #[case(MovingAverageType::Wilder, 3)]
257 #[case(MovingAverageType::Hull, 3)]
258 fn test_composite_warmup_uses_selected_ma(
259 #[case] ma_type: MovingAverageType,
260 #[case] warmup: usize,
261 ) {
262 let mut macd = MovingAverageConvergenceDivergence::new(1, 2, Some(2), Some(ma_type), None);
263
264 for i in 1..warmup {
265 macd.update_raw(i as f64);
266 assert!(!macd.initialized(), "initialized at input {i}");
267 }
268
269 macd.update_raw(warmup as f64);
270 assert!(macd.initialized());
271 }
272
273 #[rstest]
274 fn test_hull_signal_waits_for_full_warmup() {
275 let mut macd = MovingAverageConvergenceDivergence::new(
276 1,
277 2,
278 Some(20),
279 Some(MovingAverageType::Hull),
280 None,
281 );
282
283 for i in 1..24 {
284 macd.update_raw(f64::from(i));
285 assert!(!macd.initialized(), "initialized at input {i}");
286 }
287
288 macd.update_raw(24.0);
289 assert!(macd.initialized());
290 }
291
292 #[rstest]
293 fn test_value_with_one_input(mut macd_10: MovingAverageConvergenceDivergence) {
294 macd_10.update_raw(1.0);
295 assert_eq!(macd_10.value, 0.0);
296 }
297
298 #[rstest]
299 fn test_value_with_three_inputs(mut macd_10: MovingAverageConvergenceDivergence) {
300 macd_10.update_raw(1.0);
301 macd_10.update_raw(2.0);
302 macd_10.update_raw(3.0);
303 assert_eq!(macd_10.value, 0.0);
304 }
305
306 #[rstest]
307 fn test_value_before_signal_ready(mut macd_10: MovingAverageConvergenceDivergence) {
308 macd_10.update_raw(1.00000);
309 macd_10.update_raw(1.00010);
310 macd_10.update_raw(1.00020);
311 macd_10.update_raw(1.00030);
312 macd_10.update_raw(1.00040);
313 macd_10.update_raw(1.00050);
314 macd_10.update_raw(1.00040);
315 macd_10.update_raw(1.00030);
316 macd_10.update_raw(1.00020);
317 macd_10.update_raw(1.00010);
318 macd_10.update_raw(1.00000);
319 assert_eq!(
320 (macd_10.value, macd_10.signal, macd_10.histogram),
321 (0.0, 0.0, 0.0)
322 );
323 assert!(!macd_10.initialized);
324 }
325
326 #[rstest]
327 fn test_handle_quote_tick(
328 mut macd_10: MovingAverageConvergenceDivergence,
329 stub_quote: QuoteTick,
330 ) {
331 macd_10.handle_quote(&stub_quote).unwrap();
332 assert_eq!(macd_10.value, 0.0);
333 }
334
335 #[rstest]
336 fn test_handle_trade_tick(
337 mut macd_10: MovingAverageConvergenceDivergence,
338 stub_trade: TradeTick,
339 ) {
340 macd_10.handle_trade(&stub_trade);
341 assert_eq!(macd_10.value, 0.0);
342 }
343
344 #[rstest]
345 fn test_handle_bar(
346 mut macd_10: MovingAverageConvergenceDivergence,
347 bar_ethusdt_binance_minute_bid: Bar,
348 ) {
349 macd_10.handle_bar(&bar_ethusdt_binance_minute_bid);
350 assert_eq!(macd_10.value, 0.0);
351 assert!(!macd_10.initialized);
352 }
353
354 #[rstest]
355 fn test_reset(mut macd_10: MovingAverageConvergenceDivergence) {
356 macd_10.update_raw(1.0);
357 macd_10.reset();
358 assert_eq!(macd_10.value, 0.0);
359 assert_eq!(macd_10.signal, 0.0);
360 assert_eq!(macd_10.histogram, 0.0);
361 assert_eq!(macd_10.count, 0);
362 assert_eq!(macd_10.fast_ma.value(), 0.0);
363 assert_eq!(macd_10.slow_ma.value(), 0.0);
364 assert!(!macd_10.has_inputs);
365 assert!(!macd_10.initialized);
366 }
367
368 #[rstest]
369 fn count_matches_inputs(mut macd_10: MovingAverageConvergenceDivergence) {
370 assert_eq!(macd_10.count(), 0);
371
372 for i in 1..=12 {
373 macd_10.update_raw(f64::from(i));
374 assert_eq!(macd_10.count(), i as usize);
375 }
376 }
377}