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