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