wickra_core/indicators/
flag_pennant.rs1use crate::indicators::pattern_swing::{SwingTracker, SWING_THRESHOLD};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7const MAX_RETRACE_FRACTION: f64 = 0.5;
10
11#[derive(Debug, Clone)]
29pub struct FlagPennant {
30 swing: SwingTracker,
31 has_emitted: bool,
32}
33
34impl FlagPennant {
35 pub const fn new() -> Self {
37 Self {
38 swing: SwingTracker::new(SWING_THRESHOLD, 3),
39 has_emitted: false,
40 }
41 }
42}
43
44impl Default for FlagPennant {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl Indicator for FlagPennant {
51 type Input = Candle;
52 type Output = f64;
53
54 #[inline]
55 fn update(&mut self, candle: Candle) -> Option<f64> {
56 let advanced = self.swing.update(candle);
57 let pivots = self.swing.pivots();
58 if pivots.len() < 3 {
61 return None;
62 }
63 self.has_emitted = true;
64 if !advanced {
67 return Some(0.0);
68 }
69 let n = pivots.len();
70 let pole_start = pivots[n - 3];
71 let pole_end = pivots[n - 2];
72 let consolidation = pivots[n - 1];
73 let pole = (pole_end.price - pole_start.price).abs();
74 let pullback = (consolidation.price - pole_end.price).abs();
75
76 if pole > 0.0 && pullback < MAX_RETRACE_FRACTION * pole {
77 return Some(if pole_end.direction > 0.0 { 1.0 } else { -1.0 });
79 }
80 Some(0.0)
81 }
82
83 fn reset(&mut self) {
84 self.swing.reset();
85 self.has_emitted = false;
86 }
87
88 #[inline]
89 fn warmup_period(&self) -> usize {
90 4
92 }
93
94 #[inline]
95 fn is_ready(&self) -> bool {
96 self.has_emitted
97 }
98
99 #[inline]
100 fn name(&self) -> &'static str {
101 "FlagPennant"
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::indicators::pattern_swing::candles_for_pivots;
109 use crate::traits::BatchExt;
110
111 fn run(pivots: &[f64]) -> Vec<f64> {
112 let mut indicator = FlagPennant::new();
113 candles_for_pivots(pivots)
114 .into_iter()
115 .filter_map(|c| indicator.update(c))
116 .collect()
117 }
118
119 #[test]
120 fn accessors_and_metadata() {
121 let indicator = FlagPennant::new();
122 assert_eq!(indicator.name(), "FlagPennant");
123 assert_eq!(indicator.warmup_period(), 4);
124 assert!(!indicator.is_ready());
125 assert!(!FlagPennant::default().is_ready());
126 }
127
128 #[test]
129 fn bull_flag_is_plus_one() {
130 let out = run(&[150.0, 100.0, 140.0, 130.0]);
132 assert_eq!(*out.last().unwrap(), 1.0);
133 }
134
135 #[test]
136 fn bear_flag_is_minus_one() {
137 let out = run(&[140.0, 100.0, 110.0]);
139 assert_eq!(*out.last().unwrap(), -1.0);
140 }
141
142 #[test]
143 fn deep_pullback_is_not_a_flag() {
144 let out = run(&[150.0, 100.0, 140.0, 104.0]);
146 assert_eq!(*out.last().unwrap(), 0.0);
147 }
148
149 #[test]
150 fn reset_clears_state() {
151 let mut indicator = FlagPennant::new();
152 for c in candles_for_pivots(&[150.0, 100.0, 140.0]) {
153 let _ = indicator.update(c);
154 }
155 indicator.reset();
156 assert!(!indicator.is_ready());
157 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
158 assert_eq!(indicator.update(c), None);
159 }
160
161 #[test]
162 fn batch_equals_streaming() {
163 let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 130.0]);
164 let mut a = FlagPennant::new();
165 let mut b = FlagPennant::new();
166 assert_eq!(
167 a.batch(&candles),
168 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
169 );
170 }
171}