Skip to main content

wickra_core/indicators/
fib_confluence.rs

1//! Fibonacci Confluence — the strongest retracement cluster across recent legs.
2
3use crate::indicators::pattern_swing::{
4    approx_equal, SwingTracker, LEVEL_TOLERANCE, SWING_THRESHOLD,
5};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9/// How many recent pivots to consider; six pivots yield up to five legs.
10const PIVOT_HISTORY: usize = 6;
11
12/// The retracement ratios contributed by each leg to the confluence search.
13const RATIOS: [f64; 3] = [0.382, 0.5, 0.618];
14
15/// The strongest Fibonacci confluence zone found across recent swing legs.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct FibConfluenceOutput {
18    /// Mean price of the densest cluster of retracement levels.
19    pub price: f64,
20    /// Number of retracement levels that fall inside the cluster (its strength).
21    pub strength: f64,
22}
23
24/// Fibonacci Confluence (`FibConfluence`).
25///
26/// Computes the 38.2% / 50% / 61.8% retracement prices of every leg among the
27/// last six confirmed pivots, then reports the densest price cluster — where
28/// levels from different legs stack up, the zone the market is most likely to
29/// react to. `price` is the cluster mean; `strength` is how many levels it
30/// gathers.
31///
32/// Parameter-free; construction is infallible. Returns `None` until at least two
33/// legs (three pivots) exist.
34///
35/// See `crates/wickra-core/src/indicators/fib_confluence.rs`.
36#[derive(Debug, Clone)]
37pub struct FibConfluence {
38    swing: SwingTracker,
39}
40
41impl FibConfluence {
42    /// Construct a new Fibonacci Confluence tracker.
43    #[must_use]
44    pub const fn new() -> Self {
45        Self {
46            swing: SwingTracker::new(SWING_THRESHOLD, PIVOT_HISTORY),
47        }
48    }
49
50    fn confluence(&self) -> Option<FibConfluenceOutput> {
51        let pivots = self.swing.pivots();
52        if pivots.len() < 3 {
53            return None;
54        }
55        let levels: Vec<f64> = pivots
56            .windows(2)
57            .flat_map(|leg| {
58                let (start, end) = (leg[0].price, leg[1].price);
59                RATIOS.map(|r| end + r * (start - end))
60            })
61            .collect();
62        // The `len < 3` guard guarantees at least two legs, hence a non-empty
63        // level set, so `max_by` always yields a cluster.
64        let (count, total) = levels
65            .iter()
66            .map(|&center| {
67                let members: Vec<f64> = levels
68                    .iter()
69                    .copied()
70                    .filter(|&x| approx_equal(x, center, LEVEL_TOLERANCE))
71                    .collect();
72                (members.len(), members.iter().sum::<f64>())
73            })
74            .max_by(|a, b| a.0.cmp(&b.0))
75            .expect("at least two legs guarantee a non-empty level set");
76        Some(FibConfluenceOutput {
77            price: total / count as f64,
78            strength: count as f64,
79        })
80    }
81}
82
83impl Default for FibConfluence {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl Indicator for FibConfluence {
90    type Input = Candle;
91    type Output = FibConfluenceOutput;
92
93    fn update(&mut self, candle: Candle) -> Option<FibConfluenceOutput> {
94        self.swing.update(candle);
95        self.confluence()
96    }
97
98    fn reset(&mut self) {
99        self.swing.reset();
100    }
101
102    fn warmup_period(&self) -> usize {
103        3
104    }
105
106    fn is_ready(&self) -> bool {
107        self.swing.pivots().len() >= 3
108    }
109
110    fn name(&self) -> &'static str {
111        "FibConfluence"
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::indicators::pattern_swing::candles_for_pivots;
119    use crate::traits::BatchExt;
120    use approx::assert_relative_eq;
121
122    #[test]
123    fn accessors_and_metadata() {
124        let indicator = FibConfluence::new();
125        assert_eq!(indicator.name(), "FibConfluence");
126        assert_eq!(indicator.warmup_period(), 3);
127        assert!(!indicator.is_ready());
128        assert!(!FibConfluence::default().is_ready());
129    }
130
131    #[test]
132    fn no_output_before_two_legs() {
133        let mut indicator = FibConfluence::new();
134        let outputs: Vec<_> = candles_for_pivots(&[200.0, 100.0])
135            .into_iter()
136            .map(|c| indicator.update(c))
137            .collect();
138        assert!(outputs.iter().all(Option::is_none));
139        assert!(!indicator.is_ready());
140    }
141
142    #[test]
143    fn picks_the_densest_cluster() {
144        // Legs 200->100 and 100->160. The 38.2% of each (138.2 and ~137.08)
145        // sit within 3% of each other and form the densest cluster (strength 2).
146        let mut indicator = FibConfluence::new();
147        let mut last = None;
148        for candle in candles_for_pivots(&[200.0, 100.0, 160.0]) {
149            last = indicator.update(candle);
150        }
151        let v = last.unwrap();
152        assert!(indicator.is_ready());
153        assert_relative_eq!(v.strength, 2.0);
154        let want = (138.2 + (160.0 + 0.382 * (100.0 - 160.0))) / 2.0;
155        assert_relative_eq!(v.price, want, epsilon = 1e-9);
156    }
157
158    #[test]
159    fn reset_clears_state() {
160        let mut indicator = FibConfluence::new();
161        for candle in candles_for_pivots(&[200.0, 100.0, 160.0]) {
162            let _ = indicator.update(candle);
163        }
164        assert!(indicator.is_ready());
165        indicator.reset();
166        assert!(!indicator.is_ready());
167        let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
168        assert!(indicator.update(c).is_none());
169    }
170
171    #[test]
172    fn batch_equals_streaming() {
173        let candles = candles_for_pivots(&[200.0, 100.0, 160.0, 120.0]);
174        let mut a = FibConfluence::new();
175        let mut b = FibConfluence::new();
176        assert_eq!(
177            a.batch(&candles),
178            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
179        );
180    }
181}