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/// # Example
41///
42/// ```
43/// use wickra_core::{FibExtension, Candle, Indicator};
44///
45/// let mut indicator = FibExtension::new();
46/// // `None` during warmup, then `Some(_)` once enough bars are seen.
47/// let mut out = None;
48/// for i in 0..40i64 {
49///     let p = 100.0 + (i as f64 * 0.4).sin() * 5.0;
50///     let candle = Candle::new(p, p + 1.5, p - 1.5, p + 0.3, 1_000.0, i).unwrap();
51///     out = indicator.update(candle);
52/// }
53/// let _ = out;
54/// ```
55#[derive(Debug, Clone)]
56pub struct FibExtension {
57    swing: SwingTracker,
58}
59
60impl FibExtension {
61    /// Construct a new Fibonacci Extension tracker.
62    #[must_use]
63    pub const fn new() -> Self {
64        Self {
65            swing: SwingTracker::new(SWING_THRESHOLD, 2),
66        }
67    }
68
69    /// Extension price at ratio `e` for a leg from `start` to `end`: the total
70    /// move is `e` times the leg, measured from `start`.
71    fn level(start: f64, end: f64, e: f64) -> f64 {
72        start + e * (end - start)
73    }
74
75    fn levels(&self) -> Option<FibExtensionOutput> {
76        let pivots = self.swing.pivots();
77        let [start, end] = [pivots.first()?.price, pivots.get(1)?.price];
78        Some(FibExtensionOutput {
79            level_1272: Self::level(start, end, RATIOS[0]),
80            level_1414: Self::level(start, end, RATIOS[1]),
81            level_1618: Self::level(start, end, RATIOS[2]),
82            level_2000: Self::level(start, end, RATIOS[3]),
83            level_2618: Self::level(start, end, RATIOS[4]),
84        })
85    }
86}
87
88impl Default for FibExtension {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94impl Indicator for FibExtension {
95    type Input = Candle;
96    type Output = FibExtensionOutput;
97
98    fn update(&mut self, candle: Candle) -> Option<FibExtensionOutput> {
99        self.swing.update(candle);
100        self.levels()
101    }
102
103    fn reset(&mut self) {
104        self.swing.reset();
105    }
106
107    fn warmup_period(&self) -> usize {
108        2
109    }
110
111    fn is_ready(&self) -> bool {
112        self.swing.pivots().len() >= 2
113    }
114
115    fn name(&self) -> &'static str {
116        "FibExtension"
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::indicators::pattern_swing::candles_for_pivots;
124    use crate::traits::BatchExt;
125    use approx::assert_relative_eq;
126
127    #[test]
128    fn accessors_and_metadata() {
129        let indicator = FibExtension::new();
130        assert_eq!(indicator.name(), "FibExtension");
131        assert_eq!(indicator.warmup_period(), 2);
132        assert!(!indicator.is_ready());
133        assert!(!FibExtension::default().is_ready());
134    }
135
136    #[test]
137    fn no_output_before_two_pivots() {
138        let mut indicator = FibExtension::new();
139        let outputs: Vec<_> = candles_for_pivots(&[120.0])
140            .into_iter()
141            .map(|c| indicator.update(c))
142            .collect();
143        assert!(outputs.iter().all(Option::is_none));
144    }
145
146    #[test]
147    fn extension_levels_of_a_down_leg() {
148        // Leg start = 200 (high), end = 100 (low): a 100-point drop continued.
149        let mut indicator = FibExtension::new();
150        let mut last = None;
151        for candle in candles_for_pivots(&[200.0, 100.0]) {
152            last = indicator.update(candle);
153        }
154        let v = last.unwrap();
155        assert!(indicator.is_ready());
156        // 161.8% extension projects 1.618 * (-100) below the 200 origin.
157        assert_relative_eq!(v.level_1272, 200.0 - 127.2);
158        assert_relative_eq!(v.level_1414, 200.0 - 141.4);
159        assert_relative_eq!(v.level_1618, 200.0 - 161.8);
160        assert_relative_eq!(v.level_2000, 0.0);
161        assert_relative_eq!(v.level_2618, 200.0 - 261.8);
162    }
163
164    #[test]
165    fn reset_clears_state() {
166        let mut indicator = FibExtension::new();
167        for candle in candles_for_pivots(&[200.0, 100.0]) {
168            let _ = indicator.update(candle);
169        }
170        indicator.reset();
171        assert!(!indicator.is_ready());
172        let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
173        assert!(indicator.update(c).is_none());
174    }
175
176    #[test]
177    fn batch_equals_streaming() {
178        let candles = candles_for_pivots(&[200.0, 100.0, 150.0]);
179        let mut a = FibExtension::new();
180        let mut b = FibExtension::new();
181        assert_eq!(
182            a.batch(&candles),
183            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
184        );
185    }
186}