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 self.count += 1;
208
209 if !self.has_inputs {
210 self.prior_value = Some(value);
211 self.efficiency_ratio.update_raw(value);
212 self.value = value;
213 self.has_inputs = true;
214 return;
215 }
216
217 self.efficiency_ratio.update_raw(value);
218 self.prior_value = Some(self.value);
219
220 let smoothing_constant = self
222 .efficiency_ratio
223 .value
224 .mul_add(self.alpha_diff(), self.alpha_slow)
225 .powi(2);
226
227 self.value = smoothing_constant
230 .mul_add(value - self.prior_value.unwrap(), self.prior_value.unwrap());
231
232 if self.efficiency_ratio.initialized() {
233 self.initialized = true;
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use nautilus_model::data::{Bar, QuoteTick, TradeTick};
241 use rstest::rstest;
242
243 use crate::{
244 average::ama::AdaptiveMovingAverage,
245 indicator::{Indicator, MovingAverage},
246 stubs::*,
247 testing::assert_approx_equal,
248 };
249
250 #[rstest]
251 fn test_ama_initialized(indicator_ama_10: AdaptiveMovingAverage) {
252 let display_str = format!("{indicator_ama_10}");
253 assert_eq!(display_str, "AdaptiveMovingAverage(10,2,30)");
254 assert_eq!(indicator_ama_10.name(), "AdaptiveMovingAverage");
255 assert!(!indicator_ama_10.has_inputs());
256 assert!(!indicator_ama_10.initialized());
257 }
258
259 #[rstest]
260 fn test_value_with_one_input(mut indicator_ama_10: AdaptiveMovingAverage) {
261 indicator_ama_10.update_raw(1.0);
262 assert_eq!(indicator_ama_10.value, 1.0);
263 }
264
265 #[rstest]
266 fn test_value_with_two_inputs(mut indicator_ama_10: AdaptiveMovingAverage) {
267 indicator_ama_10.update_raw(1.0);
268 indicator_ama_10.update_raw(2.0);
269 assert_approx_equal(indicator_ama_10.value, 1.44444444444);
270 }
271
272 #[rstest]
273 fn test_value_with_three_inputs(mut indicator_ama_10: AdaptiveMovingAverage) {
274 indicator_ama_10.update_raw(1.0);
275 indicator_ama_10.update_raw(2.0);
276 indicator_ama_10.update_raw(3.0);
277 assert_approx_equal(indicator_ama_10.value, 2.13580246914);
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..=10 {
288 indicator_ama_10.update_raw(f64::from(value));
289 }
290 assert!(indicator_ama_10.initialized);
291
292 reset(&mut indicator_ama_10);
293
294 assert!(!indicator_ama_10.initialized);
295 assert!(!indicator_ama_10.has_inputs);
296 assert_eq!(indicator_ama_10.value, 0.0);
297 assert_eq!(indicator_ama_10.prior_value, None);
298 assert_eq!(indicator_ama_10.count, 0);
299 assert!(!indicator_ama_10.efficiency_ratio.has_inputs());
300 assert!(!indicator_ama_10.efficiency_ratio.initialized());
301 assert_eq!(indicator_ama_10.efficiency_ratio.value, 0.0);
302 }
303
304 #[rstest]
305 fn test_initialized_after_correct_number_of_input(indicator_ama_10: AdaptiveMovingAverage) {
306 let mut ama = indicator_ama_10;
307 for _ in 0..9 {
308 ama.update_raw(1.0);
309 }
310 assert!(!ama.initialized);
311 ama.update_raw(1.0);
312 assert!(ama.initialized);
313 }
314
315 #[rstest]
316 fn test_count_increments(mut indicator_ama_10: AdaptiveMovingAverage) {
317 assert_eq!(indicator_ama_10.count(), 0);
318 indicator_ama_10.update_raw(1.0);
319 assert_eq!(indicator_ama_10.count(), 1);
320 indicator_ama_10.update_raw(2.0);
321 indicator_ama_10.update_raw(3.0);
322 assert_eq!(indicator_ama_10.count(), 3);
323 }
324
325 #[rstest]
326 fn test_handle_quote_tick(mut indicator_ama_10: AdaptiveMovingAverage, stub_quote: QuoteTick) {
327 indicator_ama_10.handle_quote(&stub_quote).unwrap();
328 assert!(indicator_ama_10.has_inputs);
329 assert!(!indicator_ama_10.initialized);
330 assert_eq!(indicator_ama_10.value, 1501.0);
331 assert_eq!(indicator_ama_10.count(), 1);
332 }
333
334 #[rstest]
335 fn test_handle_trade_tick_update(
336 mut indicator_ama_10: AdaptiveMovingAverage,
337 stub_trade: TradeTick,
338 ) {
339 indicator_ama_10.handle_trade(&stub_trade);
340 assert!(indicator_ama_10.has_inputs);
341 assert!(!indicator_ama_10.initialized);
342 assert_eq!(indicator_ama_10.value, 1500.0);
343 assert_eq!(indicator_ama_10.count(), 1);
344 }
345
346 #[rstest]
347 fn handle_handle_bar(
348 mut indicator_ama_10: AdaptiveMovingAverage,
349 bar_ethusdt_binance_minute_bid: Bar,
350 ) {
351 indicator_ama_10.handle_bar(&bar_ethusdt_binance_minute_bid);
352 assert!(indicator_ama_10.has_inputs);
353 assert!(!indicator_ama_10.initialized);
354 assert_eq!(indicator_ama_10.value, 1522.0);
355 assert_eq!(indicator_ama_10.count(), 1);
356 }
357
358 #[rstest]
359 fn new_panics_when_slow_not_greater_than_fast() {
360 let result = std::panic::catch_unwind(|| {
361 let _ = AdaptiveMovingAverage::new(10, 20, 20, None);
362 });
363 assert!(result.is_err());
364 }
365
366 #[rstest]
367 #[case(0)]
368 #[case(1)]
369 #[should_panic(expected = "`period` must be at least 2")]
370 fn new_panics_when_er_period_is_below_two(#[case] period: usize) {
371 let _ = AdaptiveMovingAverage::new(period, 2, 30, None);
372 }
373
374 #[rstest]
375 fn new_panics_when_fast_is_zero() {
376 let result = std::panic::catch_unwind(|| {
377 let _ = AdaptiveMovingAverage::new(10, 0, 30, None);
378 });
379 assert!(result.is_err());
380 }
381
382 #[rstest]
383 fn new_panics_when_slow_is_zero() {
384 let result = std::panic::catch_unwind(|| {
385 let _ = AdaptiveMovingAverage::new(10, 2, 0, None);
386 });
387 assert!(result.is_err());
388 }
389
390 #[rstest]
391 fn new_panics_when_slow_less_than_fast() {
392 let result = std::panic::catch_unwind(|| {
393 let _ = AdaptiveMovingAverage::new(10, 20, 5, None);
394 });
395 assert!(result.is_err());
396 }
397
398 #[rstest]
399 fn new_checked_rejects_slow_period_max() {
400 let error =
401 AdaptiveMovingAverage::new_checked(10, usize::MAX - 1, usize::MAX, None).unwrap_err();
402
403 assert_eq!(
404 error.to_string(),
405 "`period_slow` must be less than `usize::MAX`",
406 );
407 }
408}