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    #[inline]
91    fn update(&mut self, candle: Candle) -> Option<FibProjectionOutput> {
92        self.swing.update(candle);
93        self.levels()
94    }
95
96    fn reset(&mut self) {
97        self.swing.reset();
98    }
99
100    #[inline]
101    fn warmup_period(&self) -> usize {
102        3
103    }
104
105    #[inline]
106    fn is_ready(&self) -> bool {
107        self.swing.pivots().len() >= 3
108    }
109
110    #[inline]
111    fn name(&self) -> &'static str {
112        "FibProjection"
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::indicators::pattern_swing::candles_for_pivots;
120    use crate::traits::BatchExt;
121    use approx::assert_relative_eq;
122
123    #[test]
124    fn accessors_and_metadata() {
125        let indicator = FibProjection::new();
126        assert_eq!(indicator.name(), "FibProjection");
127        assert_eq!(indicator.warmup_period(), 3);
128        assert!(!indicator.is_ready());
129        assert!(!FibProjection::default().is_ready());
130    }
131
132    #[test]
133    fn no_output_before_three_pivots() {
134        let mut indicator = FibProjection::new();
135        let outputs: Vec<_> = candles_for_pivots(&[200.0, 100.0])
136            .into_iter()
137            .map(|c| indicator.update(c))
138            .collect();
139        assert!(outputs.iter().all(Option::is_none));
140        assert!(!indicator.is_ready());
141    }
142
143    #[test]
144    fn measured_move_from_three_pivots() {
145        // A = 200 (high), B = 160 (low), C = 190 (high). A->B = -40, projected
146        // down from C.
147        let mut indicator = FibProjection::new();
148        let mut last = None;
149        for candle in candles_for_pivots(&[200.0, 160.0, 190.0]) {
150            last = indicator.update(candle);
151        }
152        let v = last.unwrap();
153        assert!(indicator.is_ready());
154        let (a, b, c) = (200.0, 160.0, 190.0);
155        assert_relative_eq!(v.level_618, c + 0.618 * (b - a));
156        assert_relative_eq!(v.level_1000, c + (b - a));
157        assert_relative_eq!(v.level_1618, c + 1.618 * (b - a));
158        assert_relative_eq!(v.level_2618, c + 2.618 * (b - a));
159    }
160
161    #[test]
162    fn reset_clears_state() {
163        let mut indicator = FibProjection::new();
164        for candle in candles_for_pivots(&[200.0, 160.0, 190.0]) {
165            let _ = indicator.update(candle);
166        }
167        assert!(indicator.is_ready());
168        indicator.reset();
169        assert!(!indicator.is_ready());
170        let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
171        assert!(indicator.update(c).is_none());
172    }
173
174    #[test]
175    fn batch_equals_streaming() {
176        let candles = candles_for_pivots(&[200.0, 160.0, 190.0, 150.0]);
177        let mut a = FibProjection::new();
178        let mut b = FibProjection::new();
179        assert_eq!(
180            a.batch(&candles),
181            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
182        );
183    }
184}