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