wickra_core/indicators/
fib_arcs.rs1use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8const RATIOS: [f64; 3] = [0.382, 0.5, 0.618];
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct FibArcsOutput {
14 pub arc_382: f64,
16 pub arc_500: f64,
18 pub arc_618: f64,
20}
21
22#[derive(Debug, Clone)]
40pub struct FibArcs {
41 swing: SwingTracker,
42}
43
44impl FibArcs {
45 #[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 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 fn down_leg() -> Vec<Candle> {
119 vec![
120 c(200.0, 199.0, 0),
121 c(190.0, 160.0, 1), c(150.0, 100.0, 2), c(110.0, 105.0, 3), ]
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 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 let mut indicator = FibArcs::new();
168 for candle in down_leg() {
169 let _ = indicator.update(candle);
170 }
171 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}