wickra_core/indicators/
adaptive_cycle.rs1use crate::indicators::hilbert_dominant_cycle::HilbertDominantCycle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
30pub struct AdaptiveCycle {
31 cycle: HilbertDominantCycle,
32 last_value: Option<f64>,
33}
34
35impl AdaptiveCycle {
36 pub fn new() -> Self {
38 Self::default()
39 }
40
41 pub const fn value(&self) -> Option<f64> {
43 self.last_value
44 }
45}
46
47impl Indicator for AdaptiveCycle {
48 type Input = f64;
49 type Output = f64;
50
51 #[inline]
52 fn update(&mut self, input: f64) -> Option<f64> {
53 let period = self.cycle.update(input)?;
54 let half = (period * 0.5).round().clamp(3.0, 25.0);
55 self.last_value = Some(half);
56 Some(half)
57 }
58
59 fn reset(&mut self) {
60 self.cycle.reset();
61 self.last_value = None;
62 }
63
64 #[inline]
65 fn warmup_period(&self) -> usize {
66 self.cycle.warmup_period()
67 }
68
69 #[inline]
70 fn is_ready(&self) -> bool {
71 self.last_value.is_some()
72 }
73
74 #[inline]
75 fn name(&self) -> &'static str {
76 "AdaptiveCycle"
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::traits::BatchExt;
84
85 #[test]
86 fn accessors_and_metadata() {
87 let mut ac = AdaptiveCycle::new();
88 assert_eq!(ac.warmup_period(), 50);
89 assert_eq!(ac.name(), "AdaptiveCycle");
90 assert!(!ac.is_ready());
91 assert!(ac.value().is_none());
92 let prices: Vec<f64> = (0..120)
93 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
94 .collect();
95 ac.batch(&prices);
96 assert!(ac.is_ready());
97 assert!(ac.value().is_some());
98 }
99
100 #[test]
101 fn output_within_clamp_band() {
102 let prices: Vec<f64> = (0..200)
103 .map(|i| 100.0 + (f64::from(i) * 0.5).sin() * 5.0)
104 .collect();
105 let mut ac = AdaptiveCycle::new();
106 for v in ac.batch(&prices).into_iter().flatten() {
107 assert!((3.0..=25.0).contains(&v), "period {v} out of band");
108 assert_eq!(v, v.round(), "expected integer-valued output");
109 }
110 }
111
112 #[test]
113 fn batch_equals_streaming() {
114 let prices: Vec<f64> = (0..200)
115 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
116 .collect();
117 let mut a = AdaptiveCycle::new();
118 let mut b = AdaptiveCycle::new();
119 let batch = a.batch(&prices);
120 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
121 assert_eq!(batch, streamed);
122 }
123
124 #[test]
125 fn ignores_non_finite_input() {
126 let mut ac = AdaptiveCycle::new();
127 let prices: Vec<f64> = (0..120)
128 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
129 .collect();
130 ac.batch(&prices);
131 let before = ac.value();
132 assert!(before.is_some());
133 assert_eq!(ac.update(f64::NAN), None);
134 }
135
136 #[test]
137 fn reset_clears_state() {
138 let mut ac = AdaptiveCycle::new();
139 let prices: Vec<f64> = (0..120)
140 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 5.0)
141 .collect();
142 ac.batch(&prices);
143 assert!(ac.is_ready());
144 ac.reset();
145 assert!(!ac.is_ready());
146 }
147}