Skip to main content

wickra_core/indicators/
fib_extension.rs

1//! Fibonacci Extension 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 five canonical extension ratios, in ascending order. Each is a multiple
8/// of the swing leg measured from its origin, so `1.0` sits on the leg's end and
9/// every ratio here projects further in the direction of the move.
10const RATIOS: [f64; 5] = [1.272, 1.414, 1.618, 2.0, 2.618];
11
12/// Fibonacci Extension levels for the most recent swing leg.
13///
14/// Each field is the price reached if the move continues to the matching
15/// multiple of the leg, measured from the leg's start.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct FibExtensionOutput {
18    /// 127.2% extension.
19    pub level_1272: f64,
20    /// 141.4% extension.
21    pub level_1414: f64,
22    /// 161.8% extension — the "golden" extension.
23    pub level_1618: f64,
24    /// 200% extension.
25    pub level_2000: f64,
26    /// 261.8% extension.
27    pub level_2618: f64,
28}
29
30/// Fibonacci Extension (`FibExtension`).
31///
32/// Tracks confirmed swing pivots with a baked-in 5% reversal threshold and, once
33/// two pivots exist, projects the leg between them to the canonical extension
34/// ratios — the price targets a continuation of the move would reach.
35///
36/// Parameter-free; construction is infallible. Returns `None` until the first
37/// leg is complete.
38///
39/// See `crates/wickra-core/src/indicators/fib_extension.rs`.
40#[derive(Debug, Clone)]
41pub struct FibExtension {
42    swing: SwingTracker,
43}
44
45impl FibExtension {
46    /// Construct a new Fibonacci Extension tracker.
47    #[must_use]
48    pub const fn new() -> Self {
49        Self {
50            swing: SwingTracker::new(SWING_THRESHOLD, 2),
51        }
52    }
53
54    /// Extension price at ratio `e` for a leg from `start` to `end`: the total
55    /// move is `e` times the leg, measured from `start`.
56    fn level(start: f64, end: f64, e: f64) -> f64 {
57        start + e * (end - start)
58    }
59
60    fn levels(&self) -> Option<FibExtensionOutput> {
61        let pivots = self.swing.pivots();
62        let [start, end] = [pivots.first()?.price, pivots.get(1)?.price];
63        Some(FibExtensionOutput {
64            level_1272: Self::level(start, end, RATIOS[0]),
65            level_1414: Self::level(start, end, RATIOS[1]),
66            level_1618: Self::level(start, end, RATIOS[2]),
67            level_2000: Self::level(start, end, RATIOS[3]),
68            level_2618: Self::level(start, end, RATIOS[4]),
69        })
70    }
71}
72
73impl Default for FibExtension {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79impl Indicator for FibExtension {
80    type Input = Candle;
81    type Output = FibExtensionOutput;
82
83    fn update(&mut self, candle: Candle) -> Option<FibExtensionOutput> {
84        self.swing.update(candle);
85        self.levels()
86    }
87
88    fn reset(&mut self) {
89        self.swing.reset();
90    }
91
92    fn warmup_period(&self) -> usize {
93        2
94    }
95
96    fn is_ready(&self) -> bool {
97        self.swing.pivots().len() >= 2
98    }
99
100    fn name(&self) -> &'static str {
101        "FibExtension"
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::indicators::pattern_swing::candles_for_pivots;
109    use crate::traits::BatchExt;
110    use approx::assert_relative_eq;
111
112    #[test]
113    fn accessors_and_metadata() {
114        let indicator = FibExtension::new();
115        assert_eq!(indicator.name(), "FibExtension");
116        assert_eq!(indicator.warmup_period(), 2);
117        assert!(!indicator.is_ready());
118        assert!(!FibExtension::default().is_ready());
119    }
120
121    #[test]
122    fn no_output_before_two_pivots() {
123        let mut indicator = FibExtension::new();
124        let outputs: Vec<_> = candles_for_pivots(&[120.0])
125            .into_iter()
126            .map(|c| indicator.update(c))
127            .collect();
128        assert!(outputs.iter().all(Option::is_none));
129    }
130
131    #[test]
132    fn extension_levels_of_a_down_leg() {
133        // Leg start = 200 (high), end = 100 (low): a 100-point drop continued.
134        let mut indicator = FibExtension::new();
135        let mut last = None;
136        for candle in candles_for_pivots(&[200.0, 100.0]) {
137            last = indicator.update(candle);
138        }
139        let v = last.unwrap();
140        assert!(indicator.is_ready());
141        // 161.8% extension projects 1.618 * (-100) below the 200 origin.
142        assert_relative_eq!(v.level_1272, 200.0 - 127.2);
143        assert_relative_eq!(v.level_1414, 200.0 - 141.4);
144        assert_relative_eq!(v.level_1618, 200.0 - 161.8);
145        assert_relative_eq!(v.level_2000, 0.0);
146        assert_relative_eq!(v.level_2618, 200.0 - 261.8);
147    }
148
149    #[test]
150    fn reset_clears_state() {
151        let mut indicator = FibExtension::new();
152        for candle in candles_for_pivots(&[200.0, 100.0]) {
153            let _ = indicator.update(candle);
154        }
155        indicator.reset();
156        assert!(!indicator.is_ready());
157        let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
158        assert!(indicator.update(c).is_none());
159    }
160
161    #[test]
162    fn batch_equals_streaming() {
163        let candles = candles_for_pivots(&[200.0, 100.0, 150.0]);
164        let mut a = FibExtension::new();
165        let mut b = FibExtension::new();
166        assert_eq!(
167            a.batch(&candles),
168            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
169        );
170    }
171}