wickra_core/indicators/
fib_projection.rs1use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7const RATIOS: [f64; 4] = [0.618, 1.0, 1.618, 2.618];
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct FibProjectionOutput {
14 pub level_618: f64,
16 pub level_1000: f64,
18 pub level_1618: f64,
20 pub level_2618: f64,
22}
23
24#[derive(Debug, Clone)]
50pub struct FibProjection {
51 swing: SwingTracker,
52}
53
54impl FibProjection {
55 #[must_use]
57 pub const fn new() -> Self {
58 Self {
59 swing: SwingTracker::new(SWING_THRESHOLD, 3),
60 }
61 }
62
63 fn levels(&self) -> Option<FibProjectionOutput> {
64 let pivots = self.swing.pivots();
65 let [a, b, c] = [
66 pivots.first()?.price,
67 pivots.get(1)?.price,
68 pivots.get(2)?.price,
69 ];
70 let project = |p: f64| c + p * (b - a);
71 Some(FibProjectionOutput {
72 level_618: project(RATIOS[0]),
73 level_1000: project(RATIOS[1]),
74 level_1618: project(RATIOS[2]),
75 level_2618: project(RATIOS[3]),
76 })
77 }
78}
79
80impl Default for FibProjection {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl Indicator for FibProjection {
87 type Input = Candle;
88 type Output = FibProjectionOutput;
89
90 fn update(&mut self, candle: Candle) -> Option<FibProjectionOutput> {
91 self.swing.update(candle);
92 self.levels()
93 }
94
95 fn reset(&mut self) {
96 self.swing.reset();
97 }
98
99 fn warmup_period(&self) -> usize {
100 3
101 }
102
103 fn is_ready(&self) -> bool {
104 self.swing.pivots().len() >= 3
105 }
106
107 fn name(&self) -> &'static str {
108 "FibProjection"
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::indicators::pattern_swing::candles_for_pivots;
116 use crate::traits::BatchExt;
117 use approx::assert_relative_eq;
118
119 #[test]
120 fn accessors_and_metadata() {
121 let indicator = FibProjection::new();
122 assert_eq!(indicator.name(), "FibProjection");
123 assert_eq!(indicator.warmup_period(), 3);
124 assert!(!indicator.is_ready());
125 assert!(!FibProjection::default().is_ready());
126 }
127
128 #[test]
129 fn no_output_before_three_pivots() {
130 let mut indicator = FibProjection::new();
131 let outputs: Vec<_> = candles_for_pivots(&[200.0, 100.0])
132 .into_iter()
133 .map(|c| indicator.update(c))
134 .collect();
135 assert!(outputs.iter().all(Option::is_none));
136 assert!(!indicator.is_ready());
137 }
138
139 #[test]
140 fn measured_move_from_three_pivots() {
141 let mut indicator = FibProjection::new();
144 let mut last = None;
145 for candle in candles_for_pivots(&[200.0, 160.0, 190.0]) {
146 last = indicator.update(candle);
147 }
148 let v = last.unwrap();
149 assert!(indicator.is_ready());
150 let (a, b, c) = (200.0, 160.0, 190.0);
151 assert_relative_eq!(v.level_618, c + 0.618 * (b - a));
152 assert_relative_eq!(v.level_1000, c + (b - a));
153 assert_relative_eq!(v.level_1618, c + 1.618 * (b - a));
154 assert_relative_eq!(v.level_2618, c + 2.618 * (b - a));
155 }
156
157 #[test]
158 fn reset_clears_state() {
159 let mut indicator = FibProjection::new();
160 for candle in candles_for_pivots(&[200.0, 160.0, 190.0]) {
161 let _ = indicator.update(candle);
162 }
163 assert!(indicator.is_ready());
164 indicator.reset();
165 assert!(!indicator.is_ready());
166 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
167 assert!(indicator.update(c).is_none());
168 }
169
170 #[test]
171 fn batch_equals_streaming() {
172 let candles = candles_for_pivots(&[200.0, 160.0, 190.0, 150.0]);
173 let mut a = FibProjection::new();
174 let mut b = FibProjection::new();
175 assert_eq!(
176 a.batch(&candles),
177 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
178 );
179 }
180}