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/// # Example
32///
33/// ```
34/// use wickra_core::{FibTimeZones, Candle, Indicator};
35///
36/// let mut indicator = FibTimeZones::new();
37/// // `None` during warmup, then `Some(_)` once enough bars are seen.
38/// let mut out = None;
39/// for i in 0..40i64 {
40///     let p = 100.0 + (i as f64 * 0.4).sin() * 5.0;
41///     let candle = Candle::new(p, p + 1.5, p - 1.5, p + 0.3, 1_000.0, i).unwrap();
42///     out = indicator.update(candle);
43/// }
44/// let _ = out;
45/// ```
46#[derive(Debug, Clone)]
47pub struct FibTimeZones {
48    swing: SwingTracker,
49}
50
51impl FibTimeZones {
52    /// Construct a new Fibonacci Time Zones tracker.
53    #[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        // Walk the time-zone sequence 1, 2, 3, 5, 8, … : `lo` advances through the
64        // members, `on_zone` records a hit, and the loop exits with `lo` holding
65        // the smallest member strictly greater than `distance`.
66        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    /// One pivot confirms at bar 0 (high @200, confirmed at bar 1); subsequent
130    /// flat bars neither extend nor confirm, so the anchor stays at bar 0 and the
131    /// distance equals the current bar index.
132    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        // The bootstrap bar confirms nothing.
153        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()); // bootstrap, no pivot yet
165        assert!(indicator.is_ready());
166        // out[i] is reported at current bar i; anchor at bar 0 → distance = i.
167        let d1 = out[1].unwrap(); // distance 1 → a zone
168        assert_relative_eq!(d1.on_zone, 1.0);
169        assert_relative_eq!(d1.bars_to_next, 1.0); // next zone at 2
170        let d4 = out[4].unwrap(); // distance 4 → not a zone
171        assert_relative_eq!(d4.on_zone, 0.0);
172        assert_relative_eq!(d4.bars_to_next, 1.0); // next zone at 5
173        let d5 = out[5].unwrap(); // distance 5 → a zone
174        assert_relative_eq!(d5.on_zone, 1.0);
175        assert_relative_eq!(d5.bars_to_next, 3.0); // next zone at 8
176    }
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}