wickra_core/indicators/
engulfing.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
38pub struct Engulfing {
39 prev: Option<Candle>,
40 has_emitted: bool,
41}
42
43impl Engulfing {
44 pub const fn new() -> Self {
46 Self {
47 prev: None,
48 has_emitted: false,
49 }
50 }
51}
52
53impl Indicator for Engulfing {
54 type Input = Candle;
55 type Output = f64;
56
57 fn update(&mut self, candle: Candle) -> Option<f64> {
58 self.has_emitted = true;
59 let prev = self.prev;
60 self.prev = Some(candle);
61 let Some(p) = prev else {
62 return Some(0.0);
63 };
64 let prev_body = (p.close - p.open).abs();
65 let curr_body = (candle.close - candle.open).abs();
66 if prev_body <= 0.0 || curr_body <= prev_body {
67 return Some(0.0);
68 }
69 let prev_red = p.close < p.open;
70 let prev_green = p.close > p.open;
71 let curr_green = candle.close > candle.open;
72 let curr_red = candle.close < candle.open;
73 if prev_red && curr_green && candle.open <= p.close && candle.close >= p.open {
74 Some(1.0)
75 } else if prev_green && curr_red && candle.open >= p.close && candle.close <= p.open {
76 Some(-1.0)
77 } else {
78 Some(0.0)
79 }
80 }
81
82 fn reset(&mut self) {
83 self.prev = None;
84 self.has_emitted = false;
85 }
86
87 fn warmup_period(&self) -> usize {
88 2
89 }
90
91 fn is_ready(&self) -> bool {
92 self.has_emitted
93 }
94
95 fn name(&self) -> &'static str {
96 "Engulfing"
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::traits::BatchExt;
104
105 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
106 Candle::new(open, high, low, close, 1.0, ts).unwrap()
107 }
108
109 #[test]
110 fn accessors_and_metadata() {
111 let e = Engulfing::new();
112 assert_eq!(e.name(), "Engulfing");
113 assert_eq!(e.warmup_period(), 2);
114 assert!(!e.is_ready());
115 }
116
117 #[test]
118 fn bullish_engulfing_is_plus_one() {
119 let mut e = Engulfing::new();
120 assert_eq!(e.update(c(11.0, 11.2, 9.8, 10.0, 0)), Some(0.0));
122 assert_eq!(e.update(c(9.5, 12.0, 9.5, 11.5, 1)), Some(1.0));
123 }
124
125 #[test]
126 fn bearish_engulfing_is_minus_one() {
127 let mut e = Engulfing::new();
128 assert_eq!(e.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
130 assert_eq!(e.update(c(12.0, 12.0, 9.0, 9.0, 1)), Some(-1.0));
131 }
132
133 #[test]
134 fn same_direction_is_not_engulfing() {
135 let mut e = Engulfing::new();
136 e.update(c(10.0, 11.0, 9.8, 11.0, 0));
137 assert_eq!(e.update(c(9.5, 12.0, 9.5, 11.5, 1)), Some(0.0));
139 }
140
141 #[test]
142 fn smaller_body_is_not_engulfing() {
143 let mut e = Engulfing::new();
144 e.update(c(11.0, 11.2, 8.0, 8.5, 0));
145 assert_eq!(e.update(c(8.6, 9.0, 8.4, 8.7, 1)), Some(0.0));
147 }
148
149 #[test]
150 fn first_bar_returns_zero() {
151 let mut e = Engulfing::new();
152 assert_eq!(e.update(c(10.0, 11.0, 9.0, 11.0, 0)), Some(0.0));
153 }
154
155 #[test]
156 fn batch_equals_streaming() {
157 let candles: Vec<Candle> = (0..40)
158 .map(|i| {
159 let base = 100.0 + i as f64;
160 if i % 3 == 0 {
161 c(base + 1.0, base + 1.5, base - 0.5, base, i)
162 } else {
163 c(base - 1.0, base + 2.0, base - 1.5, base + 2.0, i)
164 }
165 })
166 .collect();
167 let mut a = Engulfing::new();
168 let mut b = Engulfing::new();
169 assert_eq!(
170 a.batch(&candles),
171 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
172 );
173 }
174
175 #[test]
176 fn reset_clears_state() {
177 let mut e = Engulfing::new();
178 e.update(c(10.0, 11.0, 9.0, 11.0, 0));
179 e.update(c(11.0, 12.0, 10.0, 12.0, 1));
180 assert!(e.is_ready());
181 e.reset();
182 assert!(!e.is_ready());
183 assert_eq!(e.update(c(11.0, 11.2, 9.8, 10.0, 0)), Some(0.0));
185 }
186}