nautilus_indicators/momentum/
macd.rs1use std::fmt::Display;
17
18use nautilus_model::{
19 data::{Bar, QuoteTick, TradeTick},
20 enums::PriceType,
21};
22
23use crate::{
24 average::{MovingAverageFactory, MovingAverageType},
25 indicator::{Indicator, MovingAverage},
26};
27
28#[repr(C)]
29#[derive(Debug)]
30#[cfg_attr(
31 feature = "python",
32 pyo3::pyclass(module = "nautilus_trader.indicators", unsendable)
33)]
34#[cfg_attr(
35 feature = "python",
36 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
37)]
38pub struct MovingAverageConvergenceDivergence {
39 pub fast_period: usize,
40 pub slow_period: usize,
41 pub ma_type: MovingAverageType,
42 pub count: usize,
43 pub price_type: PriceType,
44 pub value: f64,
45 pub initialized: bool,
46 has_inputs: bool,
47 fast_ma: Box<dyn MovingAverage + Send + 'static>,
48 slow_ma: Box<dyn MovingAverage + Send + 'static>,
49}
50
51impl Display for MovingAverageConvergenceDivergence {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 write!(
54 f,
55 "{}({},{},{},{})",
56 self.name(),
57 self.fast_period,
58 self.slow_period,
59 self.ma_type,
60 self.price_type
61 )
62 }
63}
64
65impl Indicator for MovingAverageConvergenceDivergence {
66 fn name(&self) -> String {
67 stringify!(MovingAverageConvergenceDivergence).to_string()
68 }
69
70 fn has_inputs(&self) -> bool {
71 self.has_inputs
72 }
73
74 fn initialized(&self) -> bool {
75 self.initialized
76 }
77
78 fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
79 self.update_raw(quote.extract_price(self.price_type)?.into());
80 Ok(())
81 }
82
83 fn handle_trade(&mut self, trade: &TradeTick) {
84 self.update_raw((&trade.price).into());
85 }
86
87 fn handle_bar(&mut self, bar: &Bar) {
88 self.update_raw((&bar.close).into());
89 }
90
91 fn reset(&mut self) {
92 self.value = 0.0;
93 self.count = 0;
94 self.fast_ma.reset();
95 self.slow_ma.reset();
96 self.has_inputs = false;
97 self.initialized = false;
98 }
99}
100
101impl MovingAverageConvergenceDivergence {
102 #[must_use]
104 pub fn new(
105 fast_period: usize,
106 slow_period: usize,
107 ma_type: Option<MovingAverageType>,
108 price_type: Option<PriceType>,
109 ) -> Self {
110 Self {
111 fast_period,
112 slow_period,
113 ma_type: ma_type.unwrap_or(MovingAverageType::Simple),
114 price_type: price_type.unwrap_or(PriceType::Last),
115 value: 0.0,
116 count: 0,
117 initialized: false,
118 has_inputs: false,
119 fast_ma: MovingAverageFactory::create(
120 ma_type.unwrap_or(MovingAverageType::Simple),
121 fast_period,
122 ),
123 slow_ma: MovingAverageFactory::create(
124 ma_type.unwrap_or(MovingAverageType::Simple),
125 slow_period,
126 ),
127 }
128 }
129}
130
131impl MovingAverage for MovingAverageConvergenceDivergence {
132 fn value(&self) -> f64 {
133 self.value
134 }
135
136 fn count(&self) -> usize {
137 self.count
138 }
139
140 fn update_raw(&mut self, close: f64) {
141 self.fast_ma.update_raw(close);
142 self.slow_ma.update_raw(close);
143 self.value = self.fast_ma.value() - self.slow_ma.value();
144 self.count += 1;
145
146 if !self.initialized {
148 self.has_inputs = true;
149
150 if self.fast_ma.initialized() && self.slow_ma.initialized() {
151 self.initialized = true;
152 }
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use nautilus_model::data::{Bar, QuoteTick, TradeTick};
160 use rstest::rstest;
161
162 use crate::{
163 indicator::{Indicator, MovingAverage},
164 momentum::macd::MovingAverageConvergenceDivergence,
165 stubs::*,
166 testing::assert_approx_equal,
167 };
168
169 #[rstest]
170 fn test_macd_initialized(macd_10: MovingAverageConvergenceDivergence) {
171 let display_st = format!("{macd_10}");
172 assert_eq!(
173 display_st,
174 "MovingAverageConvergenceDivergence(10,8,SIMPLE,BID)"
175 );
176 assert_eq!(macd_10.fast_period, 10);
177 assert_eq!(macd_10.slow_period, 8);
178 assert!(!macd_10.initialized());
179 assert!(!macd_10.has_inputs());
180 }
181
182 #[rstest]
183 fn test_initialized_with_required_input(mut macd_10: MovingAverageConvergenceDivergence) {
184 for i in 1..10 {
185 macd_10.update_raw(f64::from(i));
186 }
187 assert!(!macd_10.initialized);
188 macd_10.update_raw(10.0);
189 assert!(macd_10.initialized);
190 }
191
192 #[rstest]
193 fn test_value_with_one_input(mut macd_10: MovingAverageConvergenceDivergence) {
194 macd_10.update_raw(1.0);
195 assert_eq!(macd_10.value, 0.0);
196 }
197
198 #[rstest]
199 fn test_value_with_three_inputs(mut macd_10: MovingAverageConvergenceDivergence) {
200 macd_10.update_raw(1.0);
201 macd_10.update_raw(2.0);
202 macd_10.update_raw(3.0);
203 assert_eq!(macd_10.value, 0.0);
204 }
205
206 #[rstest]
207 fn test_value_with_ten_inputs(mut macd_10: MovingAverageConvergenceDivergence) {
208 macd_10.update_raw(1.00000);
209 macd_10.update_raw(1.00010);
210 macd_10.update_raw(1.00020);
211 macd_10.update_raw(1.00030);
212 macd_10.update_raw(1.00040);
213 macd_10.update_raw(1.00050);
214 macd_10.update_raw(1.00040);
215 macd_10.update_raw(1.00030);
216 macd_10.update_raw(1.00020);
217 macd_10.update_raw(1.00010);
218 macd_10.update_raw(1.00000);
219 assert_approx_equal(macd_10.value, -2.5e-5);
220 }
221
222 #[rstest]
223 fn test_handle_quote_tick(
224 mut macd_10: MovingAverageConvergenceDivergence,
225 stub_quote: QuoteTick,
226 ) {
227 macd_10.handle_quote(&stub_quote).unwrap();
228 assert_eq!(macd_10.value, 0.0);
229 }
230
231 #[rstest]
232 fn test_handle_trade_tick(
233 mut macd_10: MovingAverageConvergenceDivergence,
234 stub_trade: TradeTick,
235 ) {
236 macd_10.handle_trade(&stub_trade);
237 assert_eq!(macd_10.value, 0.0);
238 }
239
240 #[rstest]
241 fn test_handle_bar(
242 mut macd_10: MovingAverageConvergenceDivergence,
243 bar_ethusdt_binance_minute_bid: Bar,
244 ) {
245 macd_10.handle_bar(&bar_ethusdt_binance_minute_bid);
246 assert_eq!(macd_10.value, 0.0);
247 assert!(!macd_10.initialized);
248 }
249
250 #[rstest]
251 fn test_reset(mut macd_10: MovingAverageConvergenceDivergence) {
252 macd_10.update_raw(1.0);
253 macd_10.reset();
254 assert_eq!(macd_10.value, 0.0);
255 assert_eq!(macd_10.count, 0);
256 assert_eq!(macd_10.fast_ma.value(), 0.0);
257 assert_eq!(macd_10.slow_ma.value(), 0.0);
258 assert!(!macd_10.has_inputs);
259 assert!(!macd_10.initialized);
260 }
261
262 #[rstest]
263 fn count_matches_inputs(mut macd_10: MovingAverageConvergenceDivergence) {
264 assert_eq!(macd_10.count(), 0);
265
266 for i in 1..=12 {
267 macd_10.update_raw(f64::from(i));
268 assert_eq!(macd_10.count(), i as usize);
269 }
270 }
271}