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