wickra_core/indicators/
abcd.rs1use crate::indicators::pattern_swing::{approx_equal, ratios_in, SwingTracker, SWING_THRESHOLD};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
20pub struct Abcd {
21 swing: SwingTracker,
22 has_emitted: bool,
23}
24
25impl Abcd {
26 pub const fn new() -> Self {
28 Self {
29 swing: SwingTracker::new(SWING_THRESHOLD, 4),
30 has_emitted: false,
31 }
32 }
33}
34
35impl Default for Abcd {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl Indicator for Abcd {
42 type Input = Candle;
43 type Output = f64;
44
45 #[inline]
46 fn update(&mut self, candle: Candle) -> Option<f64> {
47 let advanced = self.swing.update(candle);
48 let pivots = self.swing.pivots();
49 if pivots.len() < 4 {
52 return None;
53 }
54 self.has_emitted = true;
55 if !advanced {
58 return Some(0.0);
59 }
60 let len = pivots.len();
61 let pa = pivots[len - 4];
62 let pb = pivots[len - 3];
63 let pc = pivots[len - 2];
64 let pd = pivots[len - 1];
65 let ab = (pb.price - pa.price).abs();
66 let bc = (pc.price - pb.price).abs();
67 let cd = (pd.price - pc.price).abs();
68 let ratios_ok = ratios_in(&[(bc / ab, 0.382, 0.886), (cd / bc, 1.13, 2.618)]);
69 let legs_equal = approx_equal(ab, cd, 0.10);
70 if ratios_ok && legs_equal {
71 return Some(if pd.direction < 0.0 { 1.0 } else { -1.0 });
72 }
73 Some(0.0)
74 }
75
76 fn reset(&mut self) {
77 self.swing.reset();
78 self.has_emitted = false;
79 }
80
81 #[inline]
82 fn warmup_period(&self) -> usize {
83 5
84 }
85
86 #[inline]
87 fn is_ready(&self) -> bool {
88 self.has_emitted
89 }
90
91 #[inline]
92 fn name(&self) -> &'static str {
93 "Abcd"
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
103 fn run(pivots: &[f64]) -> Vec<f64> {
104 let mut indicator = Abcd::new();
105 candles_for_pivots(pivots)
106 .into_iter()
107 .filter_map(|c| indicator.update(c))
108 .collect()
109 }
110
111 #[test]
112 fn accessors_and_metadata() {
113 let indicator = Abcd::new();
114 assert_eq!(indicator.name(), "Abcd");
115 assert_eq!(indicator.warmup_period(), 5);
116 assert!(!indicator.is_ready());
117 assert!(!Abcd::default().is_ready());
118 }
119
120 #[test]
121 fn bullish_abcd_is_plus_one() {
122 let out = run(&[140.0, 100.0, 124.7, 84.7]);
124 assert_eq!(*out.last().unwrap(), 1.0);
125 assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
126 }
127
128 #[test]
129 fn bearish_abcd_is_minus_one() {
130 let out = run(&[150.0, 100.0, 140.0, 115.3, 155.3]);
131 assert_eq!(*out.last().unwrap(), -1.0);
132 }
133
134 #[test]
135 fn unequal_legs_do_not_trigger() {
136 let out = run(&[150.0, 100.0, 140.0, 118.0, 200.0]);
138 assert_eq!(*out.last().unwrap(), 0.0);
139 }
140
141 #[test]
142 fn reset_clears_state() {
143 let mut indicator = Abcd::new();
144 for c in candles_for_pivots(&[140.0, 100.0, 124.7]) {
145 let _ = indicator.update(c);
146 }
147 indicator.reset();
148 assert!(!indicator.is_ready());
149 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
150 assert_eq!(indicator.update(c), None);
151 }
152
153 #[test]
154 fn batch_equals_streaming() {
155 let candles = candles_for_pivots(&[140.0, 100.0, 124.7, 84.7]);
156 let mut a = Abcd::new();
157 let mut b = Abcd::new();
158 assert_eq!(
159 a.batch(&candles),
160 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
161 );
162 }
163}