wickra_core/indicators/
bat.rs1use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
20pub struct Bat {
21 swing: SwingTracker,
22 has_emitted: bool,
23}
24
25impl Bat {
26 pub const fn new() -> Self {
28 Self {
29 swing: SwingTracker::new(SWING_THRESHOLD, 5),
30 has_emitted: false,
31 }
32 }
33}
34
35impl Default for Bat {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl Indicator for Bat {
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() < 5 {
52 return None;
53 }
54 self.has_emitted = true;
55 if !advanced {
58 return Some(0.0);
59 }
60 let p = xabcd(pivots);
61 let xa = (p.a - p.x).abs();
62 let ab = (p.b - p.a).abs();
63 let bc = (p.c - p.b).abs();
64 let cd = (p.d - p.c).abs();
65 let ad = (p.d - p.a).abs();
66 let matched = ratios_in(&[
67 (ab / xa, 0.382, 0.50),
68 (bc / ab, 0.382, 0.886),
69 (cd / bc, 1.618, 2.618),
70 (ad / xa, 0.84, 0.93),
71 ]);
72 if matched {
73 return Some(if p.bullish { 1.0 } else { -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 6
86 }
87
88 #[inline]
89 fn is_ready(&self) -> bool {
90 self.has_emitted
91 }
92
93 #[inline]
94 fn name(&self) -> &'static str {
95 "Bat"
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::indicators::pattern_swing::candles_for_pivots;
103 use crate::traits::BatchExt;
104
105 fn run(pivots: &[f64]) -> Vec<f64> {
106 let mut indicator = Bat::new();
107 candles_for_pivots(pivots)
108 .into_iter()
109 .filter_map(|c| indicator.update(c))
110 .collect()
111 }
112
113 #[test]
114 fn accessors_and_metadata() {
115 let indicator = Bat::new();
116 assert_eq!(indicator.name(), "Bat");
117 assert_eq!(indicator.warmup_period(), 6);
118 assert!(!indicator.is_ready());
119 assert!(!Bat::default().is_ready());
120 }
121
122 #[test]
123 fn bullish_bat_is_plus_one() {
124 let out = run(&[150.0, 100.0, 140.0, 122.0, 137.0, 104.56]);
125 assert_eq!(*out.last().unwrap(), 1.0);
126 assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
127 }
128
129 #[test]
130 fn bearish_bat_is_minus_one() {
131 let out = run(&[150.0, 110.0, 128.0, 113.0, 145.44]);
132 assert_eq!(*out.last().unwrap(), -1.0);
133 }
134
135 #[test]
136 fn out_of_ratio_does_not_trigger() {
137 let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]);
138 assert_eq!(*out.last().unwrap(), 0.0);
139 }
140
141 #[test]
142 fn reset_clears_state() {
143 let mut indicator = Bat::new();
144 for c in candles_for_pivots(&[150.0, 100.0, 140.0]) {
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(&[150.0, 100.0, 140.0, 122.0, 137.0, 104.56]);
156 let mut a = Bat::new();
157 let mut b = Bat::new();
158 assert_eq!(
159 a.batch(&candles),
160 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
161 );
162 }
163}