wickra_core/indicators/
fib_extension.rs1use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7const RATIOS: [f64; 5] = [1.272, 1.414, 1.618, 2.0, 2.618];
11
12#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct FibExtensionOutput {
18 pub level_1272: f64,
20 pub level_1414: f64,
22 pub level_1618: f64,
24 pub level_2000: f64,
26 pub level_2618: f64,
28}
29
30#[derive(Debug, Clone)]
56pub struct FibExtension {
57 swing: SwingTracker,
58}
59
60impl FibExtension {
61 #[must_use]
63 pub const fn new() -> Self {
64 Self {
65 swing: SwingTracker::new(SWING_THRESHOLD, 2),
66 }
67 }
68
69 fn level(start: f64, end: f64, e: f64) -> f64 {
72 start + e * (end - start)
73 }
74
75 fn levels(&self) -> Option<FibExtensionOutput> {
76 let pivots = self.swing.pivots();
77 let [start, end] = [pivots.first()?.price, pivots.get(1)?.price];
78 Some(FibExtensionOutput {
79 level_1272: Self::level(start, end, RATIOS[0]),
80 level_1414: Self::level(start, end, RATIOS[1]),
81 level_1618: Self::level(start, end, RATIOS[2]),
82 level_2000: Self::level(start, end, RATIOS[3]),
83 level_2618: Self::level(start, end, RATIOS[4]),
84 })
85 }
86}
87
88impl Default for FibExtension {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94impl Indicator for FibExtension {
95 type Input = Candle;
96 type Output = FibExtensionOutput;
97
98 #[inline]
99 fn update(&mut self, candle: Candle) -> Option<FibExtensionOutput> {
100 self.swing.update(candle);
101 self.levels()
102 }
103
104 fn reset(&mut self) {
105 self.swing.reset();
106 }
107
108 #[inline]
109 fn warmup_period(&self) -> usize {
110 2
111 }
112
113 #[inline]
114 fn is_ready(&self) -> bool {
115 self.swing.pivots().len() >= 2
116 }
117
118 #[inline]
119 fn name(&self) -> &'static str {
120 "FibExtension"
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::indicators::pattern_swing::candles_for_pivots;
128 use crate::traits::BatchExt;
129 use approx::assert_relative_eq;
130
131 #[test]
132 fn accessors_and_metadata() {
133 let indicator = FibExtension::new();
134 assert_eq!(indicator.name(), "FibExtension");
135 assert_eq!(indicator.warmup_period(), 2);
136 assert!(!indicator.is_ready());
137 assert!(!FibExtension::default().is_ready());
138 }
139
140 #[test]
141 fn no_output_before_two_pivots() {
142 let mut indicator = FibExtension::new();
143 let outputs: Vec<_> = candles_for_pivots(&[120.0])
144 .into_iter()
145 .map(|c| indicator.update(c))
146 .collect();
147 assert!(outputs.iter().all(Option::is_none));
148 }
149
150 #[test]
151 fn extension_levels_of_a_down_leg() {
152 let mut indicator = FibExtension::new();
154 let mut last = None;
155 for candle in candles_for_pivots(&[200.0, 100.0]) {
156 last = indicator.update(candle);
157 }
158 let v = last.unwrap();
159 assert!(indicator.is_ready());
160 assert_relative_eq!(v.level_1272, 200.0 - 127.2);
162 assert_relative_eq!(v.level_1414, 200.0 - 141.4);
163 assert_relative_eq!(v.level_1618, 200.0 - 161.8);
164 assert_relative_eq!(v.level_2000, 0.0);
165 assert_relative_eq!(v.level_2618, 200.0 - 261.8);
166 }
167
168 #[test]
169 fn reset_clears_state() {
170 let mut indicator = FibExtension::new();
171 for candle in candles_for_pivots(&[200.0, 100.0]) {
172 let _ = indicator.update(candle);
173 }
174 indicator.reset();
175 assert!(!indicator.is_ready());
176 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
177 assert!(indicator.update(c).is_none());
178 }
179
180 #[test]
181 fn batch_equals_streaming() {
182 let candles = candles_for_pivots(&[200.0, 100.0, 150.0]);
183 let mut a = FibExtension::new();
184 let mut b = FibExtension::new();
185 assert_eq!(
186 a.batch(&candles),
187 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
188 );
189 }
190}