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)]
32pub struct FibTimeZones {
33 swing: SwingTracker,
34}
35
36impl FibTimeZones {
37 #[must_use]
39 pub const fn new() -> Self {
40 Self {
41 swing: SwingTracker::new(SWING_THRESHOLD, 2),
42 }
43 }
44
45 fn zones(&self) -> Option<FibTimeZonesOutput> {
46 let anchor = self.swing.pivots().last()?;
47 let distance = self.swing.current_bar() - anchor.bar;
48 let (mut lo, mut hi) = (1usize, 2usize);
52 let mut on_zone = false;
53 while lo <= distance {
54 if lo == distance {
55 on_zone = true;
56 }
57 let next = lo + hi;
58 lo = hi;
59 hi = next;
60 }
61 Some(FibTimeZonesOutput {
62 on_zone: f64::from(u8::from(on_zone)),
63 bars_to_next: (lo - distance) as f64,
64 })
65 }
66}
67
68impl Default for FibTimeZones {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74impl Indicator for FibTimeZones {
75 type Input = Candle;
76 type Output = FibTimeZonesOutput;
77
78 fn update(&mut self, candle: Candle) -> Option<FibTimeZonesOutput> {
79 self.swing.update(candle);
80 self.zones()
81 }
82
83 fn reset(&mut self) {
84 self.swing.reset();
85 }
86
87 fn warmup_period(&self) -> usize {
88 2
89 }
90
91 fn is_ready(&self) -> bool {
92 !self.swing.pivots().is_empty()
93 }
94
95 fn name(&self) -> &'static str {
96 "FibTimeZones"
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::traits::BatchExt;
104 use approx::assert_relative_eq;
105
106 fn c(high: f64, low: f64, ts: i64) -> Candle {
107 Candle::new(low, high, low, low, 1.0, ts).unwrap()
108 }
109
110 fn anchored_run() -> Vec<Candle> {
114 let mut bars = vec![c(200.0, 199.0, 0), c(190.0, 150.0, 1)];
115 for ts in 2..=5 {
116 bars.push(c(155.0, 151.0, ts));
117 }
118 bars
119 }
120
121 #[test]
122 fn accessors_and_metadata() {
123 let indicator = FibTimeZones::new();
124 assert_eq!(indicator.name(), "FibTimeZones");
125 assert_eq!(indicator.warmup_period(), 2);
126 assert!(!indicator.is_ready());
127 assert!(!FibTimeZones::default().is_ready());
128 }
129
130 #[test]
131 fn no_output_before_first_pivot() {
132 let mut indicator = FibTimeZones::new();
133 assert!(indicator.update(c(200.0, 199.0, 0)).is_none());
135 assert!(!indicator.is_ready());
136 }
137
138 #[test]
139 fn flags_zones_and_counts_to_next() {
140 let mut indicator = FibTimeZones::new();
141 let out: Vec<_> = anchored_run()
142 .into_iter()
143 .map(|x| indicator.update(x))
144 .collect();
145 assert!(out[0].is_none()); assert!(indicator.is_ready());
147 let d1 = out[1].unwrap(); assert_relative_eq!(d1.on_zone, 1.0);
150 assert_relative_eq!(d1.bars_to_next, 1.0); let d4 = out[4].unwrap(); assert_relative_eq!(d4.on_zone, 0.0);
153 assert_relative_eq!(d4.bars_to_next, 1.0); let d5 = out[5].unwrap(); assert_relative_eq!(d5.on_zone, 1.0);
156 assert_relative_eq!(d5.bars_to_next, 3.0); }
158
159 #[test]
160 fn reset_clears_state() {
161 let mut indicator = FibTimeZones::new();
162 for candle in anchored_run() {
163 let _ = indicator.update(candle);
164 }
165 assert!(indicator.is_ready());
166 indicator.reset();
167 assert!(!indicator.is_ready());
168 assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
169 }
170
171 #[test]
172 fn batch_equals_streaming() {
173 let candles = anchored_run();
174 let mut a = FibTimeZones::new();
175 let mut b = FibTimeZones::new();
176 assert_eq!(
177 a.batch(&candles),
178 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
179 );
180 }
181}