wickra_core/indicators/
butterfly.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 Butterfly {
21 swing: SwingTracker,
22 has_emitted: bool,
23}
24
25impl Butterfly {
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 Butterfly {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl Indicator for Butterfly {
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() < 5 {
52 return Some(0.0);
53 }
54 let p = xabcd(pivots);
55 let xa = (p.a - p.x).abs();
56 let ab = (p.b - p.a).abs();
57 let bc = (p.c - p.b).abs();
58 let cd = (p.d - p.c).abs();
59 let ad = (p.d - p.a).abs();
60 let matched = ratios_in(&[
61 (ab / xa, 0.74, 0.84),
62 (bc / ab, 0.382, 0.886),
63 (cd / bc, 1.618, 2.618),
64 (ad / xa, 1.27, 1.618),
65 ]);
66 if matched {
67 return Some(if p.bullish { 1.0 } else { -1.0 });
68 }
69 Some(0.0)
70 }
71
72 fn reset(&mut self) {
73 self.swing.reset();
74 self.has_emitted = false;
75 }
76
77 fn warmup_period(&self) -> usize {
78 6
79 }
80
81 fn is_ready(&self) -> bool {
82 self.has_emitted
83 }
84
85 fn name(&self) -> &'static str {
86 "Butterfly"
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::indicators::pattern_swing::candles_for_pivots;
94 use crate::traits::BatchExt;
95
96 fn run(pivots: &[f64]) -> Vec<f64> {
97 let mut indicator = Butterfly::new();
98 candles_for_pivots(pivots)
99 .into_iter()
100 .map(|c| indicator.update(c).unwrap())
101 .collect()
102 }
103
104 #[test]
105 fn accessors_and_metadata() {
106 let indicator = Butterfly::new();
107 assert_eq!(indicator.name(), "Butterfly");
108 assert_eq!(indicator.warmup_period(), 6);
109 assert!(!indicator.is_ready());
110 assert!(!Butterfly::default().is_ready());
111 }
112
113 #[test]
114 fn bullish_butterfly_is_plus_one() {
115 let out = run(&[150.0, 100.0, 140.0, 108.6, 128.0, 79.8]);
116 assert_eq!(*out.last().unwrap(), 1.0);
117 assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
118 }
119
120 #[test]
121 fn bearish_butterfly_is_minus_one() {
122 let out = run(&[150.0, 110.0, 141.4, 121.4, 170.2]);
123 assert_eq!(*out.last().unwrap(), -1.0);
124 }
125
126 #[test]
127 fn out_of_ratio_does_not_trigger() {
128 let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]);
129 assert_eq!(*out.last().unwrap(), 0.0);
130 }
131
132 #[test]
133 fn reset_clears_state() {
134 let mut indicator = Butterfly::new();
135 for c in candles_for_pivots(&[150.0, 100.0, 140.0]) {
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(&[150.0, 100.0, 140.0, 108.6, 128.0, 79.8]);
147 let mut a = Butterfly::new();
148 let mut b = Butterfly::new();
149 assert_eq!(
150 a.batch(&candles),
151 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
152 );
153 }
154}