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    fn update(&mut self, candle: Candle) -> Option<FibTimeZonesOutput> {
94        self.swing.update(candle);
95        self.zones()
96    }
97
98    fn reset(&mut self) {
99        self.swing.reset();
100    }
101
102    fn warmup_period(&self) -> usize {
103        2
104    }
105
106    fn is_ready(&self) -> bool {
107        !self.swing.pivots().is_empty()
108    }
109
110    fn name(&self) -> &'static str {
111        "FibTimeZones"
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::traits::BatchExt;
119    use approx::assert_relative_eq;
120
121    fn c(high: f64, low: f64, ts: i64) -> Candle {
122        Candle::new(low, high, low, low, 1.0, ts).unwrap()
123    }
124
125    /// One pivot confirms at bar 0 (high @200, confirmed at bar 1); subsequent
126    /// flat bars neither extend nor confirm, so the anchor stays at bar 0 and the
127    /// distance equals the current bar index.
128    fn anchored_run() -> Vec<Candle> {
129        let mut bars = vec![c(200.0, 199.0, 0), c(190.0, 150.0, 1)];
130        for ts in 2..=5 {
131            bars.push(c(155.0, 151.0, ts));
132        }
133        bars
134    }
135
136    #[test]
137    fn accessors_and_metadata() {
138        let indicator = FibTimeZones::new();
139        assert_eq!(indicator.name(), "FibTimeZones");
140        assert_eq!(indicator.warmup_period(), 2);
141        assert!(!indicator.is_ready());
142        assert!(!FibTimeZones::default().is_ready());
143    }
144
145    #[test]
146    fn no_output_before_first_pivot() {
147        let mut indicator = FibTimeZones::new();
148        // The bootstrap bar confirms nothing.
149        assert!(indicator.update(c(200.0, 199.0, 0)).is_none());
150        assert!(!indicator.is_ready());
151    }
152
153    #[test]
154    fn flags_zones_and_counts_to_next() {
155        let mut indicator = FibTimeZones::new();
156        let out: Vec<_> = anchored_run()
157            .into_iter()
158            .map(|x| indicator.update(x))
159            .collect();
160        assert!(out[0].is_none()); // bootstrap, no pivot yet
161        assert!(indicator.is_ready());
162        // out[i] is reported at current bar i; anchor at bar 0 → distance = i.
163        let d1 = out[1].unwrap(); // distance 1 → a zone
164        assert_relative_eq!(d1.on_zone, 1.0);
165        assert_relative_eq!(d1.bars_to_next, 1.0); // next zone at 2
166        let d4 = out[4].unwrap(); // distance 4 → not a zone
167        assert_relative_eq!(d4.on_zone, 0.0);
168        assert_relative_eq!(d4.bars_to_next, 1.0); // next zone at 5
169        let d5 = out[5].unwrap(); // distance 5 → a zone
170        assert_relative_eq!(d5.on_zone, 1.0);
171        assert_relative_eq!(d5.bars_to_next, 3.0); // next zone at 8
172    }
173
174    #[test]
175    fn reset_clears_state() {
176        let mut indicator = FibTimeZones::new();
177        for candle in anchored_run() {
178            let _ = indicator.update(candle);
179        }
180        assert!(indicator.is_ready());
181        indicator.reset();
182        assert!(!indicator.is_ready());
183        assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
184    }
185
186    #[test]
187    fn batch_equals_streaming() {
188        let candles = anchored_run();
189        let mut a = FibTimeZones::new();
190        let mut b = FibTimeZones::new();
191        assert_eq!(
192            a.batch(&candles),
193            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
194        );
195    }
196}