nautilus_indicators/average/
ama.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 ratio::efficiency_ratio::EfficiencyRatio,
27};
28
29#[repr(C)]
35#[derive(Debug)]
36#[cfg_attr(
37 feature = "python",
38 pyo3::pyclass(module = "nautilus_trader.indicators")
39)]
40#[cfg_attr(
41 feature = "python",
42 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
43)]
44pub struct AdaptiveMovingAverage {
45 pub period_efficiency_ratio: usize,
47 pub period_fast: usize,
49 pub period_slow: usize,
51 pub price_type: PriceType,
53 pub value: f64,
55 pub count: usize,
57 pub initialized: bool,
58 has_inputs: bool,
59 efficiency_ratio: EfficiencyRatio,
60 prior_value: Option<f64>,
61 alpha_fast: f64,
62 alpha_slow: f64,
63}
64
65impl Display for AdaptiveMovingAverage {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(
68 f,
69 "{}({},{},{})",
70 self.name(),
71 self.period_efficiency_ratio,
72 self.period_fast,
73 self.period_slow
74 )
75 }
76}
77
78impl Indicator for AdaptiveMovingAverage {
79 fn name(&self) -> String {
80 stringify!(AdaptiveMovingAverage).to_string()
81 }
82
83 fn has_inputs(&self) -> bool {
84 self.has_inputs
85 }
86
87 fn initialized(&self) -> bool {
88 self.initialized
89 }
90
91 fn handle_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
92 self.update_raw(quote.extract_price(self.price_type)?.into());
93 Ok(())
94 }
95
96 fn handle_trade(&mut self, trade: &TradeTick) {
97 self.update_raw((&trade.price).into());
98 }
99
100 fn handle_bar(&mut self, bar: &Bar) {
101 self.update_raw((&bar.close).into());
102 }
103
104 fn reset(&mut self) {
105 self.value = 0.0;
106 self.prior_value = None;
107 self.count = 0;
108 self.has_inputs = false;
109 self.initialized = false;
110 self.efficiency_ratio.reset();
111 }
112}
113
114impl AdaptiveMovingAverage {
115 #[must_use]
127 pub fn new(
128 period_efficiency_ratio: usize,
129 period_fast: usize,
130 period_slow: usize,
131 price_type: Option<PriceType>,
132 ) -> Self {
133 Self::new_checked(
134 period_efficiency_ratio,
135 period_fast,
136 period_slow,
137 price_type,
138 )
139 .expect(FAILED)
140 }
141
142 pub(crate) fn new_checked(
143 period_efficiency_ratio: usize,
144 period_fast: usize,
145 period_slow: usize,
146 price_type: Option<PriceType>,
147 ) -> anyhow::Result<Self> {
148 check_predicate_true(period_fast > 0, "`period_fast` must be positive")?;
149 check_predicate_true(period_slow > 0, "`period_slow` must be positive")?;
150 check_predicate_true(
151 period_slow < usize::MAX,
152 "`period_slow` must be less than `usize::MAX`",
153 )?;
154 check_predicate_true(
155 period_slow > period_fast,
156 "`period_slow` must be greater than `period_fast`",
157 )?;
158
159 let efficiency_ratio = EfficiencyRatio::new_checked(period_efficiency_ratio, price_type)?;
160
161 Ok(Self {
162 period_efficiency_ratio,
163 period_fast,
164 period_slow,
165 price_type: price_type.unwrap_or(PriceType::Last),
166 value: 0.0,
167 count: 0,
168 alpha_fast: 2.0 / (period_fast + 1) as f64,
169 alpha_slow: 2.0 / (period_slow + 1) as f64,
170 prior_value: None,
171 has_inputs: false,
172 initialized: false,
173 efficiency_ratio,
174 })
175 }
176
177 #[must_use]
178 pub fn alpha_diff(&self) -> f64 {
179 self.alpha_fast - self.alpha_slow
180 }
181
182 #[must_use]
183 pub const fn alpha_fast(&self) -> f64 {
184 self.alpha_fast
185 }
186
187 #[must_use]
188 pub const fn alpha_slow(&self) -> f64 {
189 self.alpha_slow
190 }
191
192 pub fn reset(&mut self) {
193 Indicator::reset(self);
194 }
195}
196
197impl MovingAverage for AdaptiveMovingAverage {
198 fn value(&self) -> f64 {
199 self.value
200 }
201
202 fn count(&self) -> usize {
203 self.count
204 }
205
206 fn update_raw(&mut self, value: f64) {
207 if !value.is_finite() {
208 return;
209 }
210
211 self.count += 1;
212 self.has_inputs = true;
213 self.efficiency_ratio.update_raw(value);
214
215 if self.count <= self.period_efficiency_ratio {
216 self.prior_value = Some(value);
218 return;
219 }
220 let prior = self.prior_value.unwrap_or(value);
221
222 let smoothing = self
223 .efficiency_ratio
224 .value
225 .mul_add(self.alpha_diff(), self.alpha_slow)
226 .powi(2);
227 let current = smoothing.mul_add(value - prior, prior);
228 self.prior_value = Some(current);
229 self.value = current;
230 self.initialized = true;
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use nautilus_model::data::{Bar, QuoteTick, TradeTick};
237 use rstest::rstest;
238
239 use crate::{
240 average::ama::AdaptiveMovingAverage,
241 indicator::{Indicator, MovingAverage},
242 stubs::*,
243 testing::assert_approx_equal,
244 };
245
246 #[rstest]
247 fn test_ama_initialized(indicator_ama_10: AdaptiveMovingAverage) {
248 let display_str = format!("{indicator_ama_10}");
249 assert_eq!(display_str, "AdaptiveMovingAverage(10,2,30)");
250 assert_eq!(indicator_ama_10.name(), "AdaptiveMovingAverage");
251 assert!(!indicator_ama_10.has_inputs());
252 assert!(!indicator_ama_10.initialized());
253 }
254
255 #[rstest]
256 fn test_value_with_one_input(mut indicator_ama_10: AdaptiveMovingAverage) {
257 indicator_ama_10.update_raw(1.0);
258 assert_eq!(indicator_ama_10.value, 0.0);
259 assert!(!indicator_ama_10.initialized());
260 }
261
262 #[rstest]
263 fn test_value_with_two_inputs(mut indicator_ama_10: AdaptiveMovingAverage) {
264 for value in [1.0, 2.0] {
265 indicator_ama_10.update_raw(value);
266 }
267 assert_eq!(indicator_ama_10.value, 0.0);
268 assert!(!indicator_ama_10.initialized());
269 }
270
271 #[rstest]
272 fn test_value_with_three_inputs(mut indicator_ama_10: AdaptiveMovingAverage) {
273 for value in [1.0, 2.0, 3.0] {
274 indicator_ama_10.update_raw(value);
275 }
276 assert_eq!(indicator_ama_10.value, 0.0);
277 assert!(!indicator_ama_10.initialized());
278 }
279
280 #[rstest]
281 #[case::inherent(AdaptiveMovingAverage::reset)]
282 #[case::indicator(<AdaptiveMovingAverage as Indicator>::reset)]
283 fn test_reset(
284 #[case] reset: fn(&mut AdaptiveMovingAverage),
285 mut indicator_ama_10: AdaptiveMovingAverage,
286 ) {
287 for value in 1..=11 {
288 indicator_ama_10.update_raw(f64::from(value));
289 }
290 assert!(indicator_ama_10.initialized);
291 reset(&mut indicator_ama_10);
292 assert!(!indicator_ama_10.initialized);
293 assert!(!indicator_ama_10.has_inputs);
294 assert_eq!(indicator_ama_10.value, 0.0);
295 assert_eq!(indicator_ama_10.prior_value, None);
296 assert_eq!(indicator_ama_10.count, 0);
297 assert!(!indicator_ama_10.efficiency_ratio.has_inputs());
298 assert!(!indicator_ama_10.efficiency_ratio.initialized());
299 assert_eq!(indicator_ama_10.efficiency_ratio.value, 0.0);
300 }
301
302 #[rstest]
303 fn test_initialized_after_correct_number_of_input(mut indicator_ama_10: AdaptiveMovingAverage) {
304 for value in 1..=10 {
305 indicator_ama_10.update_raw(f64::from(value));
306 assert!(!indicator_ama_10.initialized());
307 }
308 indicator_ama_10.update_raw(11.0);
309 assert!(indicator_ama_10.initialized());
310 }
311
312 #[rstest]
313 fn test_first_value_seeds_from_previous_input(mut indicator_ama_10: AdaptiveMovingAverage) {
314 for value in 1..=11 {
316 indicator_ama_10.update_raw(f64::from(value));
317 }
318 assert_approx_equal(indicator_ama_10.value, 10.0 + 4.0 / 9.0);
319 }
320
321 #[rstest]
322 fn test_count_increments(mut indicator_ama_10: AdaptiveMovingAverage) {
323 assert_eq!(indicator_ama_10.count(), 0);
324 indicator_ama_10.update_raw(1.0);
325 assert_eq!(indicator_ama_10.count(), 1);
326 indicator_ama_10.update_raw(2.0);
327 indicator_ama_10.update_raw(3.0);
328 assert_eq!(indicator_ama_10.count(), 3);
329 }
330
331 #[rstest]
332 fn test_handle_quote_tick(mut indicator_ama_10: AdaptiveMovingAverage, stub_quote: QuoteTick) {
333 indicator_ama_10.handle_quote(&stub_quote).unwrap();
334 assert!(indicator_ama_10.has_inputs);
335 assert!(!indicator_ama_10.initialized);
336 assert_eq!(indicator_ama_10.value, 0.0);
337 assert_eq!(indicator_ama_10.count(), 1);
338 }
339
340 #[rstest]
341 fn test_handle_trade_tick_update(
342 mut indicator_ama_10: AdaptiveMovingAverage,
343 stub_trade: TradeTick,
344 ) {
345 indicator_ama_10.handle_trade(&stub_trade);
346 assert!(indicator_ama_10.has_inputs);
347 assert!(!indicator_ama_10.initialized);
348 assert_eq!(indicator_ama_10.value, 0.0);
349 assert_eq!(indicator_ama_10.count(), 1);
350 }
351
352 #[rstest]
353 fn handle_handle_bar(
354 mut indicator_ama_10: AdaptiveMovingAverage,
355 bar_ethusdt_binance_minute_bid: Bar,
356 ) {
357 indicator_ama_10.handle_bar(&bar_ethusdt_binance_minute_bid);
358 assert!(indicator_ama_10.has_inputs);
359 assert!(!indicator_ama_10.initialized);
360 assert_eq!(indicator_ama_10.value, 0.0);
361 assert_eq!(indicator_ama_10.count(), 1);
362 }
363
364 #[rstest]
365 fn new_panics_when_slow_not_greater_than_fast() {
366 let result = std::panic::catch_unwind(|| {
367 let _ = AdaptiveMovingAverage::new(10, 20, 20, None);
368 });
369 assert!(result.is_err());
370 }
371
372 #[rstest]
373 #[case(0)]
374 #[should_panic(expected = "`period` must be positive")]
375 fn new_panics_when_er_period_is_zero(#[case] period: usize) {
376 let _ = AdaptiveMovingAverage::new(period, 2, 30, None);
377 }
378
379 #[rstest]
380 fn new_panics_when_fast_is_zero() {
381 let result = std::panic::catch_unwind(|| {
382 let _ = AdaptiveMovingAverage::new(10, 0, 30, None);
383 });
384 assert!(result.is_err());
385 }
386
387 #[rstest]
388 fn new_panics_when_slow_is_zero() {
389 let result = std::panic::catch_unwind(|| {
390 let _ = AdaptiveMovingAverage::new(10, 2, 0, None);
391 });
392 assert!(result.is_err());
393 }
394
395 #[rstest]
396 fn new_panics_when_slow_less_than_fast() {
397 let result = std::panic::catch_unwind(|| {
398 let _ = AdaptiveMovingAverage::new(10, 20, 5, None);
399 });
400 assert!(result.is_err());
401 }
402
403 #[rstest]
404 fn new_checked_rejects_slow_period_max() {
405 let error =
406 AdaptiveMovingAverage::new_checked(10, usize::MAX - 1, usize::MAX, None).unwrap_err();
407
408 assert_eq!(
409 error.to_string(),
410 "`period_slow` must be less than `usize::MAX`",
411 );
412 }
413}