nautilus_indicators/average/
ama.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::fmt::Display;
17
18use nautilus_model::{
19    data::{Bar, QuoteTick, TradeTick},
20    enums::PriceType,
21};
22
23use crate::{
24    indicator::{Indicator, MovingAverage},
25    ratio::efficiency_ratio::EfficiencyRatio,
26};
27
28/// An indicator which calculates an adaptive moving average (AMA) across a
29/// rolling window. Developed by Perry Kaufman, the AMA is a moving average
30/// designed to account for market noise and volatility. The AMA will closely
31/// follow prices when the price swings are relatively small and the noise is
32/// low. The AMA will increase lag when the price swings increase.
33#[repr(C)]
34#[derive(Debug)]
35#[cfg_attr(
36    feature = "python",
37    pyo3::pyclass(module = "posei_trader.core.nautilus_pyo3.indicators")
38)]
39pub struct AdaptiveMovingAverage {
40    /// The period for the internal `EfficiencyRatio` indicator.
41    pub period_efficiency_ratio: usize,
42    /// The period for the fast smoothing constant (> 0).
43    pub period_fast: usize,
44    /// The period for the slow smoothing constant (> `period_fast`).
45    pub period_slow: usize,
46    /// The price type used for calculations.
47    pub price_type: PriceType,
48    /// The last indicator value.
49    pub value: f64,
50    /// The input count for the indicator.
51    pub count: usize,
52    pub initialized: bool,
53    has_inputs: bool,
54    efficiency_ratio: EfficiencyRatio,
55    prior_value: Option<f64>,
56    alpha_fast: f64,
57    alpha_slow: f64,
58}
59
60impl Display for AdaptiveMovingAverage {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(
63            f,
64            "{}({},{},{})",
65            self.name(),
66            self.period_efficiency_ratio,
67            self.period_fast,
68            self.period_slow
69        )
70    }
71}
72
73impl Indicator for AdaptiveMovingAverage {
74    fn name(&self) -> String {
75        stringify!(AdaptiveMovingAverage).to_string()
76    }
77
78    fn has_inputs(&self) -> bool {
79        self.has_inputs
80    }
81
82    fn initialized(&self) -> bool {
83        self.initialized
84    }
85
86    fn handle_quote(&mut self, quote: &QuoteTick) {
87        self.update_raw(quote.extract_price(self.price_type).into());
88    }
89
90    fn handle_trade(&mut self, trade: &TradeTick) {
91        self.update_raw((&trade.price).into());
92    }
93
94    fn handle_bar(&mut self, bar: &Bar) {
95        self.update_raw((&bar.close).into());
96    }
97
98    fn reset(&mut self) {
99        self.value = 0.0;
100        self.count = 0;
101        self.has_inputs = false;
102        self.initialized = false;
103    }
104}
105
106impl AdaptiveMovingAverage {
107    /// Creates a new [`AdaptiveMovingAverage`] instance.
108    ///
109    /// # Panics
110    ///
111    /// This function panics if:
112    /// - `period_efficiency_ratio` == 0.
113    /// - `period_fast` == 0.
114    /// - `period_slow` == 0.
115    /// - `period_slow` ≤ `period_fast`.
116    #[must_use]
117    pub fn new(
118        period_efficiency_ratio: usize,
119        period_fast: usize,
120        period_slow: usize,
121        price_type: Option<PriceType>,
122    ) -> Self {
123        assert!(
124            period_efficiency_ratio > 0,
125            "period_efficiency_ratio must be a positive integer"
126        );
127        assert!(period_fast > 0, "period_fast must be a positive integer");
128        assert!(period_slow > 0, "period_slow must be a positive integer");
129        assert!(
130            period_slow > period_fast,
131            "period_slow ({period_slow}) must be greater than period_fast ({period_fast})"
132        );
133        Self {
134            period_efficiency_ratio,
135            period_fast,
136            period_slow,
137            price_type: price_type.unwrap_or(PriceType::Last),
138            value: 0.0,
139            count: 0,
140            alpha_fast: 2.0 / (period_fast + 1) as f64,
141            alpha_slow: 2.0 / (period_slow + 1) as f64,
142            prior_value: None,
143            has_inputs: false,
144            initialized: false,
145            efficiency_ratio: EfficiencyRatio::new(period_efficiency_ratio, price_type),
146        }
147    }
148
149    #[must_use]
150    pub fn alpha_diff(&self) -> f64 {
151        self.alpha_fast - self.alpha_slow
152    }
153
154    pub const fn reset(&mut self) {
155        self.value = 0.0;
156        self.prior_value = None;
157        self.count = 0;
158        self.has_inputs = false;
159        self.initialized = false;
160    }
161}
162
163impl MovingAverage for AdaptiveMovingAverage {
164    fn value(&self) -> f64 {
165        self.value
166    }
167
168    fn count(&self) -> usize {
169        self.count
170    }
171
172    fn update_raw(&mut self, value: f64) {
173        self.count += 1;
174
175        if !self.has_inputs {
176            self.prior_value = Some(value);
177            self.efficiency_ratio.update_raw(value);
178            self.value = value;
179            self.has_inputs = true;
180            return;
181        }
182
183        self.efficiency_ratio.update_raw(value);
184        self.prior_value = Some(self.value);
185
186        // Calculate the smoothing constant
187        let smoothing_constant = self
188            .efficiency_ratio
189            .value
190            .mul_add(self.alpha_diff(), self.alpha_slow)
191            .powi(2);
192
193        // Calculate the AMA
194        // TODO: Remove unwraps
195        self.value = smoothing_constant
196            .mul_add(value - self.prior_value.unwrap(), self.prior_value.unwrap());
197
198        if self.efficiency_ratio.initialized() {
199            self.initialized = true;
200        }
201    }
202}
203
204////////////////////////////////////////////////////////////////////////////////
205// Tests
206////////////////////////////////////////////////////////////////////////////////
207#[cfg(test)]
208mod tests {
209    use nautilus_model::data::{Bar, QuoteTick, TradeTick};
210    use rstest::rstest;
211
212    use crate::{
213        average::ama::AdaptiveMovingAverage,
214        indicator::{Indicator, MovingAverage},
215        stubs::*,
216    };
217
218    #[rstest]
219    fn test_ama_initialized(indicator_ama_10: AdaptiveMovingAverage) {
220        let display_str = format!("{indicator_ama_10}");
221        assert_eq!(display_str, "AdaptiveMovingAverage(10,2,30)");
222        assert_eq!(indicator_ama_10.name(), "AdaptiveMovingAverage");
223        assert!(!indicator_ama_10.has_inputs());
224        assert!(!indicator_ama_10.initialized());
225    }
226
227    #[rstest]
228    fn test_value_with_one_input(mut indicator_ama_10: AdaptiveMovingAverage) {
229        indicator_ama_10.update_raw(1.0);
230        assert_eq!(indicator_ama_10.value, 1.0);
231    }
232
233    #[rstest]
234    fn test_value_with_two_inputs(mut indicator_ama_10: AdaptiveMovingAverage) {
235        indicator_ama_10.update_raw(1.0);
236        indicator_ama_10.update_raw(2.0);
237        assert_eq!(indicator_ama_10.value, 1.444_444_444_444_444_2);
238    }
239
240    #[rstest]
241    fn test_value_with_three_inputs(mut indicator_ama_10: AdaptiveMovingAverage) {
242        indicator_ama_10.update_raw(1.0);
243        indicator_ama_10.update_raw(2.0);
244        indicator_ama_10.update_raw(3.0);
245        assert_eq!(indicator_ama_10.value, 2.135_802_469_135_802);
246    }
247
248    #[rstest]
249    fn test_reset(mut indicator_ama_10: AdaptiveMovingAverage) {
250        for _ in 0..10 {
251            indicator_ama_10.update_raw(1.0);
252        }
253        assert!(indicator_ama_10.initialized);
254        indicator_ama_10.reset();
255        assert!(!indicator_ama_10.initialized);
256        assert!(!indicator_ama_10.has_inputs);
257        assert_eq!(indicator_ama_10.value, 0.0);
258        assert_eq!(indicator_ama_10.count, 0);
259    }
260
261    #[rstest]
262    fn test_initialized_after_correct_number_of_input(indicator_ama_10: AdaptiveMovingAverage) {
263        let mut ama = indicator_ama_10;
264        for _ in 0..9 {
265            ama.update_raw(1.0);
266        }
267        assert!(!ama.initialized);
268        ama.update_raw(1.0);
269        assert!(ama.initialized);
270    }
271
272    #[rstest]
273    fn test_count_increments(mut indicator_ama_10: AdaptiveMovingAverage) {
274        assert_eq!(indicator_ama_10.count(), 0);
275        indicator_ama_10.update_raw(1.0);
276        assert_eq!(indicator_ama_10.count(), 1);
277        indicator_ama_10.update_raw(2.0);
278        indicator_ama_10.update_raw(3.0);
279        assert_eq!(indicator_ama_10.count(), 3);
280    }
281
282    #[rstest]
283    fn test_handle_quote_tick(mut indicator_ama_10: AdaptiveMovingAverage, stub_quote: QuoteTick) {
284        indicator_ama_10.handle_quote(&stub_quote);
285        assert!(indicator_ama_10.has_inputs);
286        assert!(!indicator_ama_10.initialized);
287        assert_eq!(indicator_ama_10.value, 1501.0);
288        assert_eq!(indicator_ama_10.count(), 1);
289    }
290
291    #[rstest]
292    fn test_handle_trade_tick_update(
293        mut indicator_ama_10: AdaptiveMovingAverage,
294        stub_trade: TradeTick,
295    ) {
296        indicator_ama_10.handle_trade(&stub_trade);
297        assert!(indicator_ama_10.has_inputs);
298        assert!(!indicator_ama_10.initialized);
299        assert_eq!(indicator_ama_10.value, 1500.0);
300        assert_eq!(indicator_ama_10.count(), 1);
301    }
302
303    #[rstest]
304    fn handle_handle_bar(
305        mut indicator_ama_10: AdaptiveMovingAverage,
306        bar_ethusdt_binance_minute_bid: Bar,
307    ) {
308        indicator_ama_10.handle_bar(&bar_ethusdt_binance_minute_bid);
309        assert!(indicator_ama_10.has_inputs);
310        assert!(!indicator_ama_10.initialized);
311        assert_eq!(indicator_ama_10.value, 1522.0);
312        assert_eq!(indicator_ama_10.count(), 1);
313    }
314
315    #[rstest]
316    fn new_panics_when_slow_not_greater_than_fast() {
317        let result = std::panic::catch_unwind(|| {
318            let _ = AdaptiveMovingAverage::new(10, 20, 20, None);
319        });
320        assert!(result.is_err());
321    }
322
323    #[rstest]
324    fn new_panics_when_er_is_zero() {
325        let result = std::panic::catch_unwind(|| {
326            let _ = AdaptiveMovingAverage::new(0, 2, 30, None);
327        });
328        assert!(result.is_err());
329    }
330
331    #[rstest]
332    fn new_panics_when_fast_is_zero() {
333        let result = std::panic::catch_unwind(|| {
334            let _ = AdaptiveMovingAverage::new(10, 0, 30, None);
335        });
336        assert!(result.is_err());
337    }
338
339    #[rstest]
340    fn new_panics_when_slow_is_zero() {
341        let result = std::panic::catch_unwind(|| {
342            let _ = AdaptiveMovingAverage::new(10, 2, 0, None);
343        });
344        assert!(result.is_err());
345    }
346
347    #[rstest]
348    fn new_panics_when_slow_less_than_fast() {
349        let result = std::panic::catch_unwind(|| {
350            let _ = AdaptiveMovingAverage::new(10, 20, 5, None);
351        });
352        assert!(result.is_err());
353    }
354}