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