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 fn update(&mut self, section: CrossSection) -> Option<f64> {
102 Some(self.step(§ion))
103 }
104
105 fn reset(&mut self) {
106 self.ema_fast = 0.0;
107 self.ema_slow = 0.0;
108 self.seeded = false;
109 self.has_emitted = false;
110 }
111
112 fn warmup_period(&self) -> usize {
113 1
114 }
115
116 fn is_ready(&self) -> bool {
117 self.has_emitted
118 }
119
120 fn name(&self) -> &'static str {
121 "McClellanOscillator"
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::cross_section::Member;
129 use crate::traits::BatchExt;
130
131 fn section(up: usize, down: usize) -> CrossSection {
132 let mut members = Vec::new();
133 for _ in 0..up {
134 members.push(Member::new(1.0, 10.0, false, false));
135 }
136 for _ in 0..down {
137 members.push(Member::new(-1.0, 10.0, false, false));
138 }
139 members.push(Member::new(0.0, 10.0, false, false));
140 CrossSection::new(members, 0).unwrap()
141 }
142
143 #[test]
144 fn accessors_and_metadata() {
145 let osc = McClellanOscillator::new();
146 assert_eq!(osc.name(), "McClellanOscillator");
147 assert_eq!(osc.warmup_period(), 1);
148 assert!(!osc.is_ready());
149 }
150
151 #[test]
152 fn seeds_to_zero_on_first_tick() {
153 let mut osc = McClellanOscillator::new();
154 assert_eq!(osc.update(section(3, 1)), Some(0.0));
156 assert!(osc.is_ready());
157 }
158
159 #[test]
160 fn tracks_breadth_momentum_after_seeding() {
161 let mut osc = McClellanOscillator::new();
162 osc.update(section(3, 1)); let value = osc.update(section(1, 3)).unwrap();
166 assert!((value - (-50.0)).abs() < 1e-9);
167 let value = osc.update(section(2, 2)).unwrap();
169 assert!((value - (-67.5)).abs() < 1e-9);
170 }
171
172 #[test]
173 fn empty_participation_yields_zero_rana() {
174 let mut osc = McClellanOscillator::new();
175 assert_eq!(osc.update(section(0, 0)), Some(0.0));
177 }
178
179 #[test]
180 fn reset_clears_state() {
181 let mut osc = McClellanOscillator::new();
182 osc.update(section(3, 1));
183 osc.update(section(1, 3));
184 assert!(osc.is_ready());
185 osc.reset();
186 assert!(!osc.is_ready());
187 assert_eq!(osc.update(section(1, 3)), Some(0.0));
189 }
190
191 #[test]
192 fn batch_equals_streaming() {
193 let sections = vec![section(3, 1), section(1, 3), section(2, 2), section(0, 0)];
194 let mut a = McClellanOscillator::new();
195 let mut b = McClellanOscillator::new();
196 assert_eq!(
197 a.batch(§ions),
198 sections
199 .iter()
200 .map(|s| b.update(s.clone()))
201 .collect::<Vec<_>>()
202 );
203 }
204}