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 #[inline]
80 fn update(&mut self, candle: Candle) -> Option<FibFanOutput> {
81 self.swing.update(candle);
82 self.fan()
83 }
84
85 fn reset(&mut self) {
86 self.swing.reset();
87 }
88
89 #[inline]
90 fn warmup_period(&self) -> usize {
91 2
92 }
93
94 #[inline]
95 fn is_ready(&self) -> bool {
96 self.swing.pivots().len() >= 2
97 }
98
99 #[inline]
100 fn name(&self) -> &'static str {
101 "FibFan"
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::traits::BatchExt;
109 use approx::assert_relative_eq;
110
111 fn c(high: f64, low: f64, ts: i64) -> Candle {
112 Candle::new(low, high, low, low, 1.0, ts).unwrap()
113 }
114
115 fn down_leg() -> Vec<Candle> {
118 vec![
119 c(200.0, 199.0, 0), c(190.0, 160.0, 1), c(150.0, 100.0, 2), c(110.0, 105.0, 3), ]
124 }
125
126 #[test]
127 fn accessors_and_metadata() {
128 let indicator = FibFan::new();
129 assert_eq!(indicator.name(), "FibFan");
130 assert_eq!(indicator.warmup_period(), 2);
131 assert!(!indicator.is_ready());
132 assert!(!FibFan::default().is_ready());
133 }
134
135 #[test]
136 fn no_output_before_two_pivots() {
137 let mut indicator = FibFan::new();
138 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 fan_lines_open_with_elapsed_time() {
149 let mut indicator = FibFan::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 assert_relative_eq!(v.fan_382, 200.0 - 0.382 * 150.0);
158 assert_relative_eq!(v.fan_500, 125.0);
159 assert_relative_eq!(v.fan_618, 200.0 - 0.618 * 150.0);
160 }
161
162 #[test]
163 fn reset_clears_state() {
164 let mut indicator = FibFan::new();
165 for candle in down_leg() {
166 let _ = indicator.update(candle);
167 }
168 assert!(indicator.is_ready());
169 indicator.reset();
170 assert!(!indicator.is_ready());
171 assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
172 }
173
174 #[test]
175 fn batch_equals_streaming() {
176 let candles = down_leg();
177 let mut a = FibFan::new();
178 let mut b = FibFan::new();
179 assert_eq!(
180 a.batch(&candles),
181 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
182 );
183 }
184}