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