wickra_core/indicators/
wedge.rs1use crate::indicators::pattern_swing::{recent_legs, SwingTracker, SWING_THRESHOLD};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
23pub struct Wedge {
24 swing: SwingTracker,
25 has_emitted: bool,
26}
27
28impl Wedge {
29 pub const fn new() -> Self {
31 Self {
32 swing: SwingTracker::new(SWING_THRESHOLD, 4),
33 has_emitted: false,
34 }
35 }
36}
37
38impl Default for Wedge {
39 fn default() -> Self {
40 Self::new()
41 }
42}
43
44impl Indicator for Wedge {
45 type Input = Candle;
46 type Output = f64;
47
48 #[inline]
49 fn update(&mut self, candle: Candle) -> Option<f64> {
50 let advanced = self.swing.update(candle);
51 let pivots = self.swing.pivots();
52 if pivots.len() < 4 {
55 return None;
56 }
57 self.has_emitted = true;
58 if !advanced {
61 return Some(0.0);
62 }
63 let (high_old, high_new, low_old, low_new) = recent_legs(pivots);
64 let high_slope = high_new - high_old;
65 let low_slope = low_new - low_old;
66
67 if high_slope > 0.0 && low_slope > 0.0 && low_slope > high_slope {
69 return Some(-1.0);
70 }
71 if high_slope < 0.0 && low_slope < 0.0 && high_slope < low_slope {
73 return Some(1.0);
74 }
75 Some(0.0)
76 }
77
78 fn reset(&mut self) {
79 self.swing.reset();
80 self.has_emitted = false;
81 }
82
83 #[inline]
84 fn warmup_period(&self) -> usize {
85 5
87 }
88
89 #[inline]
90 fn is_ready(&self) -> bool {
91 self.has_emitted
92 }
93
94 #[inline]
95 fn name(&self) -> &'static str {
96 "Wedge"
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::indicators::pattern_swing::candles_for_pivots;
104 use crate::traits::BatchExt;
105
106 fn run(pivots: &[f64]) -> Vec<f64> {
107 let mut indicator = Wedge::new();
108 candles_for_pivots(pivots)
109 .into_iter()
110 .filter_map(|c| indicator.update(c))
111 .collect()
112 }
113
114 #[test]
115 fn accessors_and_metadata() {
116 let indicator = Wedge::new();
117 assert_eq!(indicator.name(), "Wedge");
118 assert_eq!(indicator.warmup_period(), 5);
119 assert!(!indicator.is_ready());
120 assert!(!Wedge::default().is_ready());
121 }
122
123 #[test]
124 fn rising_wedge_is_minus_one() {
125 let out = run(&[110.0, 90.0, 100.0, 94.0, 103.0]);
127 assert_eq!(*out.last().unwrap(), -1.0);
128 }
129
130 #[test]
131 fn falling_wedge_is_plus_one() {
132 let out = run(&[120.0, 100.0, 106.0, 99.0]);
134 assert_eq!(*out.last().unwrap(), 1.0);
135 }
136
137 #[test]
138 fn diverging_swings_are_not_a_wedge() {
139 let out = run(&[110.0, 100.0, 130.0, 80.0]);
141 assert_eq!(*out.last().unwrap(), 0.0);
142 }
143
144 #[test]
145 fn reset_clears_state() {
146 let mut indicator = Wedge::new();
147 for c in candles_for_pivots(&[110.0, 90.0, 100.0]) {
148 let _ = indicator.update(c);
149 }
150 indicator.reset();
151 assert!(!indicator.is_ready());
152 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
153 assert_eq!(indicator.update(c), None);
154 }
155
156 #[test]
157 fn batch_equals_streaming() {
158 let candles = candles_for_pivots(&[110.0, 90.0, 100.0, 94.0, 103.0]);
159 let mut a = Wedge::new();
160 let mut b = Wedge::new();
161 assert_eq!(
162 a.batch(&candles),
163 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
164 );
165 }
166}