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