wickra_core/indicators/
fib_time_zones.rs1use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct FibTimeZonesOutput {
12 pub on_zone: f64,
15 pub bars_to_next: f64,
18}
19
20#[derive(Debug, Clone)]
47pub struct FibTimeZones {
48 swing: SwingTracker,
49}
50
51impl FibTimeZones {
52 #[must_use]
54 pub const fn new() -> Self {
55 Self {
56 swing: SwingTracker::new(SWING_THRESHOLD, 2),
57 }
58 }
59
60 fn zones(&self) -> Option<FibTimeZonesOutput> {
61 let anchor = self.swing.pivots().last()?;
62 let distance = self.swing.current_bar() - anchor.bar;
63 let (mut lo, mut hi) = (1usize, 2usize);
67 let mut on_zone = false;
68 while lo <= distance {
69 if lo == distance {
70 on_zone = true;
71 }
72 let next = lo + hi;
73 lo = hi;
74 hi = next;
75 }
76 Some(FibTimeZonesOutput {
77 on_zone: f64::from(u8::from(on_zone)),
78 bars_to_next: (lo - distance) as f64,
79 })
80 }
81}
82
83impl Default for FibTimeZones {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl Indicator for FibTimeZones {
90 type Input = Candle;
91 type Output = FibTimeZonesOutput;
92
93 #[inline]
94 fn update(&mut self, candle: Candle) -> Option<FibTimeZonesOutput> {
95 self.swing.update(candle);
96 self.zones()
97 }
98
99 fn reset(&mut self) {
100 self.swing.reset();
101 }
102
103 #[inline]
104 fn warmup_period(&self) -> usize {
105 2
106 }
107
108 #[inline]
109 fn is_ready(&self) -> bool {
110 !self.swing.pivots().is_empty()
111 }
112
113 #[inline]
114 fn name(&self) -> &'static str {
115 "FibTimeZones"
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::traits::BatchExt;
123 use approx::assert_relative_eq;
124
125 fn c(high: f64, low: f64, ts: i64) -> Candle {
126 Candle::new(low, high, low, low, 1.0, ts).unwrap()
127 }
128
129 fn anchored_run() -> Vec<Candle> {
133 let mut bars = vec![c(200.0, 199.0, 0), c(190.0, 150.0, 1)];
134 for ts in 2..=5 {
135 bars.push(c(155.0, 151.0, ts));
136 }
137 bars
138 }
139
140 #[test]
141 fn accessors_and_metadata() {
142 let indicator = FibTimeZones::new();
143 assert_eq!(indicator.name(), "FibTimeZones");
144 assert_eq!(indicator.warmup_period(), 2);
145 assert!(!indicator.is_ready());
146 assert!(!FibTimeZones::default().is_ready());
147 }
148
149 #[test]
150 fn no_output_before_first_pivot() {
151 let mut indicator = FibTimeZones::new();
152 assert!(indicator.update(c(200.0, 199.0, 0)).is_none());
154 assert!(!indicator.is_ready());
155 }
156
157 #[test]
158 fn flags_zones_and_counts_to_next() {
159 let mut indicator = FibTimeZones::new();
160 let out: Vec<_> = anchored_run()
161 .into_iter()
162 .map(|x| indicator.update(x))
163 .collect();
164 assert!(out[0].is_none()); assert!(indicator.is_ready());
166 let d1 = out[1].unwrap(); assert_relative_eq!(d1.on_zone, 1.0);
169 assert_relative_eq!(d1.bars_to_next, 1.0); let d4 = out[4].unwrap(); assert_relative_eq!(d4.on_zone, 0.0);
172 assert_relative_eq!(d4.bars_to_next, 1.0); let d5 = out[5].unwrap(); assert_relative_eq!(d5.on_zone, 1.0);
175 assert_relative_eq!(d5.bars_to_next, 3.0); }
177
178 #[test]
179 fn reset_clears_state() {
180 let mut indicator = FibTimeZones::new();
181 for candle in anchored_run() {
182 let _ = indicator.update(candle);
183 }
184 assert!(indicator.is_ready());
185 indicator.reset();
186 assert!(!indicator.is_ready());
187 assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
188 }
189
190 #[test]
191 fn batch_equals_streaming() {
192 let candles = anchored_run();
193 let mut a = FibTimeZones::new();
194 let mut b = FibTimeZones::new();
195 assert_eq!(
196 a.batch(&candles),
197 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
198 );
199 }
200}