1use 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)]
40pub struct FibFan {
41 swing: SwingTracker,
42}
43
44impl FibFan {
45 #[must_use]
47 pub const fn new() -> Self {
48 Self {
49 swing: SwingTracker::new(SWING_THRESHOLD, 2),
50 }
51 }
52
53 fn fan(&self) -> Option<FibFanOutput> {
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;
60 let elapsed = (self.swing.current_bar() - start.bar) as f64;
61 let progress = elapsed / span_bars;
62 let line = |r: f64| start.price + (1.0 - r) * (end.price - start.price) * progress;
63 Some(FibFanOutput {
64 fan_382: line(RATIOS[0]),
65 fan_500: line(RATIOS[1]),
66 fan_618: line(RATIOS[2]),
67 })
68 }
69}
70
71impl Default for FibFan {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl Indicator for FibFan {
78 type Input = Candle;
79 type Output = FibFanOutput;
80
81 #[inline]
82 fn update(&mut self, candle: Candle) -> Option<FibFanOutput> {
83 self.swing.update(candle);
84 self.fan()
85 }
86
87 fn reset(&mut self) {
88 self.swing.reset();
89 }
90
91 #[inline]
92 fn warmup_period(&self) -> usize {
93 2
94 }
95
96 #[inline]
97 fn is_ready(&self) -> bool {
98 self.swing.pivots().len() >= 2
99 }
100
101 #[inline]
102 fn name(&self) -> &'static str {
103 "FibFan"
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::traits::BatchExt;
111 use approx::assert_relative_eq;
112
113 fn c(high: f64, low: f64, ts: i64) -> Candle {
114 Candle::new(low, high, low, low, 1.0, ts).unwrap()
115 }
116
117 fn down_leg() -> Vec<Candle> {
120 vec![
121 c(200.0, 199.0, 0), c(190.0, 160.0, 1), c(150.0, 100.0, 2), c(110.0, 105.0, 3), ]
126 }
127
128 #[test]
129 fn accessors_and_metadata() {
130 let indicator = FibFan::new();
131 assert_eq!(indicator.name(), "FibFan");
132 assert_eq!(indicator.warmup_period(), 2);
133 assert!(!indicator.is_ready());
134 assert!(!FibFan::default().is_ready());
135 }
136
137 #[test]
138 fn no_output_before_two_pivots() {
139 let mut indicator = FibFan::new();
140 let outputs: Vec<_> = [c(200.0, 199.0, 0), c(190.0, 150.0, 1)]
142 .into_iter()
143 .map(|x| indicator.update(x))
144 .collect();
145 assert!(outputs.iter().all(Option::is_none));
146 assert!(!indicator.is_ready());
147 }
148
149 #[test]
150 fn fan_lines_open_with_elapsed_time() {
151 let mut indicator = FibFan::new();
152 let mut last = None;
153 for candle in down_leg() {
154 last = indicator.update(candle);
155 }
156 let v = last.unwrap();
157 assert!(indicator.is_ready());
158 assert_relative_eq!(v.fan_382, 200.0 - (1.0 - 0.382) * 150.0);
160 assert_relative_eq!(v.fan_500, 125.0);
161 assert_relative_eq!(v.fan_618, 200.0 - (1.0 - 0.618) * 150.0);
162 }
163
164 #[test]
165 fn reset_clears_state() {
166 let mut indicator = FibFan::new();
167 for candle in down_leg() {
168 let _ = indicator.update(candle);
169 }
170 assert!(indicator.is_ready());
171 indicator.reset();
172 assert!(!indicator.is_ready());
173 assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
174 }
175
176 #[test]
177 fn batch_equals_streaming() {
178 let candles = down_leg();
179 let mut a = FibFan::new();
180 let mut b = FibFan::new();
181 assert_eq!(
182 a.batch(&candles),
183 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
184 );
185 }
186
187 fn fast_series() -> Vec<Candle> {
190 vec![
191 c(200.0, 199.0, 0), c(190.0, 150.0, 1), c(160.0, 155.0, 2), c(180.0, 170.0, 3), c(175.0, 165.0, 4), c(170.0, 168.0, 5), ]
198 }
199
200 #[test]
201 fn first_value_lands_on_the_earliest_possible_bar() {
202 let out = FibFan::new().batch(&fast_series());
203 assert!(out[..1].iter().all(Option::is_none));
205 assert!(out[1].is_none());
208 assert!(out[2].is_some());
209 }
210
211 #[test]
212 fn hand_computed_down_then_up_leg() {
213 let mut indicator = FibFan::new();
214 let out = indicator.batch(&fast_series());
215 let v2 = out[2].unwrap();
219 assert_relative_eq!(v2.fan_382, 138.2, epsilon = 1e-9);
220 assert_relative_eq!(v2.fan_500, 150.0, epsilon = 1e-9);
221 assert_relative_eq!(v2.fan_618, 161.8, epsilon = 1e-9);
222 let v3 = out[3].unwrap();
224 assert_relative_eq!(v3.fan_382, 107.3, epsilon = 1e-9);
225 assert_relative_eq!(v3.fan_500, 125.0, epsilon = 1e-9);
226 assert_relative_eq!(v3.fan_618, 142.7, epsilon = 1e-9);
227 let v4 = out[4].unwrap();
230 assert_relative_eq!(v4.fan_382, 177.81, epsilon = 1e-9);
231 assert_relative_eq!(v4.fan_500, 172.5, epsilon = 1e-9);
232 assert_relative_eq!(v4.fan_618, 167.19, epsilon = 1e-9);
233 let v5 = out[5].unwrap();
235 assert_relative_eq!(v5.fan_382, 187.08, epsilon = 1e-9);
236 assert_relative_eq!(v5.fan_500, 180.0, epsilon = 1e-9);
237 assert_relative_eq!(v5.fan_618, 172.92, epsilon = 1e-9);
238 assert!(v5.fan_382 > v5.fan_500 && v5.fan_500 > v5.fan_618);
240 }
241
242 #[test]
243 fn line_at_unit_progress_is_measured_back_from_leg_end() {
244 let mut indicator = FibFan::new();
249 let v = indicator.batch(&down_leg())[3].unwrap();
250 let at_end = |line: f64| (line - 200.0) / 1.5 + 200.0;
252 assert_relative_eq!(at_end(v.fan_382), 100.0 + 0.382 * 100.0, epsilon = 1e-9);
253 assert_relative_eq!(at_end(v.fan_500), 150.0, epsilon = 1e-9);
254 assert_relative_eq!(at_end(v.fan_618), 100.0 + 0.618 * 100.0, epsilon = 1e-9);
255 }
256
257 #[test]
258 fn reset_replays_identically_to_fresh_instance() {
259 let candles = fast_series();
260 let mut indicator = FibFan::new();
261 let first = indicator.batch(&candles);
262 indicator.reset();
263 let second = indicator.batch(&candles);
264 assert_eq!(first, second);
265 assert_eq!(second, FibFan::default().batch(&candles));
266 }
267
268 #[test]
269 fn batch_equals_streaming_over_multiple_legs() {
270 let candles = fast_series();
271 let mut streaming = FibFan::new();
272 let streamed: Vec<_> = candles.iter().map(|x| streaming.update(*x)).collect();
273 assert_eq!(FibFan::new().batch(&candles), streamed);
274 }
275}