nautilus_indicators/average/
ema.rs1use std::fmt::Display;
17
18use nautilus_model::{
19 data::{Bar, QuoteTick, TradeTick},
20 enums::PriceType,
21};
22
23use crate::indicator::{Indicator, MovingAverage};
24
25#[repr(C)]
26#[derive(Debug)]
27#[cfg_attr(
28 feature = "python",
29 pyo3::pyclass(module = "nautilus_trader.indicators")
30)]
31#[cfg_attr(
32 feature = "python",
33 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
34)]
35pub struct ExponentialMovingAverage {
36 pub period: usize,
37 pub price_type: PriceType,
38 pub alpha: f64,
39 pub value: f64,
40 pub count: usize,
41 pub initialized: bool,
42 has_inputs: bool,
43}
44
45impl Display for ExponentialMovingAverage {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(f, "{}({})", self.name(), self.period)
48 }
49}
50
51impl Indicator for ExponentialMovingAverage {
52 fn name(&self) -> String {
53 stringify!(ExponentialMovingAverage).to_string()
54 }
55
56 fn has_inputs(&self) -> bool {
57 self.has_inputs
58 }
59
60 fn initialized(&self) -> bool {
61 self.initialized
62 }
63
64 fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
65 self.update_raw(quote.extract_price(self.price_type)?.into());
66 Ok(())
67 }
68
69 fn handle_trade(&mut self, trade: &TradeTick) {
70 self.update_raw((&trade.price).into());
71 }
72
73 fn handle_bar(&mut self, bar: &Bar) {
74 self.update_raw((&bar.close).into());
75 }
76
77 fn reset(&mut self) {
78 self.value = 0.0;
79 self.count = 0;
80 self.has_inputs = false;
81 self.initialized = false;
82 }
83}
84
85impl ExponentialMovingAverage {
86 #[must_use]
92 pub fn new(period: usize, price_type: Option<PriceType>) -> Self {
93 assert!(
94 period > 0,
95 "ExponentialMovingAverage::new → `period` must be positive (> 0); got {period}"
96 );
97 Self {
98 period,
99 price_type: price_type.unwrap_or(PriceType::Last),
100 alpha: 2.0 / (period as f64 + 1.0),
101 value: 0.0,
102 count: 0,
103 has_inputs: false,
104 initialized: false,
105 }
106 }
107}
108
109impl MovingAverage for ExponentialMovingAverage {
110 fn value(&self) -> f64 {
111 self.value
112 }
113
114 fn count(&self) -> usize {
115 self.count
116 }
117
118 fn update_raw(&mut self, value: f64) {
119 if !self.has_inputs {
120 self.has_inputs = true;
121 self.value = value;
122 self.count = 1;
123
124 if self.period == 1 {
125 self.initialized = true;
126 }
127 return;
128 }
129
130 self.value = self.alpha.mul_add(value, (1.0 - self.alpha) * self.value);
131 self.count += 1;
132
133 if !self.initialized && self.count >= self.period {
135 self.initialized = true;
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use nautilus_model::{
143 data::{Bar, QuoteTick, TradeTick},
144 enums::PriceType,
145 };
146 use rstest::rstest;
147
148 use crate::{
149 average::ema::ExponentialMovingAverage,
150 indicator::{Indicator, MovingAverage},
151 stubs::*,
152 testing::assert_approx_equal,
153 };
154
155 #[rstest]
156 fn test_ema_initialized(indicator_ema_10: ExponentialMovingAverage) {
157 let ema = indicator_ema_10;
158 let display_str = format!("{ema}");
159 assert_eq!(display_str, "ExponentialMovingAverage(10)");
160 assert_eq!(ema.period, 10);
161 assert_eq!(ema.price_type, PriceType::Mid);
162 assert_approx_equal(ema.alpha, 0.181818181818);
163 assert!(!ema.initialized);
164 }
165
166 #[rstest]
167 fn test_one_value_input(indicator_ema_10: ExponentialMovingAverage) {
168 let mut ema = indicator_ema_10;
169 ema.update_raw(1.0);
170 assert_eq!(ema.count, 1);
171 assert_eq!(ema.value, 1.0);
172 }
173
174 #[rstest]
175 fn test_ema_update_raw(indicator_ema_10: ExponentialMovingAverage) {
176 let mut ema = indicator_ema_10;
177 ema.update_raw(1.0);
178 ema.update_raw(2.0);
179 ema.update_raw(3.0);
180 ema.update_raw(4.0);
181 ema.update_raw(5.0);
182 ema.update_raw(6.0);
183 ema.update_raw(7.0);
184 ema.update_raw(8.0);
185 ema.update_raw(9.0);
186 ema.update_raw(10.0);
187
188 assert!(ema.has_inputs());
189 assert!(ema.initialized());
190 assert_eq!(ema.count, 10);
191 assert_approx_equal(ema.value, 6.23936848012);
192 }
193
194 #[rstest]
195 fn test_reset(indicator_ema_10: ExponentialMovingAverage) {
196 let mut ema = indicator_ema_10;
197 ema.update_raw(1.0);
198 assert_eq!(ema.count, 1);
199 ema.reset();
200 assert_eq!(ema.count, 0);
201 assert_eq!(ema.value, 0.0);
202 assert!(!ema.initialized);
203 }
204
205 #[rstest]
206 fn test_handle_quote_tick_single(
207 indicator_ema_10: ExponentialMovingAverage,
208 stub_quote: QuoteTick,
209 ) {
210 let mut ema = indicator_ema_10;
211 ema.handle_quote(&stub_quote).unwrap();
212 assert!(ema.has_inputs());
213 assert_eq!(ema.value, 1501.0);
214 }
215
216 #[rstest]
217 fn test_handle_quote_tick_multi(mut indicator_ema_10: ExponentialMovingAverage) {
218 let tick1 = stub_quote("1500.0", "1502.0");
219 let tick2 = stub_quote("1502.0", "1504.0");
220
221 indicator_ema_10.handle_quote(&tick1).unwrap();
222 indicator_ema_10.handle_quote(&tick2).unwrap();
223 assert_eq!(indicator_ema_10.count, 2);
224 assert_approx_equal(indicator_ema_10.value, 1501.36363636);
225 }
226
227 #[rstest]
228 fn test_handle_trade_tick(indicator_ema_10: ExponentialMovingAverage, stub_trade: TradeTick) {
229 let mut ema = indicator_ema_10;
230 ema.handle_trade(&stub_trade);
231 assert!(ema.has_inputs());
232 assert_eq!(ema.value, 1500.0);
233 }
234
235 #[rstest]
236 fn handle_handle_bar(
237 mut indicator_ema_10: ExponentialMovingAverage,
238 bar_ethusdt_binance_minute_bid: Bar,
239 ) {
240 indicator_ema_10.handle_bar(&bar_ethusdt_binance_minute_bid);
241 assert!(indicator_ema_10.has_inputs);
242 assert!(!indicator_ema_10.initialized);
243 assert_eq!(indicator_ema_10.value, 1522.0);
244 }
245
246 #[rstest]
247 fn test_period_one_behaviour() {
248 let mut ema = ExponentialMovingAverage::new(1, None);
249 assert_eq!(ema.alpha, 1.0, "α must be 1 when period = 1");
250
251 ema.update_raw(10.0);
252 assert!(ema.initialized());
253 assert_eq!(ema.value(), 10.0);
254
255 ema.update_raw(42.0);
256 assert_eq!(
257 ema.value(),
258 42.0,
259 "With α = 1, the EMA must track the latest sample exactly"
260 );
261 }
262
263 #[rstest]
264 fn test_default_price_type_is_last() {
265 let ema = ExponentialMovingAverage::new(3, None);
266 assert_eq!(
267 ema.price_type,
268 PriceType::Last,
269 "`price_type` default mismatch"
270 );
271 }
272
273 #[rstest]
274 fn test_nan_poisoning_and_reset_recovery() {
275 let mut ema = ExponentialMovingAverage::new(4, None);
276 for x in 0..3 {
277 ema.update_raw(f64::from(x));
278 assert!(ema.value().is_finite());
279 }
280
281 ema.update_raw(f64::NAN);
282 assert!(ema.value().is_nan());
283
284 ema.update_raw(123.456);
285 assert!(ema.value().is_nan());
286
287 ema.reset();
288 assert!(!ema.has_inputs());
289 ema.update_raw(7.0);
290 assert_eq!(ema.value(), 7.0);
291 assert!(ema.value().is_finite());
292 }
293
294 #[rstest]
295 fn test_reset_without_inputs_is_safe() {
296 let mut ema = ExponentialMovingAverage::new(8, None);
297 ema.reset();
298 assert!(!ema.has_inputs());
299 assert_eq!(ema.count(), 0);
300 assert!(!ema.initialized());
301 }
302
303 #[rstest]
304 fn test_has_inputs_lifecycle() {
305 let mut ema = ExponentialMovingAverage::new(5, None);
306 assert!(!ema.has_inputs());
307
308 ema.update_raw(1.23);
309 assert!(ema.has_inputs());
310
311 ema.reset();
312 assert!(!ema.has_inputs());
313 }
314
315 #[rstest]
316 fn test_subnormal_inputs_do_not_underflow() {
317 let mut ema = ExponentialMovingAverage::new(2, None);
318 let tiny = f64::MIN_POSITIVE / 2.0;
319 ema.update_raw(tiny);
320 ema.update_raw(tiny);
321 assert!(
322 ema.value() > 0.0,
323 "Underflow: EMA value collapsed to zero for sub-normal inputs"
324 );
325 }
326}