Skip to main content

nautilus_indicators/average/
dema.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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/// The Double Exponential Moving Average attempts to a smoother average with less
31/// lag than the normal Exponential Moving Average (EMA)
32#[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    /// The rolling window period for the indicator (> 0).
44    pub period: usize,
45    /// The price type used for calculations.
46    pub price_type: PriceType,
47    /// The last indicator value.
48    pub value: f64,
49    /// The input count for the indicator.
50    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    /// Creates a new [`DoubleExponentialMovingAverage`] instance.
102    ///
103    /// # Panics
104    ///
105    /// Panics if `period` is not a positive integer (> 0).
106    /// Window periods must not exceed the shared indicator period limit.
107    #[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        // Composite warmup for period 10 is 2 * 10 - 1 = 19 inputs
210        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}