wickra_core/indicators/
mcginley_dynamic.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
39pub struct McGinleyDynamic {
40 period: usize,
41 seed: VecDeque<f64>,
42 seed_sum: f64,
43 current: Option<f64>,
44}
45
46const K: f64 = 0.6;
48
49impl McGinleyDynamic {
50 pub fn new(period: usize) -> Result<Self> {
53 if period == 0 {
54 return Err(Error::PeriodZero);
55 }
56 Ok(Self {
57 period,
58 seed: VecDeque::with_capacity(period),
59 seed_sum: 0.0,
60 current: None,
61 })
62 }
63
64 pub const fn period(&self) -> usize {
66 self.period
67 }
68
69 pub const fn value(&self) -> Option<f64> {
71 self.current
72 }
73}
74
75impl Indicator for McGinleyDynamic {
76 type Input = f64;
77 type Output = f64;
78
79 fn update(&mut self, input: f64) -> Option<f64> {
80 if !input.is_finite() {
81 return self.current;
82 }
83 if let Some(prev) = self.current {
84 if prev <= 0.0 || input <= 0.0 {
88 return self.current;
89 }
90 let ratio = input / prev;
91 let divisor = K * (self.period as f64) * ratio.powi(4);
92 let next = prev + (input - prev) / divisor;
93 self.current = Some(next);
94 } else {
95 self.seed.push_back(input);
96 self.seed_sum += input;
97 if self.seed.len() == self.period {
98 self.current = Some(self.seed_sum / self.period as f64);
99 }
100 }
101 self.current
102 }
103
104 fn reset(&mut self) {
105 self.seed.clear();
106 self.seed_sum = 0.0;
107 self.current = None;
108 }
109
110 fn warmup_period(&self) -> usize {
111 self.period
112 }
113
114 fn is_ready(&self) -> bool {
115 self.current.is_some()
116 }
117
118 fn name(&self) -> &'static str {
119 "McGinleyDynamic"
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::traits::BatchExt;
127 use approx::assert_relative_eq;
128
129 #[test]
130 fn rejects_zero_period() {
131 assert!(matches!(McGinleyDynamic::new(0), Err(Error::PeriodZero)));
132 }
133
134 #[test]
135 fn accessors_and_metadata() {
136 let mut md = McGinleyDynamic::new(10).unwrap();
137 assert_eq!(md.period(), 10);
138 assert_eq!(md.warmup_period(), 10);
139 assert_eq!(md.name(), "McGinleyDynamic");
140 assert_eq!(md.value(), None);
141 for i in 1..=10 {
142 md.update(f64::from(i));
143 }
144 assert!(md.value().is_some());
145 }
146
147 #[test]
148 fn constant_series_yields_the_constant() {
149 let mut md = McGinleyDynamic::new(5).unwrap();
151 let out = md.batch(&[42.0_f64; 30]);
152 for v in out.iter().skip(4).flatten() {
153 assert_relative_eq!(*v, 42.0, epsilon = 1e-12);
154 }
155 }
156
157 #[test]
158 fn warmup_emits_first_value_at_period() {
159 let mut md = McGinleyDynamic::new(3).unwrap();
160 assert_eq!(md.update(10.0), None);
162 assert_eq!(md.update(20.0), None);
163 assert_eq!(md.update(30.0), Some(20.0));
164 }
165
166 #[test]
167 fn reference_value_recurrence() {
168 let mut md = McGinleyDynamic::new(3).unwrap();
173 md.batch(&[10.0_f64, 20.0, 30.0]);
174 let v = md.update(40.0).unwrap();
175 let expected = 20.0 + 20.0 / (0.6 * 3.0 * 16.0);
176 assert_relative_eq!(v, expected, epsilon = 1e-12);
177 }
178
179 #[test]
180 fn batch_equals_streaming() {
181 let prices: Vec<f64> = (1..=80)
182 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0)
183 .collect();
184 let mut a = McGinleyDynamic::new(10).unwrap();
185 let mut b = McGinleyDynamic::new(10).unwrap();
186 assert_eq!(
187 a.batch(&prices),
188 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
189 );
190 }
191
192 #[test]
193 fn reset_clears_state() {
194 let mut md = McGinleyDynamic::new(5).unwrap();
195 md.batch(&(1..=30).map(f64::from).collect::<Vec<_>>());
196 assert!(md.is_ready());
197 md.reset();
198 assert!(!md.is_ready());
199 assert_eq!(md.update(1.0), None);
200 }
201
202 #[test]
203 fn ignores_non_finite_input() {
204 let mut md = McGinleyDynamic::new(3).unwrap();
205 md.batch(&[10.0_f64, 20.0, 30.0]);
206 let before = md.value().unwrap();
207 assert_eq!(md.update(f64::NAN), Some(before));
208 assert_eq!(md.update(f64::INFINITY), Some(before));
209 }
210
211 #[test]
212 fn holds_value_when_input_is_non_positive() {
213 let mut md = McGinleyDynamic::new(3).unwrap();
216 md.batch(&[10.0_f64, 20.0, 30.0]);
217 let before = md.value().unwrap();
218 assert_eq!(md.update(0.0), Some(before));
219 assert_eq!(md.update(-5.0), Some(before));
220 let after = md.update(40.0).unwrap();
222 assert!(after > before);
223 }
224}