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 (> 0 < `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    #[must_use]
109    pub fn new(
110        period_efficiency_ratio: usize,
111        period_fast: usize,
112        period_slow: usize,
113        price_type: Option<PriceType>,
114    ) -> Self {
115        Self {
116            period_efficiency_ratio,
117            period_fast,
118            period_slow,
119            price_type: price_type.unwrap_or(PriceType::Last),
120            value: 0.0,
121            count: 0,
122            alpha_fast: 2.0 / (period_fast + 1) as f64,
123            alpha_slow: 2.0 / (period_slow + 1) as f64,
124            prior_value: None,
125            has_inputs: false,
126            initialized: false,
127            efficiency_ratio: EfficiencyRatio::new(period_efficiency_ratio, price_type),
128        }
129    }
130
131    #[must_use]
132    pub fn alpha_diff(&self) -> f64 {
133        self.alpha_fast - self.alpha_slow
134    }
135
136    pub const fn reset(&mut self) {
137        self.value = 0.0;
138        self.prior_value = None;
139        self.count = 0;
140        self.has_inputs = false;
141        self.initialized = false;
142    }
143}
144
145impl MovingAverage for AdaptiveMovingAverage {
146    fn value(&self) -> f64 {
147        self.value
148    }
149
150    fn count(&self) -> usize {
151        self.count
152    }
153
154    fn update_raw(&mut self, value: f64) {
155        if !self.has_inputs {
156            self.prior_value = Some(value);
157            self.efficiency_ratio.update_raw(value);
158            self.value = value;
159            self.has_inputs = true;
160            return;
161        }
162        self.efficiency_ratio.update_raw(value);
163        self.prior_value = Some(self.value);
164
165        // Calculate the smoothing constant
166        let smoothing_constant = self
167            .efficiency_ratio
168            .value
169            .mul_add(self.alpha_diff(), self.alpha_slow)
170            .powi(2);
171
172        // Calculate the AMA
173        // TODO: Remove unwraps
174        self.value = smoothing_constant
175            .mul_add(value - self.prior_value.unwrap(), self.prior_value.unwrap());
176        if self.efficiency_ratio.initialized() {
177            self.initialized = true;
178        }
179    }
180}
181
182////////////////////////////////////////////////////////////////////////////////
183// Tests
184////////////////////////////////////////////////////////////////////////////////
185#[cfg(test)]
186mod tests {
187    use nautilus_model::data::{Bar, QuoteTick, TradeTick};
188    use rstest::rstest;
189
190    use crate::{
191        average::ama::AdaptiveMovingAverage,
192        indicator::{Indicator, MovingAverage},
193        stubs::*,
194    };
195
196    #[rstest]
197    fn test_ama_initialized(indicator_ama_10: AdaptiveMovingAverage) {
198        let display_str = format!("{indicator_ama_10}");
199        assert_eq!(display_str, "AdaptiveMovingAverage(10,2,30)");
200        assert_eq!(indicator_ama_10.name(), "AdaptiveMovingAverage");
201        assert!(!indicator_ama_10.has_inputs());
202        assert!(!indicator_ama_10.initialized());
203    }
204
205    #[rstest]
206    fn test_value_with_one_input(mut indicator_ama_10: AdaptiveMovingAverage) {
207        indicator_ama_10.update_raw(1.0);
208        assert_eq!(indicator_ama_10.value, 1.0);
209    }
210
211    #[rstest]
212    fn test_value_with_two_inputs(mut indicator_ama_10: AdaptiveMovingAverage) {
213        indicator_ama_10.update_raw(1.0);
214        indicator_ama_10.update_raw(2.0);
215        assert_eq!(indicator_ama_10.value, 1.444_444_444_444_444_2);
216    }
217
218    #[rstest]
219    fn test_value_with_three_inputs(mut indicator_ama_10: AdaptiveMovingAverage) {
220        indicator_ama_10.update_raw(1.0);
221        indicator_ama_10.update_raw(2.0);
222        indicator_ama_10.update_raw(3.0);
223        assert_eq!(indicator_ama_10.value, 2.135_802_469_135_802);
224    }
225
226    #[rstest]
227    fn test_reset(mut indicator_ama_10: AdaptiveMovingAverage) {
228        for _ in 0..10 {
229            indicator_ama_10.update_raw(1.0);
230        }
231        assert!(indicator_ama_10.initialized);
232        indicator_ama_10.reset();
233        assert!(!indicator_ama_10.initialized);
234        assert!(!indicator_ama_10.has_inputs);
235        assert_eq!(indicator_ama_10.value, 0.0);
236    }
237
238    #[rstest]
239    fn test_initialized_after_correct_number_of_input(indicator_ama_10: AdaptiveMovingAverage) {
240        let mut ama = indicator_ama_10;
241        for _ in 0..9 {
242            ama.update_raw(1.0);
243        }
244        assert!(!ama.initialized);
245        ama.update_raw(1.0);
246        assert!(ama.initialized);
247    }
248
249    #[rstest]
250    fn test_handle_quote_tick(mut indicator_ama_10: AdaptiveMovingAverage, stub_quote: QuoteTick) {
251        indicator_ama_10.handle_quote(&stub_quote);
252        assert!(indicator_ama_10.has_inputs);
253        assert!(!indicator_ama_10.initialized);
254        assert_eq!(indicator_ama_10.value, 1501.0);
255    }
256
257    #[rstest]
258    fn test_handle_trade_tick_update(
259        mut indicator_ama_10: AdaptiveMovingAverage,
260        stub_trade: TradeTick,
261    ) {
262        indicator_ama_10.handle_trade(&stub_trade);
263        assert!(indicator_ama_10.has_inputs);
264        assert!(!indicator_ama_10.initialized);
265        assert_eq!(indicator_ama_10.value, 1500.0);
266    }
267
268    #[rstest]
269    fn handle_handle_bar(
270        mut indicator_ama_10: AdaptiveMovingAverage,
271        bar_ethusdt_binance_minute_bid: Bar,
272    ) {
273        indicator_ama_10.handle_bar(&bar_ethusdt_binance_minute_bid);
274        assert!(indicator_ama_10.has_inputs);
275        assert!(!indicator_ama_10.initialized);
276        assert_eq!(indicator_ama_10.value, 1522.0);
277    }
278}