nautilus_indicators/average/
vidya.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, Formatter};
17
18use nautilus_model::{
19    data::{Bar, QuoteTick, TradeTick},
20    enums::PriceType,
21};
22
23use crate::{
24    average::MovingAverageType,
25    indicator::{Indicator, MovingAverage},
26    momentum::cmo::ChandeMomentumOscillator,
27};
28
29#[repr(C)]
30#[derive(Debug)]
31#[cfg_attr(
32    feature = "python",
33    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators", unsendable)
34)]
35pub struct VariableIndexDynamicAverage {
36    pub period: usize,
37    pub alpha: f64,
38    pub price_type: PriceType,
39    pub value: f64,
40    pub count: usize,
41    pub initialized: bool,
42    has_inputs: bool,
43    pub cmo: ChandeMomentumOscillator,
44    pub cmo_pct: f64,
45}
46
47impl Display for VariableIndexDynamicAverage {
48    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
49        write!(f, "{}({})", self.name(), self.period)
50    }
51}
52
53impl Indicator for VariableIndexDynamicAverage {
54    fn name(&self) -> String {
55        stringify!(VariableIndexDynamicAverage).to_string()
56    }
57
58    fn has_inputs(&self) -> bool {
59        self.has_inputs
60    }
61
62    fn initialized(&self) -> bool {
63        self.initialized
64    }
65
66    fn handle_quote(&mut self, quote: &QuoteTick) {
67        self.update_raw(quote.extract_price(self.price_type).into());
68    }
69
70    fn handle_trade(&mut self, trade: &TradeTick) {
71        self.update_raw((&trade.price).into());
72    }
73
74    fn handle_bar(&mut self, bar: &Bar) {
75        self.update_raw((&bar.close).into());
76    }
77
78    fn reset(&mut self) {
79        self.value = 0.0;
80        self.count = 0;
81        self.cmo_pct = 0.0;
82        self.alpha = 0.0;
83        self.has_inputs = false;
84        self.initialized = false;
85    }
86}
87
88impl VariableIndexDynamicAverage {
89    /// Creates a new [`VariableIndexDynamicAverage`] instance.
90    #[must_use]
91    pub fn new(
92        period: usize,
93        price_type: Option<PriceType>,
94        cmo_ma_type: Option<MovingAverageType>,
95    ) -> Self {
96        Self {
97            period,
98            price_type: price_type.unwrap_or(PriceType::Last),
99            value: 0.0,
100            count: 0,
101            has_inputs: false,
102            initialized: false,
103            alpha: 2.0 / (period as f64 + 1.0),
104            cmo: ChandeMomentumOscillator::new(period, cmo_ma_type),
105            cmo_pct: 0.0,
106        }
107    }
108}
109
110impl MovingAverage for VariableIndexDynamicAverage {
111    fn value(&self) -> f64 {
112        self.value
113    }
114
115    fn count(&self) -> usize {
116        self.count
117    }
118
119    fn update_raw(&mut self, value: f64) {
120        self.cmo.update_raw(value);
121        self.cmo_pct = (self.cmo.value / 100.0).abs();
122
123        if self.initialized {
124            self.value = (self.alpha * self.cmo_pct)
125                .mul_add(value, self.alpha.mul_add(-self.cmo_pct, 1.0) * self.value);
126        }
127
128        if !self.initialized && self.cmo.initialized {
129            self.initialized = true;
130        }
131
132        self.count += 1;
133    }
134}
135
136////////////////////////////////////////////////////////////////////////////////
137// Tests
138////////////////////////////////////////////////////////////////////////////////
139#[cfg(test)]
140mod tests {
141    use nautilus_model::data::{Bar, QuoteTick, TradeTick};
142    use rstest::rstest;
143
144    use crate::{
145        average::vidya::VariableIndexDynamicAverage,
146        indicator::{Indicator, MovingAverage},
147        stubs::*,
148    };
149
150    #[rstest]
151    fn test_vidya_initialized(indicator_vidya_10: VariableIndexDynamicAverage) {
152        let display_st = format!("{indicator_vidya_10}");
153        assert_eq!(display_st, "VariableIndexDynamicAverage(10)");
154        assert_eq!(indicator_vidya_10.period, 10);
155        assert!(!indicator_vidya_10.initialized());
156        assert!(!indicator_vidya_10.has_inputs());
157    }
158
159    #[rstest]
160    fn test_initialized_with_required_input(mut indicator_vidya_10: VariableIndexDynamicAverage) {
161        for i in 1..10 {
162            indicator_vidya_10.update_raw(f64::from(i));
163        }
164        assert!(!indicator_vidya_10.initialized);
165        indicator_vidya_10.update_raw(10.0);
166        assert!(indicator_vidya_10.initialized);
167    }
168
169    #[rstest]
170    fn test_value_with_one_input(mut indicator_vidya_10: VariableIndexDynamicAverage) {
171        indicator_vidya_10.update_raw(1.0);
172        assert_eq!(indicator_vidya_10.value, 0.0);
173    }
174
175    #[rstest]
176    fn test_value_with_three_inputs(mut indicator_vidya_10: VariableIndexDynamicAverage) {
177        indicator_vidya_10.update_raw(1.0);
178        indicator_vidya_10.update_raw(2.0);
179        indicator_vidya_10.update_raw(3.0);
180        assert_eq!(indicator_vidya_10.value, 0.0);
181    }
182
183    #[rstest]
184    fn test_value_with_ten_inputs(mut indicator_vidya_10: VariableIndexDynamicAverage) {
185        indicator_vidya_10.update_raw(1.00000);
186        indicator_vidya_10.update_raw(1.00010);
187        indicator_vidya_10.update_raw(1.00020);
188        indicator_vidya_10.update_raw(1.00030);
189        indicator_vidya_10.update_raw(1.00040);
190        indicator_vidya_10.update_raw(1.00050);
191        indicator_vidya_10.update_raw(1.00040);
192        indicator_vidya_10.update_raw(1.00030);
193        indicator_vidya_10.update_raw(1.00020);
194        indicator_vidya_10.update_raw(1.00010);
195        indicator_vidya_10.update_raw(1.00000);
196        assert_eq!(indicator_vidya_10.value, 0.046_813_474_863_949_87);
197    }
198
199    #[rstest]
200    fn test_handle_quote_tick(
201        mut indicator_vidya_10: VariableIndexDynamicAverage,
202        stub_quote: QuoteTick,
203    ) {
204        indicator_vidya_10.handle_quote(&stub_quote);
205        assert_eq!(indicator_vidya_10.value, 0.0);
206    }
207
208    #[rstest]
209    fn test_handle_trade_tick(
210        mut indicator_vidya_10: VariableIndexDynamicAverage,
211        stub_trade: TradeTick,
212    ) {
213        indicator_vidya_10.handle_trade(&stub_trade);
214        assert_eq!(indicator_vidya_10.value, 0.0);
215    }
216
217    #[rstest]
218    fn test_handle_bar(
219        mut indicator_vidya_10: VariableIndexDynamicAverage,
220        bar_ethusdt_binance_minute_bid: Bar,
221    ) {
222        indicator_vidya_10.handle_bar(&bar_ethusdt_binance_minute_bid);
223        assert_eq!(indicator_vidya_10.value, 0.0);
224        assert!(!indicator_vidya_10.initialized);
225    }
226
227    #[rstest]
228    fn test_reset(mut indicator_vidya_10: VariableIndexDynamicAverage) {
229        indicator_vidya_10.update_raw(1.0);
230        assert_eq!(indicator_vidya_10.count, 1);
231        assert_eq!(indicator_vidya_10.value, 0.0);
232        indicator_vidya_10.reset();
233        assert_eq!(indicator_vidya_10.value, 0.0);
234        assert_eq!(indicator_vidya_10.count, 0);
235        assert!(!indicator_vidya_10.has_inputs);
236        assert!(!indicator_vidya_10.initialized);
237    }
238}