nautilus_indicators/average/
vidya.rs1use std::fmt::Display;
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)]
35#[cfg_attr(
36 feature = "python",
37 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
38)]
39pub struct VariableIndexDynamicAverage {
40 pub period: usize,
41 pub alpha: f64,
42 pub price_type: PriceType,
43 pub value: f64,
44 pub count: usize,
45 pub initialized: bool,
46 pub cmo: ChandeMomentumOscillator,
47 pub cmo_pct: f64,
48 has_inputs: bool,
49}
50
51impl Display for VariableIndexDynamicAverage {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 write!(f, "{}({})", self.name(), self.period)
54 }
55}
56
57impl Indicator for VariableIndexDynamicAverage {
58 fn name(&self) -> String {
59 stringify!(VariableIndexDynamicAverage).into()
60 }
61
62 fn has_inputs(&self) -> bool {
63 self.has_inputs
64 }
65
66 fn initialized(&self) -> bool {
67 self.initialized
68 }
69
70 fn handle_quote(&mut self, quote: &QuoteTick) {
71 self.update_raw(quote.extract_price(self.price_type).into());
72 }
73
74 fn handle_trade(&mut self, trade: &TradeTick) {
75 self.update_raw((&trade.price).into());
76 }
77
78 fn handle_bar(&mut self, bar: &Bar) {
79 self.update_raw((&bar.close).into());
80 }
81
82 fn reset(&mut self) {
83 self.value = 0.0;
84 self.count = 0;
85 self.cmo_pct = 0.0;
86 self.alpha = 2.0 / (self.period as f64 + 1.0);
87 self.has_inputs = false;
88 self.initialized = false;
89 self.cmo.reset();
90 }
91}
92
93impl VariableIndexDynamicAverage {
94 #[must_use]
100 pub fn new(
101 period: usize,
102 price_type: Option<PriceType>,
103 cmo_ma_type: Option<MovingAverageType>,
104 ) -> Self {
105 assert!(
106 period > 0,
107 "VariableIndexDynamicAverage: period must be > 0 (received {period})"
108 );
109
110 Self {
111 period,
112 price_type: price_type.unwrap_or(PriceType::Last),
113 value: 0.0,
114 count: 0,
115 has_inputs: false,
116 initialized: false,
117 alpha: 2.0 / (period as f64 + 1.0),
118 cmo: ChandeMomentumOscillator::new(period, cmo_ma_type),
119 cmo_pct: 0.0,
120 }
121 }
122}
123
124impl MovingAverage for VariableIndexDynamicAverage {
125 fn value(&self) -> f64 {
126 self.value
127 }
128
129 fn count(&self) -> usize {
130 self.count
131 }
132
133 fn update_raw(&mut self, price: f64) {
134 self.cmo.update_raw(price);
135 self.cmo_pct = (self.cmo.value / 100.0).abs();
136
137 if self.initialized {
138 self.value = (self.alpha * self.cmo_pct)
139 .mul_add(price, self.alpha.mul_add(-self.cmo_pct, 1.0) * self.value);
140 }
141
142 if !self.initialized && self.cmo.initialized {
143 self.initialized = true;
144 }
145 self.has_inputs = true;
146 self.count += 1;
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use nautilus_model::data::{Bar, QuoteTick, TradeTick};
153 use rstest::rstest;
154
155 use crate::{
156 average::{sma::SimpleMovingAverage, vidya::VariableIndexDynamicAverage},
157 indicator::{Indicator, MovingAverage},
158 stubs::*,
159 };
160
161 #[rstest]
162 fn test_vidya_initialized(indicator_vidya_10: VariableIndexDynamicAverage) {
163 let display_st = format!("{indicator_vidya_10}");
164 assert_eq!(display_st, "VariableIndexDynamicAverage(10)");
165 assert_eq!(indicator_vidya_10.period, 10);
166 assert!(!indicator_vidya_10.initialized());
167 assert!(!indicator_vidya_10.has_inputs());
168 }
169
170 #[rstest]
171 #[should_panic(expected = "period must be > 0")]
172 fn sma_new_with_zero_period_panics() {
173 let _ = VariableIndexDynamicAverage::new(0, None, None);
174 }
175
176 #[rstest]
177 fn test_initialized_with_required_input(mut indicator_vidya_10: VariableIndexDynamicAverage) {
178 for i in 1..10 {
179 indicator_vidya_10.update_raw(f64::from(i));
180 }
181 assert!(!indicator_vidya_10.initialized);
182 indicator_vidya_10.update_raw(10.0);
183 assert!(indicator_vidya_10.initialized);
184 }
185
186 #[rstest]
187 fn test_value_with_one_input(mut indicator_vidya_10: VariableIndexDynamicAverage) {
188 indicator_vidya_10.update_raw(1.0);
189 assert_eq!(indicator_vidya_10.value, 0.0);
190 }
191
192 #[rstest]
193 fn test_value_with_three_inputs(mut indicator_vidya_10: VariableIndexDynamicAverage) {
194 indicator_vidya_10.update_raw(1.0);
195 indicator_vidya_10.update_raw(2.0);
196 indicator_vidya_10.update_raw(3.0);
197 assert_eq!(indicator_vidya_10.value, 0.0);
198 }
199
200 #[rstest]
201 fn test_value_with_ten_inputs(mut indicator_vidya_10: VariableIndexDynamicAverage) {
202 indicator_vidya_10.update_raw(1.00000);
203 indicator_vidya_10.update_raw(1.00010);
204 indicator_vidya_10.update_raw(1.00020);
205 indicator_vidya_10.update_raw(1.00030);
206 indicator_vidya_10.update_raw(1.00040);
207 indicator_vidya_10.update_raw(1.00050);
208 indicator_vidya_10.update_raw(1.00040);
209 indicator_vidya_10.update_raw(1.00030);
210 indicator_vidya_10.update_raw(1.00020);
211 indicator_vidya_10.update_raw(1.00010);
212 indicator_vidya_10.update_raw(1.00000);
213 assert_eq!(indicator_vidya_10.value, 0.046_813_474_863_949_87);
214 }
215
216 #[rstest]
217 fn test_handle_quote_tick(
218 mut indicator_vidya_10: VariableIndexDynamicAverage,
219 stub_quote: QuoteTick,
220 ) {
221 indicator_vidya_10.handle_quote(&stub_quote);
222 assert_eq!(indicator_vidya_10.value, 0.0);
223 }
224
225 #[rstest]
226 fn test_handle_trade_tick(
227 mut indicator_vidya_10: VariableIndexDynamicAverage,
228 stub_trade: TradeTick,
229 ) {
230 indicator_vidya_10.handle_trade(&stub_trade);
231 assert_eq!(indicator_vidya_10.value, 0.0);
232 }
233
234 #[rstest]
235 fn test_handle_bar(
236 mut indicator_vidya_10: VariableIndexDynamicAverage,
237 bar_ethusdt_binance_minute_bid: Bar,
238 ) {
239 indicator_vidya_10.handle_bar(&bar_ethusdt_binance_minute_bid);
240 assert_eq!(indicator_vidya_10.value, 0.0);
241 assert!(!indicator_vidya_10.initialized);
242 }
243
244 #[rstest]
245 fn test_reset(mut indicator_vidya_10: VariableIndexDynamicAverage) {
246 indicator_vidya_10.update_raw(1.0);
247 assert_eq!(indicator_vidya_10.count, 1);
248 assert_eq!(indicator_vidya_10.value, 0.0);
249 indicator_vidya_10.reset();
250 assert_eq!(indicator_vidya_10.value, 0.0);
251 assert_eq!(indicator_vidya_10.count, 0);
252 assert!(!indicator_vidya_10.has_inputs);
253 assert!(!indicator_vidya_10.initialized);
254 }
255
256 fn reference_ma(prices: &[f64], period: usize) -> Vec<f64> {
257 let mut buf = Vec::with_capacity(period);
258 prices
259 .iter()
260 .map(|&p| {
261 buf.push(p);
262 if buf.len() > period {
263 buf.remove(0);
264 }
265 buf.iter().copied().sum::<f64>() / buf.len() as f64
266 })
267 .collect()
268 }
269
270 #[rstest]
271 #[case(3, vec![1.0, 2.0, 3.0, 4.0, 5.0])]
272 #[case(4, vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0])]
273 #[case(2, vec![0.1, 0.2, 0.3, 0.4])]
274 fn test_sma_exact_rolling_mean(#[case] period: usize, #[case] prices: Vec<f64>) {
275 let mut sma = SimpleMovingAverage::new(period, None);
276 let expected = reference_ma(&prices, period);
277
278 for (ix, (&price, &exp)) in prices.iter().zip(expected.iter()).enumerate() {
279 sma.update_raw(price);
280 assert_eq!(sma.count(), std::cmp::min(ix + 1, period));
281
282 let actual = sma.value();
283 assert!(
284 (actual - exp).abs() < 1e-12,
285 "tick {ix}: expected {exp}, was {actual}"
286 );
287 }
288 }
289
290 #[rstest]
291 fn test_sma_matches_reference_series() {
292 const PERIOD: usize = 5;
293
294 let prices: Vec<f64> = (1u32..=15)
295 .map(|n| f64::from(n * (n + 1) / 2) * 0.37)
296 .collect();
297
298 let reference = reference_ma(&prices, PERIOD);
299
300 let mut sma = SimpleMovingAverage::new(PERIOD, None);
301
302 for (ix, (&price, &exp)) in prices.iter().zip(reference.iter()).enumerate() {
303 sma.update_raw(price);
304
305 let actual = sma.value();
306 assert!(
307 (actual - exp).abs() < 1e-12,
308 "tick {ix}: expected {exp}, was {actual}"
309 );
310 }
311 }
312
313 #[rstest]
314 fn test_vidya_alpha_bounds() {
315 let vidya_min = VariableIndexDynamicAverage::new(1, None, None);
316 assert_eq!(vidya_min.alpha, 1.0);
317
318 let vidya_large = VariableIndexDynamicAverage::new(1_000, None, None);
319 assert!(vidya_large.alpha > 0.0 && vidya_large.alpha < 0.01);
320 }
321
322 #[rstest]
323 fn test_vidya_value_constant_when_cmo_zero() {
324 let mut vidya = VariableIndexDynamicAverage::new(3, None, None);
325
326 for _ in 0..10 {
327 vidya.update_raw(100.0);
328 }
329
330 let baseline = vidya.value;
331 for _ in 0..5 {
332 vidya.update_raw(100.0);
333 assert!((vidya.value - baseline).abs() < 1e-12);
334 }
335 }
336
337 #[rstest]
338 fn test_vidya_handles_negative_prices() {
339 let mut vidya = VariableIndexDynamicAverage::new(5, None, None);
340 let negative_prices = [-1.0, -1.2, -0.8, -1.5, -1.3, -1.1];
341
342 for p in negative_prices {
343 vidya.update_raw(p);
344 assert!(vidya.value.is_finite());
345 assert!((0.0..=1.0).contains(&vidya.cmo_pct));
346 }
347
348 assert!(vidya.value < 0.0);
349 }
350}