nautilus_indicators/average/
ama.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.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 = "nautilus_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#[cfg(test)]
205mod tests {
206    use nautilus_model::data::{Bar, QuoteTick, TradeTick};
207    use rstest::rstest;
208
209    use crate::{
210        average::ama::AdaptiveMovingAverage,
211        indicator::{Indicator, MovingAverage},
212        stubs::*,
213    };
214
215    #[rstest]
216    fn test_ama_initialized(indicator_ama_10: AdaptiveMovingAverage) {
217        let display_str = format!("{indicator_ama_10}");
218        assert_eq!(display_str, "AdaptiveMovingAverage(10,2,30)");
219        assert_eq!(indicator_ama_10.name(), "AdaptiveMovingAverage");
220        assert!(!indicator_ama_10.has_inputs());
221        assert!(!indicator_ama_10.initialized());
222    }
223
224    #[rstest]
225    fn test_value_with_one_input(mut indicator_ama_10: AdaptiveMovingAverage) {
226        indicator_ama_10.update_raw(1.0);
227        assert_eq!(indicator_ama_10.value, 1.0);
228    }
229
230    #[rstest]
231    fn test_value_with_two_inputs(mut indicator_ama_10: AdaptiveMovingAverage) {
232        indicator_ama_10.update_raw(1.0);
233        indicator_ama_10.update_raw(2.0);
234        assert_eq!(indicator_ama_10.value, 1.444_444_444_444_444_2);
235    }
236
237    #[rstest]
238    fn test_value_with_three_inputs(mut indicator_ama_10: AdaptiveMovingAverage) {
239        indicator_ama_10.update_raw(1.0);
240        indicator_ama_10.update_raw(2.0);
241        indicator_ama_10.update_raw(3.0);
242        assert_eq!(indicator_ama_10.value, 2.135_802_469_135_802);
243    }
244
245    #[rstest]
246    fn test_reset(mut indicator_ama_10: AdaptiveMovingAverage) {
247        for _ in 0..10 {
248            indicator_ama_10.update_raw(1.0);
249        }
250        assert!(indicator_ama_10.initialized);
251        indicator_ama_10.reset();
252        assert!(!indicator_ama_10.initialized);
253        assert!(!indicator_ama_10.has_inputs);
254        assert_eq!(indicator_ama_10.value, 0.0);
255        assert_eq!(indicator_ama_10.count, 0);
256    }
257
258    #[rstest]
259    fn test_initialized_after_correct_number_of_input(indicator_ama_10: AdaptiveMovingAverage) {
260        let mut ama = indicator_ama_10;
261        for _ in 0..9 {
262            ama.update_raw(1.0);
263        }
264        assert!(!ama.initialized);
265        ama.update_raw(1.0);
266        assert!(ama.initialized);
267    }
268
269    #[rstest]
270    fn test_count_increments(mut indicator_ama_10: AdaptiveMovingAverage) {
271        assert_eq!(indicator_ama_10.count(), 0);
272        indicator_ama_10.update_raw(1.0);
273        assert_eq!(indicator_ama_10.count(), 1);
274        indicator_ama_10.update_raw(2.0);
275        indicator_ama_10.update_raw(3.0);
276        assert_eq!(indicator_ama_10.count(), 3);
277    }
278
279    #[rstest]
280    fn test_handle_quote_tick(mut indicator_ama_10: AdaptiveMovingAverage, stub_quote: QuoteTick) {
281        indicator_ama_10.handle_quote(&stub_quote);
282        assert!(indicator_ama_10.has_inputs);
283        assert!(!indicator_ama_10.initialized);
284        assert_eq!(indicator_ama_10.value, 1501.0);
285        assert_eq!(indicator_ama_10.count(), 1);
286    }
287
288    #[rstest]
289    fn test_handle_trade_tick_update(
290        mut indicator_ama_10: AdaptiveMovingAverage,
291        stub_trade: TradeTick,
292    ) {
293        indicator_ama_10.handle_trade(&stub_trade);
294        assert!(indicator_ama_10.has_inputs);
295        assert!(!indicator_ama_10.initialized);
296        assert_eq!(indicator_ama_10.value, 1500.0);
297        assert_eq!(indicator_ama_10.count(), 1);
298    }
299
300    #[rstest]
301    fn handle_handle_bar(
302        mut indicator_ama_10: AdaptiveMovingAverage,
303        bar_ethusdt_binance_minute_bid: Bar,
304    ) {
305        indicator_ama_10.handle_bar(&bar_ethusdt_binance_minute_bid);
306        assert!(indicator_ama_10.has_inputs);
307        assert!(!indicator_ama_10.initialized);
308        assert_eq!(indicator_ama_10.value, 1522.0);
309        assert_eq!(indicator_ama_10.count(), 1);
310    }
311
312    #[rstest]
313    fn new_panics_when_slow_not_greater_than_fast() {
314        let result = std::panic::catch_unwind(|| {
315            let _ = AdaptiveMovingAverage::new(10, 20, 20, None);
316        });
317        assert!(result.is_err());
318    }
319
320    #[rstest]
321    fn new_panics_when_er_is_zero() {
322        let result = std::panic::catch_unwind(|| {
323            let _ = AdaptiveMovingAverage::new(0, 2, 30, None);
324        });
325        assert!(result.is_err());
326    }
327
328    #[rstest]
329    fn new_panics_when_fast_is_zero() {
330        let result = std::panic::catch_unwind(|| {
331            let _ = AdaptiveMovingAverage::new(10, 0, 30, None);
332        });
333        assert!(result.is_err());
334    }
335
336    #[rstest]
337    fn new_panics_when_slow_is_zero() {
338        let result = std::panic::catch_unwind(|| {
339            let _ = AdaptiveMovingAverage::new(10, 2, 0, None);
340        });
341        assert!(result.is_err());
342    }
343
344    #[rstest]
345    fn new_panics_when_slow_less_than_fast() {
346        let result = std::panic::catch_unwind(|| {
347            let _ = AdaptiveMovingAverage::new(10, 20, 5, None);
348        });
349        assert!(result.is_err());
350    }
351}