nautilus_indicators/average/
rma.rs1use 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 indicator::{Indicator, MovingAverage},
26 support::MAX_PERIOD,
27};
28
29#[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 WilderMovingAverage {
41 pub period: usize,
42 pub price_type: PriceType,
43 pub alpha: f64,
44 pub value: f64,
45 pub count: usize,
46 pub initialized: bool,
47 has_inputs: bool,
48 seed_sum: f64,
49}
50
51impl Display for WilderMovingAverage {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 write!(f, "{}({})", self.name(), self.period)
54 }
55}
56
57impl Indicator for WilderMovingAverage {
58 fn name(&self) -> String {
59 stringify!(WilderMovingAverage).to_string()
60 }
61
62 fn has_inputs(&self) -> bool {
63 self.has_inputs
64 }
65 fn initialized(&self) -> bool {
66 self.initialized
67 }
68
69 fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
70 self.update_raw(quote.extract_price(self.price_type)?.into());
71 Ok(())
72 }
73
74 fn handle_trade(&mut self, t: &TradeTick) {
75 self.update_raw((&t.price).into());
76 }
77
78 fn handle_bar(&mut self, b: &Bar) {
79 self.update_raw((&b.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 self.seed_sum = 0.0;
88 }
89}
90
91impl WilderMovingAverage {
92 #[must_use]
98 pub fn new(period: usize, price_type: Option<PriceType>) -> Self {
99 Self::new_checked(period, price_type).expect(FAILED)
100 }
101
102 pub fn new_checked(period: usize, price_type: Option<PriceType>) -> anyhow::Result<Self> {
108 check_predicate_true(
109 period > 0,
110 &format!("WilderMovingAverage: period must be > 0 (received {period})"),
111 )?;
112 check_predicate_true(
113 period <= MAX_PERIOD,
114 &format!("period cannot exceed {MAX_PERIOD}"),
115 )?;
116 Ok(Self {
117 period,
118 price_type: price_type.unwrap_or(PriceType::Last),
119 alpha: 1.0 / period as f64,
120 value: 0.0,
121 count: 0,
122 initialized: false,
123 has_inputs: false,
124 seed_sum: 0.0,
125 })
126 }
127}
128
129impl MovingAverage for WilderMovingAverage {
130 fn value(&self) -> f64 {
131 self.value
132 }
133
134 fn count(&self) -> usize {
135 self.count
136 }
137
138 fn update_raw(&mut self, price: f64) {
139 if !price.is_finite() {
140 return;
141 }
142
143 self.has_inputs = true;
144 self.count += 1;
145 if self.count < self.period {
146 self.seed_sum += price;
147 return;
148 }
149
150 if self.count == self.period {
151 self.seed_sum += price;
152 self.value = self.seed_sum / self.period as f64;
153 self.initialized = true;
154 return;
155 }
156
157 self.value = (self.value * (self.period as f64 - 1.0) + price) / self.period as f64;
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use nautilus_model::{
164 data::{Bar, QuoteTick, TradeTick},
165 enums::PriceType,
166 };
167 use rstest::rstest;
168
169 use super::MAX_PERIOD;
170 use crate::{
171 average::rma::WilderMovingAverage,
172 indicator::{Indicator, MovingAverage},
173 stubs::*,
174 };
175
176 #[rstest]
177 #[case(0)]
178 #[case(MAX_PERIOD + 1)]
179 #[case(usize::MAX)]
180 fn test_checked_constructor_rejects_invalid_period(#[case] period: usize) {
181 assert!(WilderMovingAverage::new_checked(period, None).is_err());
182 }
183
184 #[rstest]
185 fn test_rma_initialized(indicator_rma_10: WilderMovingAverage) {
186 let rma = indicator_rma_10;
187 let display_str = format!("{rma}");
188 assert_eq!(display_str, "WilderMovingAverage(10)");
189 assert_eq!(rma.period, 10);
190 assert_eq!(rma.price_type, PriceType::Mid);
191 assert_eq!(rma.alpha, 0.1);
192 assert!(!rma.initialized);
193 }
194
195 #[rstest]
196 #[should_panic(expected = "WilderMovingAverage: period must be > 0")]
197 fn test_new_with_zero_period_panics() {
198 let _ = WilderMovingAverage::new(0, None);
199 }
200
201 #[rstest]
202 fn test_one_value_input(indicator_rma_10: WilderMovingAverage) {
203 let mut rma = indicator_rma_10;
204 rma.update_raw(1.0);
205 assert_eq!(rma.count, 1);
206 assert_eq!(rma.value, 0.0);
207 assert!(!rma.initialized());
208 }
209
210 #[rstest]
211 fn test_rma_update_raw(indicator_rma_10: WilderMovingAverage) {
212 let mut rma = indicator_rma_10;
213 for value in 1..=10 {
214 rma.update_raw(f64::from(value));
215 }
216
217 assert!(rma.has_inputs());
218 assert!(rma.initialized());
219 assert_eq!(rma.count, 10);
220 assert_eq!(rma.value, 5.5);
221 }
222
223 #[rstest]
224 fn test_reset(indicator_rma_10: WilderMovingAverage) {
225 let mut rma = indicator_rma_10;
226 rma.update_raw(1.0);
227 assert_eq!(rma.count, 1);
228 rma.reset();
229 assert_eq!(rma.count, 0);
230 assert_eq!(rma.value, 0.0);
231 assert!(!rma.initialized);
232 }
233
234 #[rstest]
235 fn test_handle_quote_tick_single(indicator_rma_10: WilderMovingAverage, stub_quote: QuoteTick) {
236 let mut rma = indicator_rma_10;
237 rma.handle_quote(&stub_quote).unwrap();
238
239 assert!(rma.has_inputs());
240 assert_eq!(rma.count, 1);
241 assert_eq!(rma.value, 0.0);
242 assert!(!rma.initialized());
243 }
244
245 #[rstest]
246 fn test_handle_quote_tick_multi(mut indicator_rma_10: WilderMovingAverage) {
247 let tick1 = stub_quote("1500.0", "1502.0");
248 let tick2 = stub_quote("1502.0", "1504.0");
249 indicator_rma_10.handle_quote(&tick1).unwrap();
250 indicator_rma_10.handle_quote(&tick2).unwrap();
251
252 assert_eq!(indicator_rma_10.count, 2);
253 assert_eq!(indicator_rma_10.value, 0.0);
254 assert!(!indicator_rma_10.initialized());
255 }
256
257 #[rstest]
258 fn test_handle_trade_tick(indicator_rma_10: WilderMovingAverage, stub_trade: TradeTick) {
259 let mut rma = indicator_rma_10;
260 rma.handle_trade(&stub_trade);
261
262 assert!(rma.has_inputs());
263 assert_eq!(rma.count, 1);
264 assert_eq!(rma.value, 0.0);
265 assert!(!rma.initialized());
266 }
267
268 #[rstest]
269 fn handle_handle_bar(
270 mut indicator_rma_10: WilderMovingAverage,
271 bar_ethusdt_binance_minute_bid: Bar,
272 ) {
273 indicator_rma_10.handle_bar(&bar_ethusdt_binance_minute_bid);
274
275 assert!(indicator_rma_10.has_inputs);
276 assert_eq!(indicator_rma_10.count, 1);
277 assert_eq!(indicator_rma_10.value, 0.0);
278 assert!(!indicator_rma_10.initialized);
279 }
280
281 #[rstest]
282 #[should_panic(expected = "WilderMovingAverage: period must be > 0")]
283 fn invalid_period_panics() {
284 let _ = WilderMovingAverage::new(0, None);
285 }
286
287 #[rstest]
288 #[case(1.0)]
289 #[case(123.456)]
290 #[case(9_876.543_21)]
291 fn first_tick_seeding_parity(#[case] seed_price: f64) {
292 let mut rma = WilderMovingAverage::new(10, None);
293 rma.update_raw(seed_price);
294
295 assert_eq!(rma.count(), 1);
296 assert_eq!(rma.value(), 0.0);
297 assert!(!rma.initialized());
298 }
299
300 #[rstest]
301 fn numeric_parity_with_reference_series() {
302 let mut rma = WilderMovingAverage::new(10, None);
303 for price in 1_u32..=10 {
304 rma.update_raw(f64::from(price));
305 }
306
307 assert!(rma.initialized());
308 assert_eq!(rma.count(), 10);
309 assert_eq!(rma.value(), 5.5);
310 }
311
312 #[rstest]
314 fn test_rma_period_one_behavior() {
315 let mut rma = WilderMovingAverage::new(1, None);
316
317 rma.update_raw(42.0);
319 assert!(rma.initialized());
320 assert_eq!(rma.count(), 1);
321 assert!((rma.value() - 42.0).abs() < 1e-12);
322
323 rma.update_raw(100.0);
325 assert_eq!(rma.count(), 2);
326 assert!((rma.value() - 100.0).abs() < 1e-12);
327 }
328
329 #[rstest]
331 fn test_rma_large_period_not_initialized() {
332 let mut rma = WilderMovingAverage::new(1_000, None);
333
334 for p in 1_u32..=999 {
335 rma.update_raw(f64::from(p));
336 }
337
338 assert_eq!(rma.count(), 999);
339 assert!(!rma.initialized());
340 }
341
342 #[rstest]
343 fn test_reset_reseeds_properly() {
344 let mut rma = WilderMovingAverage::new(3, None);
345 for value in [1.0, 2.0, 3.0, 4.0] {
346 rma.update_raw(value);
347 }
348 rma.reset();
349 for value in [10.0, 20.0, 30.0] {
350 rma.update_raw(value);
351 }
352
353 assert_eq!(rma.count(), 3);
354 assert!(rma.initialized());
355 assert_eq!(rma.value(), 20.0);
356 }
357
358 #[rstest]
359 fn test_default_price_type_is_last() {
360 let rma = WilderMovingAverage::new(5, None);
361 assert_eq!(rma.price_type, PriceType::Last);
362 }
363
364 #[rstest]
365 fn test_update_with_nan_propagates() {
366 let mut rma = WilderMovingAverage::new(3, None);
367 rma.update_raw(1.0);
368 let before = (rma.value(), rma.count());
369
370 rma.update_raw(f64::NAN);
371 rma.update_raw(f64::NEG_INFINITY);
372
373 assert_eq!((rma.value(), rma.count()), before);
374 assert!(rma.has_inputs());
375 }
376}