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 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 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 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()); assert!(indicator.is_ready());
162 let d1 = out[1].unwrap(); assert_relative_eq!(d1.on_zone, 1.0);
165 assert_relative_eq!(d1.bars_to_next, 1.0); let d4 = out[4].unwrap(); assert_relative_eq!(d4.on_zone, 0.0);
168 assert_relative_eq!(d4.bars_to_next, 1.0); let d5 = out[5].unwrap(); assert_relative_eq!(d5.on_zone, 1.0);
171 assert_relative_eq!(d5.bars_to_next, 3.0); }
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}