Skip to main content

wickra_core/indicators/
fib_projection.rs

1//! Fibonacci Projection — a measured move from the last three swing pivots.
2
3use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7/// The four canonical projection ratios, in ascending order. Each scales the
8/// A→B leg and projects it from C; `1.0` is the classic AB=CD measured move.
9const RATIOS: [f64; 4] = [0.618, 1.0, 1.618, 2.618];
10
11/// Fibonacci Projection levels (the C→D target zone of a measured move).
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct FibProjectionOutput {
14    /// 61.8% projection of the A→B leg from C.
15    pub level_618: f64,
16    /// 100% projection — the AB=CD measured move.
17    pub level_1000: f64,
18    /// 161.8% projection.
19    pub level_1618: f64,
20    /// 261.8% projection.
21    pub level_2618: f64,
22}
23
24/// Fibonacci Projection (`FibProjection`).
25///
26/// Reads the last three confirmed swing pivots as the points A, B and C of a
27/// measured move and projects the A→B leg from C at the canonical ratios — the
28/// price targets for the C→D leg.
29///
30/// Parameter-free; construction is infallible. Returns `None` until three
31/// pivots have confirmed.
32///
33/// See `crates/wickra-core/src/indicators/fib_projection.rs`.
34#[derive(Debug, Clone)]
35pub struct FibProjection {
36    swing: SwingTracker,
37}
38
39impl FibProjection {
40    /// Construct a new Fibonacci Projection tracker.
41    #[must_use]
42    pub const fn new() -> Self {
43        Self {
44            swing: SwingTracker::new(SWING_THRESHOLD, 3),
45        }
46    }
47
48    fn levels(&self) -> Option<FibProjectionOutput> {
49        let pivots = self.swing.pivots();
50        let [a, b, c] = [
51            pivots.first()?.price,
52            pivots.get(1)?.price,
53            pivots.get(2)?.price,
54        ];
55        let project = |p: f64| c + p * (b - a);
56        Some(FibProjectionOutput {
57            level_618: project(RATIOS[0]),
58            level_1000: project(RATIOS[1]),
59            level_1618: project(RATIOS[2]),
60            level_2618: project(RATIOS[3]),
61        })
62    }
63}
64
65impl Default for FibProjection {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl Indicator for FibProjection {
72    type Input = Candle;
73    type Output = FibProjectionOutput;
74
75    fn update(&mut self, candle: Candle) -> Option<FibProjectionOutput> {
76        self.swing.update(candle);
77        self.levels()
78    }
79
80    fn reset(&mut self) {
81        self.swing.reset();
82    }
83
84    fn warmup_period(&self) -> usize {
85        3
86    }
87
88    fn is_ready(&self) -> bool {
89        self.swing.pivots().len() >= 3
90    }
91
92    fn name(&self) -> &'static str {
93        "FibProjection"
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::indicators::pattern_swing::candles_for_pivots;
101    use crate::traits::BatchExt;
102    use approx::assert_relative_eq;
103
104    #[test]
105    fn accessors_and_metadata() {
106        let indicator = FibProjection::new();
107        assert_eq!(indicator.name(), "FibProjection");
108        assert_eq!(indicator.warmup_period(), 3);
109        assert!(!indicator.is_ready());
110        assert!(!FibProjection::default().is_ready());
111    }
112
113    #[test]
114    fn no_output_before_three_pivots() {
115        let mut indicator = FibProjection::new();
116        let outputs: Vec<_> = candles_for_pivots(&[200.0, 100.0])
117            .into_iter()
118            .map(|c| indicator.update(c))
119            .collect();
120        assert!(outputs.iter().all(Option::is_none));
121        assert!(!indicator.is_ready());
122    }
123
124    #[test]
125    fn measured_move_from_three_pivots() {
126        // A = 200 (high), B = 160 (low), C = 190 (high). A->B = -40, projected
127        // down from C.
128        let mut indicator = FibProjection::new();
129        let mut last = None;
130        for candle in candles_for_pivots(&[200.0, 160.0, 190.0]) {
131            last = indicator.update(candle);
132        }
133        let v = last.unwrap();
134        assert!(indicator.is_ready());
135        let (a, b, c) = (200.0, 160.0, 190.0);
136        assert_relative_eq!(v.level_618, c + 0.618 * (b - a));
137        assert_relative_eq!(v.level_1000, c + (b - a));
138        assert_relative_eq!(v.level_1618, c + 1.618 * (b - a));
139        assert_relative_eq!(v.level_2618, c + 2.618 * (b - a));
140    }
141
142    #[test]
143    fn reset_clears_state() {
144        let mut indicator = FibProjection::new();
145        for candle in candles_for_pivots(&[200.0, 160.0, 190.0]) {
146            let _ = indicator.update(candle);
147        }
148        assert!(indicator.is_ready());
149        indicator.reset();
150        assert!(!indicator.is_ready());
151        let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
152        assert!(indicator.update(c).is_none());
153    }
154
155    #[test]
156    fn batch_equals_streaming() {
157        let candles = candles_for_pivots(&[200.0, 160.0, 190.0, 150.0]);
158        let mut a = FibProjection::new();
159        let mut b = FibProjection::new();
160        assert_eq!(
161            a.batch(&candles),
162            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
163        );
164    }
165}