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.core.nautilus_pyo3.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) {
79 self.update_raw(quote.extract_price(self.price_type).into());
80 }
81
82 fn handle_trade(&mut self, trade: &TradeTick) {
83 self.update_raw((&trade.price).into());
84 }
85
86 fn handle_bar(&mut self, bar: &Bar) {
87 self.update_raw((&bar.close).into());
88 }
89
90 fn reset(&mut self) {
91 self.value = 0.0;
92 self.fast_ma.reset();
93 self.slow_ma.reset();
94 self.has_inputs = false;
95 self.initialized = false;
96 }
97}
98
99impl MovingAverageConvergenceDivergence {
100 #[must_use]
102 pub fn new(
103 fast_period: usize,
104 slow_period: usize,
105 ma_type: Option<MovingAverageType>,
106 price_type: Option<PriceType>,
107 ) -> Self {
108 Self {
109 fast_period,
110 slow_period,
111 ma_type: ma_type.unwrap_or(MovingAverageType::Simple),
112 price_type: price_type.unwrap_or(PriceType::Last),
113 value: 0.0,
114 count: 0,
115 initialized: false,
116 has_inputs: false,
117 fast_ma: MovingAverageFactory::create(
118 ma_type.unwrap_or(MovingAverageType::Simple),
119 fast_period,
120 ),
121 slow_ma: MovingAverageFactory::create(
122 ma_type.unwrap_or(MovingAverageType::Simple),
123 slow_period,
124 ),
125 }
126 }
127}
128
129impl MovingAverage for MovingAverageConvergenceDivergence {
130 fn value(&self) -> f64 {
131 self.value
132 }
133
134 fn count(&self) -> usize {
135 self.count
136 }
137
138 fn update_raw(&mut self, close: f64) {
139 self.fast_ma.update_raw(close);
140 self.slow_ma.update_raw(close);
141 self.value = self.fast_ma.value() - self.slow_ma.value();
142
143 if !self.initialized {
145 self.has_inputs = true;
146
147 if self.fast_ma.initialized() && self.slow_ma.initialized() {
148 self.initialized = true;
149 }
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use nautilus_model::data::{Bar, QuoteTick, TradeTick};
157 use rstest::rstest;
158
159 use crate::{
160 indicator::{Indicator, MovingAverage},
161 momentum::macd::MovingAverageConvergenceDivergence,
162 stubs::*,
163 testing::approx_equal,
164 };
165
166 #[rstest]
167 fn test_macd_initialized(macd_10: MovingAverageConvergenceDivergence) {
168 let display_st = format!("{macd_10}");
169 assert_eq!(
170 display_st,
171 "MovingAverageConvergenceDivergence(10,8,SIMPLE,BID)"
172 );
173 assert_eq!(macd_10.fast_period, 10);
174 assert_eq!(macd_10.slow_period, 8);
175 assert!(!macd_10.initialized());
176 assert!(!macd_10.has_inputs());
177 }
178
179 #[rstest]
180 fn test_initialized_with_required_input(mut macd_10: MovingAverageConvergenceDivergence) {
181 for i in 1..10 {
182 macd_10.update_raw(f64::from(i));
183 }
184 assert!(!macd_10.initialized);
185 macd_10.update_raw(10.0);
186 assert!(macd_10.initialized);
187 }
188
189 #[rstest]
190 fn test_value_with_one_input(mut macd_10: MovingAverageConvergenceDivergence) {
191 macd_10.update_raw(1.0);
192 assert_eq!(macd_10.value, 0.0);
193 }
194
195 #[rstest]
196 fn test_value_with_three_inputs(mut macd_10: MovingAverageConvergenceDivergence) {
197 macd_10.update_raw(1.0);
198 macd_10.update_raw(2.0);
199 macd_10.update_raw(3.0);
200 assert_eq!(macd_10.value, 0.0);
201 }
202
203 #[rstest]
204 fn test_value_with_ten_inputs(mut macd_10: MovingAverageConvergenceDivergence) {
205 macd_10.update_raw(1.00000);
206 macd_10.update_raw(1.00010);
207 macd_10.update_raw(1.00020);
208 macd_10.update_raw(1.00030);
209 macd_10.update_raw(1.00040);
210 macd_10.update_raw(1.00050);
211 macd_10.update_raw(1.00040);
212 macd_10.update_raw(1.00030);
213 macd_10.update_raw(1.00020);
214 macd_10.update_raw(1.00010);
215 macd_10.update_raw(1.00000);
216 assert!(
217 approx_equal(macd_10.value, -2.5e-5),
218 "MACD value {:.17e} not within tolerance of –2.5e-5",
219 macd_10.value
220 );
221 }
222
223 #[rstest]
224 fn test_handle_quote_tick(
225 mut macd_10: MovingAverageConvergenceDivergence,
226 stub_quote: QuoteTick,
227 ) {
228 macd_10.handle_quote(&stub_quote);
229 assert_eq!(macd_10.value, 0.0);
230 }
231
232 #[rstest]
233 fn test_handle_trade_tick(
234 mut macd_10: MovingAverageConvergenceDivergence,
235 stub_trade: TradeTick,
236 ) {
237 macd_10.handle_trade(&stub_trade);
238 assert_eq!(macd_10.value, 0.0);
239 }
240
241 #[rstest]
242 fn test_handle_bar(
243 mut macd_10: MovingAverageConvergenceDivergence,
244 bar_ethusdt_binance_minute_bid: Bar,
245 ) {
246 macd_10.handle_bar(&bar_ethusdt_binance_minute_bid);
247 assert_eq!(macd_10.value, 0.0);
248 assert!(!macd_10.initialized);
249 }
250
251 #[rstest]
252 fn test_reset(mut macd_10: MovingAverageConvergenceDivergence) {
253 macd_10.update_raw(1.0);
254 macd_10.reset();
255 assert_eq!(macd_10.value, 0.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}