Skip to main content

wickra_core/indicators/
fib_channel.rs

1//! Fibonacci Channel — a sloped base trendline plus parallel lines offset by
2//! Fibonacci multiples of the channel width.
3
4use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8/// The parallel-line ratios above the base (61.8% / 100% / 161.8% of the width).
9const RATIOS: [f64; 3] = [0.618, 1.0, 1.618];
10
11/// Fibonacci Channel line prices evaluated at the current bar.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct FibChannelOutput {
14    /// The base trendline price at the current bar.
15    pub base: f64,
16    /// Base + 61.8% of the channel width.
17    pub level_618: f64,
18    /// Base + 100% of the width — the opposite channel boundary.
19    pub level_1000: f64,
20    /// Base + 161.8% of the width.
21    pub level_1618: f64,
22}
23
24/// Fibonacci Channel (`FibChannel`).
25///
26/// From the last three confirmed pivots, the two same-direction outer pivots
27/// define a sloped base trendline and the opposite middle pivot sets the channel
28/// width (its signed distance from the base line). Parallel lines are then offset
29/// by Fibonacci multiples of that width and reported at the current bar.
30///
31/// ```text
32/// slope     = (p2 - p0) / (bar2 - bar0)
33/// base(bar) = p0 + slope * (bar - bar0)
34/// width     = p1 - base(bar1)
35/// level(r)  = base(cur) + r * width
36/// ```
37///
38/// Parameter-free; construction is infallible. Returns `None` until three pivots
39/// have confirmed.
40///
41/// See `crates/wickra-core/src/indicators/fib_channel.rs`.
42#[derive(Debug, Clone)]
43pub struct FibChannel {
44    swing: SwingTracker,
45}
46
47impl FibChannel {
48    /// Construct a new Fibonacci Channel tracker.
49    #[must_use]
50    pub const fn new() -> Self {
51        Self {
52            swing: SwingTracker::new(SWING_THRESHOLD, 3),
53        }
54    }
55
56    fn channel(&self) -> Option<FibChannelOutput> {
57        let pivots = self.swing.pivots();
58        let p0 = pivots.first()?;
59        let p1 = pivots.get(1)?;
60        let p2 = pivots.get(2)?;
61        // p0 and p2 are the same-direction outer pivots; their bars differ
62        // strictly, so the slope denominator is non-zero.
63        let slope = (p2.price - p0.price) / (p2.bar - p0.bar) as f64;
64        let base_at = |bar: usize| p0.price + slope * (bar - p0.bar) as f64;
65        let width = p1.price - base_at(p1.bar);
66        let base = base_at(self.swing.current_bar());
67        Some(FibChannelOutput {
68            base,
69            level_618: base + RATIOS[0] * width,
70            level_1000: base + RATIOS[1] * width,
71            level_1618: base + RATIOS[2] * width,
72        })
73    }
74}
75
76impl Default for FibChannel {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl Indicator for FibChannel {
83    type Input = Candle;
84    type Output = FibChannelOutput;
85
86    fn update(&mut self, candle: Candle) -> Option<FibChannelOutput> {
87        self.swing.update(candle);
88        self.channel()
89    }
90
91    fn reset(&mut self) {
92        self.swing.reset();
93    }
94
95    fn warmup_period(&self) -> usize {
96        3
97    }
98
99    fn is_ready(&self) -> bool {
100        self.swing.pivots().len() >= 3
101    }
102
103    fn name(&self) -> &'static str {
104        "FibChannel"
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::traits::BatchExt;
112    use approx::assert_relative_eq;
113
114    fn c(high: f64, low: f64, ts: i64) -> Candle {
115        Candle::new(low, high, low, low, 1.0, ts).unwrap()
116    }
117
118    /// Pivots: high 200 (bar 0), low 100 (bar 1), high 220 (bar 3); confirmed at
119    /// bar 4 so the channel is first reported at current bar 4.
120    fn three_pivots() -> Vec<Candle> {
121        vec![
122            c(200.0, 199.0, 0),
123            c(190.0, 100.0, 1), // confirm high @200, low candidate @100
124            c(110.0, 108.0, 2), // confirm low @100, high candidate @110
125            c(220.0, 210.0, 3), // extend high to 220 (bar 3)
126            c(200.0, 150.0, 4), // confirm high @220 -> three pivots
127        ]
128    }
129
130    #[test]
131    fn accessors_and_metadata() {
132        let indicator = FibChannel::new();
133        assert_eq!(indicator.name(), "FibChannel");
134        assert_eq!(indicator.warmup_period(), 3);
135        assert!(!indicator.is_ready());
136        assert!(!FibChannel::default().is_ready());
137    }
138
139    #[test]
140    fn no_output_before_three_pivots() {
141        let mut indicator = FibChannel::new();
142        let outputs: Vec<_> = [c(200.0, 199.0, 0), c(190.0, 100.0, 1), c(110.0, 108.0, 2)]
143            .into_iter()
144            .map(|x| indicator.update(x))
145            .collect();
146        // Only two pivots confirm within these three bars.
147        assert!(outputs.iter().all(Option::is_none));
148        assert!(!indicator.is_ready());
149    }
150
151    #[test]
152    fn channel_levels_from_three_pivots() {
153        let mut indicator = FibChannel::new();
154        let mut last = None;
155        for candle in three_pivots() {
156            last = indicator.update(candle);
157        }
158        let v = last.unwrap();
159        assert!(indicator.is_ready());
160        // Base through highs (0,200) and (3,220); width from low (1,100); cur = 4.
161        let slope = (220.0 - 200.0) / 3.0;
162        let base_cur = 200.0 + slope * 4.0;
163        let width = 100.0 - (200.0 + slope * 1.0);
164        assert_relative_eq!(v.base, base_cur);
165        assert_relative_eq!(v.level_1000, base_cur + width);
166        assert_relative_eq!(v.level_618, base_cur + 0.618 * width);
167        assert_relative_eq!(v.level_1618, base_cur + 1.618 * width);
168    }
169
170    #[test]
171    fn reset_clears_state() {
172        let mut indicator = FibChannel::new();
173        for candle in three_pivots() {
174            let _ = indicator.update(candle);
175        }
176        assert!(indicator.is_ready());
177        indicator.reset();
178        assert!(!indicator.is_ready());
179        assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
180    }
181
182    #[test]
183    fn batch_equals_streaming() {
184        let candles = three_pivots();
185        let mut a = FibChannel::new();
186        let mut b = FibChannel::new();
187        assert_eq!(
188            a.batch(&candles),
189            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
190        );
191    }
192}