Skip to main content

wickra_core/indicators/
fib_time_zones.rs

1//! Fibonacci Time Zones — vertical markers at Fibonacci bar-distances from the
2//! most recent swing pivot.
3
4use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8/// Where the current bar sits relative to the Fibonacci time-zone grid anchored
9/// on the most recent confirmed pivot.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct FibTimeZonesOutput {
12    /// `1.0` when the current bar lands on a Fibonacci time zone (a bar distance
13    /// of 1, 2, 3, 5, 8, 13, … from the anchor pivot), otherwise `0.0`.
14    pub on_zone: f64,
15    /// Number of bars until the next Fibonacci time zone (`0` is never returned —
16    /// when on a zone this is the gap to the following one).
17    pub bars_to_next: f64,
18}
19
20/// Fibonacci Time Zones (`FibTimeZones`).
21///
22/// Anchored on the most recent confirmed swing pivot, the Fibonacci sequence
23/// `1, 2, 3, 5, 8, 13, …` marks bars at which trend changes are classically
24/// anticipated. Reports whether the current bar is on a zone and how many bars
25/// remain until the next one.
26///
27/// Parameter-free; construction is infallible. Returns `None` until the first
28/// pivot has confirmed.
29///
30/// See `crates/wickra-core/src/indicators/fib_time_zones.rs`.
31#[derive(Debug, Clone)]
32pub struct FibTimeZones {
33    swing: SwingTracker,
34}
35
36impl FibTimeZones {
37    /// Construct a new Fibonacci Time Zones tracker.
38    #[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        // Walk the time-zone sequence 1, 2, 3, 5, 8, … : `lo` advances through the
49        // members, `on_zone` records a hit, and the loop exits with `lo` holding
50        // the smallest member strictly greater than `distance`.
51        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    /// One pivot confirms at bar 0 (high @200, confirmed at bar 1); subsequent
111    /// flat bars neither extend nor confirm, so the anchor stays at bar 0 and the
112    /// distance equals the current bar index.
113    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        // The bootstrap bar confirms nothing.
134        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()); // bootstrap, no pivot yet
146        assert!(indicator.is_ready());
147        // out[i] is reported at current bar i; anchor at bar 0 → distance = i.
148        let d1 = out[1].unwrap(); // distance 1 → a zone
149        assert_relative_eq!(d1.on_zone, 1.0);
150        assert_relative_eq!(d1.bars_to_next, 1.0); // next zone at 2
151        let d4 = out[4].unwrap(); // distance 4 → not a zone
152        assert_relative_eq!(d4.on_zone, 0.0);
153        assert_relative_eq!(d4.bars_to_next, 1.0); // next zone at 5
154        let d5 = out[5].unwrap(); // distance 5 → a zone
155        assert_relative_eq!(d5.on_zone, 1.0);
156        assert_relative_eq!(d5.bars_to_next, 3.0); // next zone at 8
157    }
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}