wickra_core/indicators/
fib_fan.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 FibFanOutput {
14 pub fan_382: f64,
16 pub fan_500: f64,
18 pub fan_618: f64,
20}
21
22#[derive(Debug, Clone)]
38pub struct FibFan {
39 swing: SwingTracker,
40}
41
42impl FibFan {
43 #[must_use]
45 pub const fn new() -> Self {
46 Self {
47 swing: SwingTracker::new(SWING_THRESHOLD, 2),
48 }
49 }
50
51 fn fan(&self) -> Option<FibFanOutput> {
52 let pivots = self.swing.pivots();
53 let start = pivots.first()?;
54 let end = pivots.get(1)?;
55 let span_bars = (end.bar - start.bar) as f64;
58 let elapsed = (self.swing.current_bar() - start.bar) as f64;
59 let progress = elapsed / span_bars;
60 let line = |r: f64| start.price + r * (end.price - start.price) * progress;
61 Some(FibFanOutput {
62 fan_382: line(RATIOS[0]),
63 fan_500: line(RATIOS[1]),
64 fan_618: line(RATIOS[2]),
65 })
66 }
67}
68
69impl Default for FibFan {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75impl Indicator for FibFan {
76 type Input = Candle;
77 type Output = FibFanOutput;
78
79 fn update(&mut self, candle: Candle) -> Option<FibFanOutput> {
80 self.swing.update(candle);
81 self.fan()
82 }
83
84 fn reset(&mut self) {
85 self.swing.reset();
86 }
87
88 fn warmup_period(&self) -> usize {
89 2
90 }
91
92 fn is_ready(&self) -> bool {
93 self.swing.pivots().len() >= 2
94 }
95
96 fn name(&self) -> &'static str {
97 "FibFan"
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::traits::BatchExt;
105 use approx::assert_relative_eq;
106
107 fn c(high: f64, low: f64, ts: i64) -> Candle {
108 Candle::new(low, high, low, low, 1.0, ts).unwrap()
109 }
110
111 fn down_leg() -> Vec<Candle> {
114 vec![
115 c(200.0, 199.0, 0), c(190.0, 160.0, 1), c(150.0, 100.0, 2), c(110.0, 105.0, 3), ]
120 }
121
122 #[test]
123 fn accessors_and_metadata() {
124 let indicator = FibFan::new();
125 assert_eq!(indicator.name(), "FibFan");
126 assert_eq!(indicator.warmup_period(), 2);
127 assert!(!indicator.is_ready());
128 assert!(!FibFan::default().is_ready());
129 }
130
131 #[test]
132 fn no_output_before_two_pivots() {
133 let mut indicator = FibFan::new();
134 let outputs: Vec<_> = [c(200.0, 199.0, 0), c(190.0, 150.0, 1)]
136 .into_iter()
137 .map(|x| indicator.update(x))
138 .collect();
139 assert!(outputs.iter().all(Option::is_none));
140 assert!(!indicator.is_ready());
141 }
142
143 #[test]
144 fn fan_lines_open_with_elapsed_time() {
145 let mut indicator = FibFan::new();
146 let mut last = None;
147 for candle in down_leg() {
148 last = indicator.update(candle);
149 }
150 let v = last.unwrap();
151 assert!(indicator.is_ready());
152 assert_relative_eq!(v.fan_382, 200.0 - 0.382 * 150.0);
154 assert_relative_eq!(v.fan_500, 125.0);
155 assert_relative_eq!(v.fan_618, 200.0 - 0.618 * 150.0);
156 }
157
158 #[test]
159 fn reset_clears_state() {
160 let mut indicator = FibFan::new();
161 for candle in down_leg() {
162 let _ = indicator.update(candle);
163 }
164 assert!(indicator.is_ready());
165 indicator.reset();
166 assert!(!indicator.is_ready());
167 assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
168 }
169
170 #[test]
171 fn batch_equals_streaming() {
172 let candles = down_leg();
173 let mut a = FibFan::new();
174 let mut b = FibFan::new();
175 assert_eq!(
176 a.batch(&candles),
177 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
178 );
179 }
180}