nautilus_indicators/average/
dema.rs1use std::fmt::Display;
17
18use nautilus_core::correctness::{FAILED, check_predicate_true};
19use nautilus_model::{
20 data::{Bar, QuoteTick, TradeTick},
21 enums::PriceType,
22};
23
24use crate::{
25 average::ema::ExponentialMovingAverage,
26 indicator::{Indicator, MovingAverage},
27 support::MAX_PERIOD,
28};
29
30#[repr(C)]
33#[derive(Debug)]
34#[cfg_attr(
35 feature = "python",
36 pyo3::pyclass(module = "nautilus_trader.indicators")
37)]
38#[cfg_attr(
39 feature = "python",
40 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
41)]
42pub struct DoubleExponentialMovingAverage {
43 pub period: usize,
45 pub price_type: PriceType,
47 pub value: f64,
49 pub count: usize,
51 pub initialized: bool,
52 has_inputs: bool,
53 ema1: ExponentialMovingAverage,
54 ema2: ExponentialMovingAverage,
55}
56
57impl Display for DoubleExponentialMovingAverage {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 write!(f, "DoubleExponentialMovingAverage(period={})", self.period)
60 }
61}
62
63impl Indicator for DoubleExponentialMovingAverage {
64 fn name(&self) -> String {
65 stringify!(DoubleExponentialMovingAverage).to_string()
66 }
67
68 fn has_inputs(&self) -> bool {
69 self.has_inputs
70 }
71
72 fn initialized(&self) -> bool {
73 self.initialized
74 }
75
76 fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
77 self.update_raw(quote.extract_price(self.price_type)?.into());
78 Ok(())
79 }
80
81 fn handle_trade(&mut self, trade: &TradeTick) {
82 self.update_raw((&trade.price).into());
83 }
84
85 fn handle_bar(&mut self, bar: &Bar) {
86 self.update_raw((&bar.close).into());
87 }
88
89 fn reset(&mut self) {
90 self.value = 0.0;
91 self.count = 0;
92 self.has_inputs = false;
93 self.initialized = false;
94
95 self.ema1.reset();
96 self.ema2.reset();
97 }
98}
99
100impl DoubleExponentialMovingAverage {
101 #[must_use]
108 pub fn new(period: usize, price_type: Option<PriceType>) -> Self {
109 Self::new_checked(period, price_type).expect(FAILED)
110 }
111
112 pub(crate) fn new_checked(
113 period: usize,
114 price_type: Option<PriceType>,
115 ) -> anyhow::Result<Self> {
116 check_predicate_true(
117 period <= MAX_PERIOD,
118 &format!("window periods cannot exceed {MAX_PERIOD}"),
119 )?;
120
121 check_predicate_true(
122 period > 0,
123 "DoubleExponentialMovingAverage: `period` must be a positive integer (> 0)",
124 )?;
125 Ok(Self {
126 period,
127 price_type: price_type.unwrap_or(PriceType::Last),
128 value: 0.0,
129 count: 0,
130 has_inputs: false,
131 initialized: false,
132 ema1: ExponentialMovingAverage::new(period, price_type),
133 ema2: ExponentialMovingAverage::new(period, price_type),
134 })
135 }
136}
137
138impl MovingAverage for DoubleExponentialMovingAverage {
139 fn value(&self) -> f64 {
140 self.value
141 }
142
143 fn count(&self) -> usize {
144 self.count
145 }
146
147 fn update_raw(&mut self, value: f64) {
148 if !value.is_finite() {
149 return;
150 }
151 self.ema1.update_raw(value);
152 self.count = self.ema1.count();
153 self.has_inputs = self.ema1.has_inputs();
154 if !self.ema1.initialized() {
155 return;
156 }
157
158 self.ema2.update_raw(self.ema1.value());
159 if !self.ema2.initialized() {
160 return;
161 }
162
163 self.value = 2.0f64.mul_add(self.ema1.value(), -self.ema2.value());
164 self.initialized = true;
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use nautilus_model::{
171 data::{Bar, QuoteTick, TradeTick},
172 enums::PriceType,
173 };
174 use rstest::rstest;
175
176 use crate::{
177 average::dema::DoubleExponentialMovingAverage,
178 indicator::{Indicator, MovingAverage},
179 stubs::*,
180 };
181
182 #[rstest]
183 fn test_dema_initialized(indicator_dema_10: DoubleExponentialMovingAverage) {
184 let display_str = format!("{indicator_dema_10}");
185 assert_eq!(display_str, "DoubleExponentialMovingAverage(period=10)");
186 assert_eq!(indicator_dema_10.period, 10);
187 assert!(!indicator_dema_10.initialized);
188 assert!(!indicator_dema_10.has_inputs);
189 }
190
191 #[rstest]
192 fn test_value_with_one_input(mut indicator_dema_10: DoubleExponentialMovingAverage) {
193 indicator_dema_10.update_raw(1.0);
194 assert_eq!(indicator_dema_10.value, 0.0);
195 assert!(!indicator_dema_10.initialized());
196 }
197
198 #[rstest]
199 fn test_value_with_three_inputs(mut indicator_dema_10: DoubleExponentialMovingAverage) {
200 for value in 1..=3 {
201 indicator_dema_10.update_raw(f64::from(value));
202 }
203 assert_eq!(indicator_dema_10.value, 0.0);
204 assert!(!indicator_dema_10.initialized());
205 }
206
207 #[rstest]
208 fn test_initialized_with_required_input(mut indicator_dema_10: DoubleExponentialMovingAverage) {
209 for i in 1..19 {
211 indicator_dema_10.update_raw(f64::from(i));
212 assert!(!indicator_dema_10.initialized);
213 }
214 indicator_dema_10.update_raw(19.0);
215 assert!(indicator_dema_10.initialized);
216 }
217
218 #[rstest]
219 fn test_handle_quote(
220 mut indicator_dema_10: DoubleExponentialMovingAverage,
221 stub_quote: QuoteTick,
222 ) {
223 indicator_dema_10.handle_quote(&stub_quote).unwrap();
224 assert_eq!(indicator_dema_10.count, 1);
225 assert_eq!(indicator_dema_10.value, 0.0);
226 assert!(indicator_dema_10.has_inputs);
227 assert!(!indicator_dema_10.initialized);
228 }
229
230 #[rstest]
231 fn test_handle_trade(
232 mut indicator_dema_10: DoubleExponentialMovingAverage,
233 stub_trade: TradeTick,
234 ) {
235 indicator_dema_10.handle_trade(&stub_trade);
236 assert_eq!(indicator_dema_10.count, 1);
237 assert_eq!(indicator_dema_10.value, 0.0);
238 assert!(indicator_dema_10.has_inputs);
239 assert!(!indicator_dema_10.initialized);
240 }
241
242 #[rstest]
243 fn test_handle_bar(
244 mut indicator_dema_10: DoubleExponentialMovingAverage,
245 bar_ethusdt_binance_minute_bid: Bar,
246 ) {
247 indicator_dema_10.handle_bar(&bar_ethusdt_binance_minute_bid);
248 assert_eq!(indicator_dema_10.count, 1);
249 assert_eq!(indicator_dema_10.value, 0.0);
250 assert!(indicator_dema_10.has_inputs);
251 assert!(!indicator_dema_10.initialized);
252 }
253
254 #[rstest]
255 fn test_reset(mut indicator_dema_10: DoubleExponentialMovingAverage) {
256 for value in 1..=10 {
257 indicator_dema_10.update_raw(f64::from(value));
258 }
259 assert_eq!(indicator_dema_10.count, 10);
260 assert_eq!(indicator_dema_10.ema1.count(), 10);
261 assert_eq!(indicator_dema_10.ema2.count(), 1);
262
263 indicator_dema_10.reset();
264
265 assert_eq!(indicator_dema_10.value, 0.0);
266 assert_eq!(indicator_dema_10.count, 0);
267 assert!(!indicator_dema_10.has_inputs);
268 assert!(!indicator_dema_10.initialized);
269 assert_eq!(indicator_dema_10.ema1.count(), 0);
270 assert_eq!(indicator_dema_10.ema2.count(), 0);
271 }
272
273 #[rstest]
274 #[should_panic(expected = "`period`")]
275 fn new_panics_on_zero_period() {
276 let _ = DoubleExponentialMovingAverage::new(0, None);
277 }
278
279 #[rstest]
280 fn test_new_with_minimum_valid_period() {
281 let dema = DoubleExponentialMovingAverage::new(1, None);
282 assert_eq!(dema.period, 1);
283 assert_eq!(dema.price_type, PriceType::Last);
284 assert_eq!(dema.count(), 0);
285 assert_eq!(dema.ema1.count(), 0);
286 assert_eq!(dema.ema2.count(), 0);
287 assert!(!dema.initialized());
288 }
289
290 #[rstest]
291 fn test_counters_are_in_sync(mut indicator_dema_10: DoubleExponentialMovingAverage) {
292 for input_count in 1..=19 {
293 indicator_dema_10.update_raw(input_count as f64);
294 assert_eq!(indicator_dema_10.count(), input_count);
295 assert_eq!(indicator_dema_10.ema1.count(), input_count);
296 assert_eq!(
297 indicator_dema_10.ema2.count(),
298 input_count.saturating_sub(indicator_dema_10.period - 1)
299 );
300 }
301
302 assert!(indicator_dema_10.initialized());
303 }
304
305 #[rstest]
306 fn test_inner_ema_values_are_reset(mut indicator_dema_10: DoubleExponentialMovingAverage) {
307 for value in 1..=19 {
308 indicator_dema_10.update_raw(f64::from(value));
309 }
310 assert_ne!(indicator_dema_10.ema1.value(), 0.0);
311 assert_ne!(indicator_dema_10.ema2.value(), 0.0);
312
313 indicator_dema_10.reset();
314
315 assert_eq!(indicator_dema_10.ema1.count(), 0);
316 assert_eq!(indicator_dema_10.ema2.count(), 0);
317 assert_eq!(indicator_dema_10.ema1.value(), 0.0);
318 assert_eq!(indicator_dema_10.ema2.value(), 0.0);
319 }
320
321 #[rstest]
322 fn test_counter_increments_from_market_data_handlers(
323 mut indicator_dema_10: DoubleExponentialMovingAverage,
324 stub_quote: QuoteTick,
325 stub_trade: TradeTick,
326 bar_ethusdt_binance_minute_bid: Bar,
327 ) {
328 assert_eq!(indicator_dema_10.count(), 0);
329 indicator_dema_10.handle_quote(&stub_quote).unwrap();
330 indicator_dema_10.handle_trade(&stub_trade);
331 indicator_dema_10.handle_bar(&bar_ethusdt_binance_minute_bid);
332
333 assert_eq!(indicator_dema_10.count(), 3);
334 assert_eq!(indicator_dema_10.ema1.count(), 3);
335 assert_eq!(indicator_dema_10.ema2.count(), 0);
336 }
337}