wickra_core/indicators/
mcclellan_oscillator.rs1use crate::cross_section::CrossSection;
4use crate::traits::Indicator;
5
6const ALPHA_FAST: f64 = 0.1;
9const ALPHA_SLOW: f64 = 0.05;
12const RANA_SCALE: f64 = 1000.0;
15
16#[derive(Debug, Clone, Default)]
54pub struct McClellanOscillator {
55 ema_fast: f64,
56 ema_slow: f64,
57 seeded: bool,
58 has_emitted: bool,
59}
60
61impl McClellanOscillator {
62 #[must_use]
64 pub const fn new() -> Self {
65 Self {
66 ema_fast: 0.0,
67 ema_slow: 0.0,
68 seeded: false,
69 has_emitted: false,
70 }
71 }
72
73 pub(crate) fn step(&mut self, section: &CrossSection) -> f64 {
79 let advancers = section.advancers();
80 let decliners = section.decliners();
81 let net = advancers as f64 - decliners as f64;
82 let participating = (advancers + decliners).max(1) as f64;
83 let rana = net / participating * RANA_SCALE;
84 if self.seeded {
85 self.ema_fast += ALPHA_FAST * (rana - self.ema_fast);
86 self.ema_slow += ALPHA_SLOW * (rana - self.ema_slow);
87 } else {
88 self.ema_fast = rana;
89 self.ema_slow = rana;
90 self.seeded = true;
91 }
92 self.has_emitted = true;
93 self.ema_fast - self.ema_slow
94 }
95}
96
97impl Indicator for McClellanOscillator {
98 type Input = CrossSection;
99 type Output = f64;
100
101 #[inline]
102 fn update(&mut self, section: CrossSection) -> Option<f64> {
103 Some(self.step(§ion))
104 }
105
106 fn reset(&mut self) {
107 self.ema_fast = 0.0;
108 self.ema_slow = 0.0;
109 self.seeded = false;
110 self.has_emitted = false;
111 }
112
113 #[inline]
114 fn warmup_period(&self) -> usize {
115 1
116 }
117
118 #[inline]
119 fn is_ready(&self) -> bool {
120 self.has_emitted
121 }
122
123 #[inline]
124 fn name(&self) -> &'static str {
125 "McClellanOscillator"
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::cross_section::Member;
133 use crate::traits::BatchExt;
134
135 fn section(up: usize, down: usize) -> CrossSection {
136 let mut members = Vec::new();
137 for _ in 0..up {
138 members.push(Member::new(1.0, 10.0, false, false));
139 }
140 for _ in 0..down {
141 members.push(Member::new(-1.0, 10.0, false, false));
142 }
143 members.push(Member::new(0.0, 10.0, false, false));
144 CrossSection::new(members, 0).unwrap()
145 }
146
147 #[test]
148 fn accessors_and_metadata() {
149 let osc = McClellanOscillator::new();
150 assert_eq!(osc.name(), "McClellanOscillator");
151 assert_eq!(osc.warmup_period(), 1);
152 assert!(!osc.is_ready());
153 }
154
155 #[test]
156 fn seeds_to_zero_on_first_tick() {
157 let mut osc = McClellanOscillator::new();
158 assert_eq!(osc.update(section(3, 1)), Some(0.0));
160 assert!(osc.is_ready());
161 }
162
163 #[test]
164 fn tracks_breadth_momentum_after_seeding() {
165 let mut osc = McClellanOscillator::new();
166 osc.update(section(3, 1)); let value = osc.update(section(1, 3)).unwrap();
170 assert!((value - (-50.0)).abs() < 1e-9);
171 let value = osc.update(section(2, 2)).unwrap();
173 assert!((value - (-67.5)).abs() < 1e-9);
174 }
175
176 #[test]
177 fn empty_participation_yields_zero_rana() {
178 let mut osc = McClellanOscillator::new();
179 assert_eq!(osc.update(section(0, 0)), Some(0.0));
181 }
182
183 #[test]
184 fn reset_clears_state() {
185 let mut osc = McClellanOscillator::new();
186 osc.update(section(3, 1));
187 osc.update(section(1, 3));
188 assert!(osc.is_ready());
189 osc.reset();
190 assert!(!osc.is_ready());
191 assert_eq!(osc.update(section(1, 3)), Some(0.0));
193 }
194
195 #[test]
196 fn batch_equals_streaming() {
197 let sections = vec![section(3, 1), section(1, 3), section(2, 2), section(0, 0)];
198 let mut a = McClellanOscillator::new();
199 let mut b = McClellanOscillator::new();
200 assert_eq!(
201 a.batch(§ions),
202 sections
203 .iter()
204 .map(|s| b.update(s.clone()))
205 .collect::<Vec<_>>()
206 );
207 }
208}