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 fn update(&mut self, candle: Candle) -> Option<f64> {
46 self.has_emitted = true;
47 if !self.swing.update(candle) {
48 return Some(0.0);
49 }
50 let pivots = self.swing.pivots();
51 if pivots.len() < 4 {
52 return Some(0.0);
53 }
54 let len = pivots.len();
55 let pa = pivots[len - 4];
56 let pb = pivots[len - 3];
57 let pc = pivots[len - 2];
58 let pd = pivots[len - 1];
59 let ab = (pb.price - pa.price).abs();
60 let bc = (pc.price - pb.price).abs();
61 let cd = (pd.price - pc.price).abs();
62 let ratios_ok = ratios_in(&[(bc / ab, 0.382, 0.886), (cd / bc, 1.13, 2.618)]);
63 let legs_equal = approx_equal(ab, cd, 0.10);
64 if ratios_ok && legs_equal {
65 return Some(if pd.direction < 0.0 { 1.0 } else { -1.0 });
66 }
67 Some(0.0)
68 }
69
70 fn reset(&mut self) {
71 self.swing.reset();
72 self.has_emitted = false;
73 }
74
75 fn warmup_period(&self) -> usize {
76 5
77 }
78
79 fn is_ready(&self) -> bool {
80 self.has_emitted
81 }
82
83 fn name(&self) -> &'static str {
84 "Abcd"
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::indicators::pattern_swing::candles_for_pivots;
92 use crate::traits::BatchExt;
93
94 fn run(pivots: &[f64]) -> Vec<f64> {
95 let mut indicator = Abcd::new();
96 candles_for_pivots(pivots)
97 .into_iter()
98 .map(|c| indicator.update(c).unwrap())
99 .collect()
100 }
101
102 #[test]
103 fn accessors_and_metadata() {
104 let indicator = Abcd::new();
105 assert_eq!(indicator.name(), "Abcd");
106 assert_eq!(indicator.warmup_period(), 5);
107 assert!(!indicator.is_ready());
108 assert!(!Abcd::default().is_ready());
109 }
110
111 #[test]
112 fn bullish_abcd_is_plus_one() {
113 let out = run(&[140.0, 100.0, 124.7, 84.7]);
115 assert_eq!(*out.last().unwrap(), 1.0);
116 assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
117 }
118
119 #[test]
120 fn bearish_abcd_is_minus_one() {
121 let out = run(&[150.0, 100.0, 140.0, 115.3, 155.3]);
122 assert_eq!(*out.last().unwrap(), -1.0);
123 }
124
125 #[test]
126 fn unequal_legs_do_not_trigger() {
127 let out = run(&[150.0, 100.0, 140.0, 118.0, 200.0]);
129 assert_eq!(*out.last().unwrap(), 0.0);
130 }
131
132 #[test]
133 fn reset_clears_state() {
134 let mut indicator = Abcd::new();
135 for c in candles_for_pivots(&[140.0, 100.0, 124.7]) {
136 let _ = indicator.update(c);
137 }
138 indicator.reset();
139 assert!(!indicator.is_ready());
140 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
141 assert_eq!(indicator.update(c), Some(0.0));
142 }
143
144 #[test]
145 fn batch_equals_streaming() {
146 let candles = candles_for_pivots(&[140.0, 100.0, 124.7, 84.7]);
147 let mut a = Abcd::new();
148 let mut b = Abcd::new();
149 assert_eq!(
150 a.batch(&candles),
151 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
152 );
153 }
154}