Skip to main content

wickra_core/indicators/
fib_retracement.rs

1//! Fibonacci Retracement of the most recent confirmed swing leg.
2
3use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7/// The seven canonical retracement ratios, in ascending order. `0.0` marks the
8/// most recent swing extreme (the end of the leg) and `1.0` the swing origin
9/// (its start); the interior ratios are the classic Fibonacci pullbacks.
10const RATIOS: [f64; 7] = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0];
11
12/// Fibonacci Retracement levels for the most recent swing leg.
13///
14/// Each field is the price at the matching retracement ratio, measured from the
15/// leg's end (`level_0`, the latest confirmed extreme) back toward its start
16/// (`level_1000`, the prior pivot).
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct FibRetracementOutput {
19    /// 0.0% — the most recent confirmed swing extreme.
20    pub level_0: f64,
21    /// 23.6% retracement.
22    pub level_236: f64,
23    /// 38.2% retracement.
24    pub level_382: f64,
25    /// 50% retracement (not a Fibonacci ratio, but conventionally drawn).
26    pub level_500: f64,
27    /// 61.8% retracement — the "golden ratio" pullback.
28    pub level_618: f64,
29    /// 78.6% retracement.
30    pub level_786: f64,
31    /// 100% — the swing origin.
32    pub level_1000: f64,
33}
34
35/// Fibonacci Retracement (`FibRetracement`).
36///
37/// Tracks confirmed swing pivots with a baked-in 5% reversal threshold (the
38/// same non-repainting logic as [`crate::indicators::ZigZag`]) and, once two
39/// pivots exist, reports the seven retracement levels of the leg between them.
40///
41/// The levels are recomputed each time a new pivot confirms; between
42/// confirmations [`Indicator::update`] returns the locked levels of the current
43/// leg. Before the first leg is complete it returns `None`.
44///
45/// Parameter-free: the threshold is a compile-time constant, mirroring the
46/// chart- and harmonic-pattern detectors, so construction is infallible.
47///
48/// See `crates/wickra-core/src/indicators/fib_retracement.rs`.
49/// # Example
50///
51/// ```
52/// use wickra_core::{FibRetracement, Candle, Indicator};
53///
54/// let mut indicator = FibRetracement::new();
55/// // `None` during warmup, then `Some(_)` once enough bars are seen.
56/// let mut out = None;
57/// for i in 0..40i64 {
58///     let p = 100.0 + (i as f64 * 0.4).sin() * 5.0;
59///     let candle = Candle::new(p, p + 1.5, p - 1.5, p + 0.3, 1_000.0, i).unwrap();
60///     out = indicator.update(candle);
61/// }
62/// let _ = out;
63/// ```
64#[derive(Debug, Clone)]
65pub struct FibRetracement {
66    swing: SwingTracker,
67}
68
69impl FibRetracement {
70    /// Construct a new Fibonacci Retracement tracker.
71    #[must_use]
72    pub const fn new() -> Self {
73        Self {
74            swing: SwingTracker::new(SWING_THRESHOLD, 2),
75        }
76    }
77
78    /// Retracement price at ratio `r` for a leg from `start` to `end`: `0.0`
79    /// sits on `end`, `1.0` on `start`.
80    fn level(start: f64, end: f64, r: f64) -> f64 {
81        end + r * (start - end)
82    }
83
84    fn levels(&self) -> Option<FibRetracementOutput> {
85        let pivots = self.swing.pivots();
86        let [start, end] = [pivots.first()?.price, pivots.get(1)?.price];
87        Some(FibRetracementOutput {
88            level_0: Self::level(start, end, RATIOS[0]),
89            level_236: Self::level(start, end, RATIOS[1]),
90            level_382: Self::level(start, end, RATIOS[2]),
91            level_500: Self::level(start, end, RATIOS[3]),
92            level_618: Self::level(start, end, RATIOS[4]),
93            level_786: Self::level(start, end, RATIOS[5]),
94            level_1000: Self::level(start, end, RATIOS[6]),
95        })
96    }
97}
98
99impl Default for FibRetracement {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105impl Indicator for FibRetracement {
106    type Input = Candle;
107    type Output = FibRetracementOutput;
108
109    fn update(&mut self, candle: Candle) -> Option<FibRetracementOutput> {
110        self.swing.update(candle);
111        self.levels()
112    }
113
114    fn reset(&mut self) {
115        self.swing.reset();
116    }
117
118    fn warmup_period(&self) -> usize {
119        2
120    }
121
122    fn is_ready(&self) -> bool {
123        self.swing.pivots().len() >= 2
124    }
125
126    fn name(&self) -> &'static str {
127        "FibRetracement"
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::indicators::pattern_swing::candles_for_pivots;
135    use crate::traits::BatchExt;
136    use approx::assert_relative_eq;
137
138    #[test]
139    fn accessors_and_metadata() {
140        let indicator = FibRetracement::new();
141        assert_eq!(indicator.name(), "FibRetracement");
142        assert_eq!(indicator.warmup_period(), 2);
143        assert!(!indicator.is_ready());
144        assert!(!FibRetracement::default().is_ready());
145    }
146
147    #[test]
148    fn no_output_before_two_pivots() {
149        let mut indicator = FibRetracement::new();
150        // A single confirmed pivot is not enough to define a leg.
151        let candles = candles_for_pivots(&[120.0]);
152        let outputs: Vec<_> = candles.into_iter().map(|c| indicator.update(c)).collect();
153        assert!(outputs.iter().all(Option::is_none));
154        assert!(!indicator.is_ready());
155    }
156
157    #[test]
158    fn retracement_levels_of_a_down_leg() {
159        // Leg start = 200 (high), end = 100 (low): a 100-point drop.
160        let mut indicator = FibRetracement::new();
161        let mut last = None;
162        for candle in candles_for_pivots(&[200.0, 100.0]) {
163            last = indicator.update(candle);
164        }
165        let v = last.unwrap();
166        assert!(indicator.is_ready());
167        // 0% on the low (end), 100% on the high (start).
168        assert_relative_eq!(v.level_0, 100.0);
169        assert_relative_eq!(v.level_1000, 200.0);
170        // 61.8% retracement of a 100-point drop, measured up from the low.
171        assert_relative_eq!(v.level_618, 161.8);
172        assert_relative_eq!(v.level_500, 150.0);
173        assert_relative_eq!(v.level_382, 138.2);
174        assert_relative_eq!(v.level_236, 123.6);
175        assert_relative_eq!(v.level_786, 178.6);
176    }
177
178    #[test]
179    fn levels_refresh_on_a_new_leg() {
180        // Four pivots, cap = 2: once the third and fourth confirm, the reported
181        // leg shifts to the latest pair (130 high -> 90 low).
182        let mut indicator = FibRetracement::new();
183        let mut last = None;
184        for candle in candles_for_pivots(&[200.0, 100.0, 130.0, 90.0]) {
185            last = indicator.update(candle);
186        }
187        let v = last.unwrap();
188        assert_relative_eq!(v.level_0, 90.0);
189        assert_relative_eq!(v.level_1000, 130.0);
190        assert_relative_eq!(v.level_618, 90.0 + 0.618 * 40.0);
191    }
192
193    #[test]
194    fn reset_clears_state() {
195        let mut indicator = FibRetracement::new();
196        for candle in candles_for_pivots(&[200.0, 100.0]) {
197            let _ = indicator.update(candle);
198        }
199        assert!(indicator.is_ready());
200        indicator.reset();
201        assert!(!indicator.is_ready());
202        let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
203        assert!(indicator.update(c).is_none());
204    }
205
206    #[test]
207    fn batch_equals_streaming() {
208        let candles = candles_for_pivots(&[200.0, 100.0, 150.0]);
209        let mut a = FibRetracement::new();
210        let mut b = FibRetracement::new();
211        assert_eq!(
212            a.batch(&candles),
213            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
214        );
215    }
216}