Skip to main content

wickra_core/indicators/
fib_arcs.rs

1//! Fibonacci Arcs — semicircular retracement levels centred on the swing end,
2//! decaying back toward it as time elapses.
3
4use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8/// The three arc ratios drawn (38.2% / 50% / 61.8%).
9const RATIOS: [f64; 3] = [0.382, 0.5, 0.618];
10
11/// Fibonacci Arc prices evaluated at the current bar.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct FibArcsOutput {
14    /// Price of the 38.2% arc at the current bar.
15    pub arc_382: f64,
16    /// Price of the 50% arc at the current bar.
17    pub arc_500: f64,
18    /// Price of the 61.8% arc at the current bar.
19    pub arc_618: f64,
20}
21
22/// Fibonacci Arcs (`FibArcs`).
23///
24/// Three arcs centred on the end of the most recent confirmed swing leg. Time is
25/// normalised by the leg's bar-width so the construction is chart-scale-free: at
26/// the leg's end bar each arc sits exactly on its retracement level, and as time
27/// elapses the arc curves back toward the swing-end price, reaching it one leg
28/// width later.
29///
30/// ```text
31/// u       = (cur - end_bar) / (end_bar - start_bar)
32/// arc(r)  = end + (start - end) * r * sqrt(max(0, 1 - u^2))
33/// ```
34///
35/// Parameter-free; construction is infallible. Returns `None` until the first
36/// leg is complete.
37///
38/// See `crates/wickra-core/src/indicators/fib_arcs.rs`.
39#[derive(Debug, Clone)]
40pub struct FibArcs {
41    swing: SwingTracker,
42}
43
44impl FibArcs {
45    /// Construct a new Fibonacci Arcs tracker.
46    #[must_use]
47    pub const fn new() -> Self {
48        Self {
49            swing: SwingTracker::new(SWING_THRESHOLD, 2),
50        }
51    }
52
53    fn arcs(&self) -> Option<FibArcsOutput> {
54        let pivots = self.swing.pivots();
55        let start = pivots.first()?;
56        let end = pivots.get(1)?;
57        // Consecutive pivots occur at strictly increasing bars → span >= 1 bar.
58        let span_bars = (end.bar - start.bar) as f64;
59        let u = (self.swing.current_bar() - end.bar) as f64 / span_bars;
60        let curve = (1.0 - u * u).max(0.0).sqrt();
61        let arc = |r: f64| end.price + (start.price - end.price) * r * curve;
62        Some(FibArcsOutput {
63            arc_382: arc(RATIOS[0]),
64            arc_500: arc(RATIOS[1]),
65            arc_618: arc(RATIOS[2]),
66        })
67    }
68}
69
70impl Default for FibArcs {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl Indicator for FibArcs {
77    type Input = Candle;
78    type Output = FibArcsOutput;
79
80    #[inline]
81    fn update(&mut self, candle: Candle) -> Option<FibArcsOutput> {
82        self.swing.update(candle);
83        self.arcs()
84    }
85
86    fn reset(&mut self) {
87        self.swing.reset();
88    }
89
90    #[inline]
91    fn warmup_period(&self) -> usize {
92        2
93    }
94
95    #[inline]
96    fn is_ready(&self) -> bool {
97        self.swing.pivots().len() >= 2
98    }
99
100    #[inline]
101    fn name(&self) -> &'static str {
102        "FibArcs"
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::traits::BatchExt;
110    use approx::assert_relative_eq;
111
112    fn c(high: f64, low: f64, ts: i64) -> Candle {
113        Candle::new(low, high, low, low, 1.0, ts).unwrap()
114    }
115
116    /// Leg start=200 (bar 0) -> end=100 (bar 2), confirmed at bar 3 so the arc is
117    /// first reported with `u = (3 - 2) / (2 - 0) = 0.5`.
118    fn down_leg() -> Vec<Candle> {
119        vec![
120            c(200.0, 199.0, 0),
121            c(190.0, 160.0, 1), // confirm high @200
122            c(150.0, 100.0, 2), // extend low to 100 (bar 2)
123            c(110.0, 105.0, 3), // confirm low @100 -> two pivots
124        ]
125    }
126
127    #[test]
128    fn accessors_and_metadata() {
129        let indicator = FibArcs::new();
130        assert_eq!(indicator.name(), "FibArcs");
131        assert_eq!(indicator.warmup_period(), 2);
132        assert!(!indicator.is_ready());
133        assert!(!FibArcs::default().is_ready());
134    }
135
136    #[test]
137    fn no_output_before_two_pivots() {
138        let mut indicator = FibArcs::new();
139        let outputs: Vec<_> = [c(200.0, 199.0, 0), c(190.0, 150.0, 1)]
140            .into_iter()
141            .map(|x| indicator.update(x))
142            .collect();
143        assert!(outputs.iter().all(Option::is_none));
144        assert!(!indicator.is_ready());
145    }
146
147    #[test]
148    fn arcs_curve_back_toward_the_swing_end() {
149        let mut indicator = FibArcs::new();
150        let mut last = None;
151        for candle in down_leg() {
152            last = indicator.update(candle);
153        }
154        let v = last.unwrap();
155        assert!(indicator.is_ready());
156        // u = 0.5 → curve = sqrt(0.75); arc(r) = 100 + 100 * r * curve.
157        let curve = 0.75_f64.sqrt();
158        assert_relative_eq!(v.arc_382, 100.0 + 100.0 * 0.382 * curve);
159        assert_relative_eq!(v.arc_500, 100.0 + 100.0 * 0.5 * curve);
160        assert_relative_eq!(v.arc_618, 100.0 + 100.0 * 0.618 * curve);
161    }
162
163    #[test]
164    fn arc_clamps_to_zero_beyond_one_leg_width() {
165        // Extend far past the end pivot so u > 1; the curve clamps to 0 and the
166        // arcs collapse onto the swing-end price.
167        let mut indicator = FibArcs::new();
168        for candle in down_leg() {
169            let _ = indicator.update(candle);
170        }
171        // Feed flat bars that neither extend nor confirm a new pivot.
172        let mut last = None;
173        for ts in 4..12 {
174            last = indicator.update(c(108.0, 106.0, ts));
175        }
176        let v = last.unwrap();
177        assert_relative_eq!(v.arc_382, 100.0);
178        assert_relative_eq!(v.arc_618, 100.0);
179    }
180
181    #[test]
182    fn reset_clears_state() {
183        let mut indicator = FibArcs::new();
184        for candle in down_leg() {
185            let _ = indicator.update(candle);
186        }
187        indicator.reset();
188        assert!(!indicator.is_ready());
189        assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
190    }
191
192    #[test]
193    fn batch_equals_streaming() {
194        let candles = down_leg();
195        let mut a = FibArcs::new();
196        let mut b = FibArcs::new();
197        assert_eq!(
198            a.batch(&candles),
199            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
200        );
201    }
202}