wickra_core/indicators/
fib_channel.rs1use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8const RATIOS: [f64; 3] = [0.618, 1.0, 1.618];
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct FibChannelOutput {
14 pub base: f64,
16 pub level_618: f64,
18 pub level_1000: f64,
20 pub level_1618: f64,
22}
23
24#[derive(Debug, Clone)]
43pub struct FibChannel {
44 swing: SwingTracker,
45}
46
47impl FibChannel {
48 #[must_use]
50 pub const fn new() -> Self {
51 Self {
52 swing: SwingTracker::new(SWING_THRESHOLD, 3),
53 }
54 }
55
56 fn channel(&self) -> Option<FibChannelOutput> {
57 let pivots = self.swing.pivots();
58 let p0 = pivots.first()?;
59 let p1 = pivots.get(1)?;
60 let p2 = pivots.get(2)?;
61 let slope = (p2.price - p0.price) / (p2.bar - p0.bar) as f64;
64 let base_at = |bar: usize| p0.price + slope * (bar - p0.bar) as f64;
65 let width = p1.price - base_at(p1.bar);
66 let base = base_at(self.swing.current_bar());
67 Some(FibChannelOutput {
68 base,
69 level_618: base + RATIOS[0] * width,
70 level_1000: base + RATIOS[1] * width,
71 level_1618: base + RATIOS[2] * width,
72 })
73 }
74}
75
76impl Default for FibChannel {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl Indicator for FibChannel {
83 type Input = Candle;
84 type Output = FibChannelOutput;
85
86 #[inline]
87 fn update(&mut self, candle: Candle) -> Option<FibChannelOutput> {
88 self.swing.update(candle);
89 self.channel()
90 }
91
92 fn reset(&mut self) {
93 self.swing.reset();
94 }
95
96 #[inline]
97 fn warmup_period(&self) -> usize {
98 3
99 }
100
101 #[inline]
102 fn is_ready(&self) -> bool {
103 self.swing.pivots().len() >= 3
104 }
105
106 #[inline]
107 fn name(&self) -> &'static str {
108 "FibChannel"
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::traits::BatchExt;
116 use approx::assert_relative_eq;
117
118 fn c(high: f64, low: f64, ts: i64) -> Candle {
119 Candle::new(low, high, low, low, 1.0, ts).unwrap()
120 }
121
122 fn three_pivots() -> Vec<Candle> {
125 vec![
126 c(200.0, 199.0, 0),
127 c(190.0, 100.0, 1), c(110.0, 108.0, 2), c(220.0, 210.0, 3), c(200.0, 150.0, 4), ]
132 }
133
134 #[test]
135 fn accessors_and_metadata() {
136 let indicator = FibChannel::new();
137 assert_eq!(indicator.name(), "FibChannel");
138 assert_eq!(indicator.warmup_period(), 3);
139 assert!(!indicator.is_ready());
140 assert!(!FibChannel::default().is_ready());
141 }
142
143 #[test]
144 fn no_output_before_three_pivots() {
145 let mut indicator = FibChannel::new();
146 let outputs: Vec<_> = [c(200.0, 199.0, 0), c(190.0, 100.0, 1), c(110.0, 108.0, 2)]
147 .into_iter()
148 .map(|x| indicator.update(x))
149 .collect();
150 assert!(outputs.iter().all(Option::is_none));
152 assert!(!indicator.is_ready());
153 }
154
155 #[test]
156 fn channel_levels_from_three_pivots() {
157 let mut indicator = FibChannel::new();
158 let mut last = None;
159 for candle in three_pivots() {
160 last = indicator.update(candle);
161 }
162 let v = last.unwrap();
163 assert!(indicator.is_ready());
164 let slope = (220.0 - 200.0) / 3.0;
166 let base_cur = 200.0 + slope * 4.0;
167 let width = 100.0 - (200.0 + slope * 1.0);
168 assert_relative_eq!(v.base, base_cur);
169 assert_relative_eq!(v.level_1000, base_cur + width);
170 assert_relative_eq!(v.level_618, base_cur + 0.618 * width);
171 assert_relative_eq!(v.level_1618, base_cur + 1.618 * width);
172 }
173
174 #[test]
175 fn reset_clears_state() {
176 let mut indicator = FibChannel::new();
177 for candle in three_pivots() {
178 let _ = indicator.update(candle);
179 }
180 assert!(indicator.is_ready());
181 indicator.reset();
182 assert!(!indicator.is_ready());
183 assert!(indicator.update(c(100.0, 99.5, 0)).is_none());
184 }
185
186 #[test]
187 fn batch_equals_streaming() {
188 let candles = three_pivots();
189 let mut a = FibChannel::new();
190 let mut b = FibChannel::new();
191 assert_eq!(
192 a.batch(&candles),
193 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
194 );
195 }
196}