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