wickra_core/indicators/
piercing_dark_cloud.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
49pub struct PiercingDarkCloud {
50 prev: Option<Candle>,
51 has_emitted: bool,
52}
53
54impl PiercingDarkCloud {
55 pub const fn new() -> Self {
57 Self {
58 prev: None,
59 has_emitted: false,
60 }
61 }
62}
63
64impl Indicator for PiercingDarkCloud {
65 type Input = Candle;
66 type Output = f64;
67
68 #[inline]
69 fn update(&mut self, candle: Candle) -> Option<f64> {
70 let prev = self.prev;
71 self.prev = Some(candle);
72 let p = prev?;
73 self.has_emitted = true;
74 let prev_red = p.close < p.open;
75 let prev_green = p.close > p.open;
76 let curr_green = candle.close > candle.open;
77 let curr_red = candle.close < candle.open;
78 let mid = f64::midpoint(p.open, p.close);
79 if prev_red
80 && curr_green
81 && candle.open < p.low
82 && candle.close > mid
83 && candle.close < p.open
84 {
85 Some(1.0)
86 } else if prev_green
87 && curr_red
88 && candle.open > p.high
89 && candle.close < mid
90 && candle.close > p.open
91 {
92 Some(-1.0)
93 } else {
94 Some(0.0)
95 }
96 }
97
98 fn reset(&mut self) {
99 self.prev = None;
100 self.has_emitted = false;
101 }
102
103 #[inline]
104 fn warmup_period(&self) -> usize {
105 2
106 }
107
108 #[inline]
109 fn is_ready(&self) -> bool {
110 self.has_emitted
111 }
112
113 #[inline]
114 fn name(&self) -> &'static str {
115 "PiercingDarkCloud"
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::traits::BatchExt;
123
124 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
125 Candle::new(open, high, low, close, 1.0, ts).unwrap()
126 }
127
128 #[test]
129 fn accessors_and_metadata() {
130 let p = PiercingDarkCloud::new();
131 assert_eq!(p.name(), "PiercingDarkCloud");
132 assert_eq!(p.warmup_period(), 2);
133 assert!(!p.is_ready());
134 }
135
136 #[test]
137 fn piercing_line_is_plus_one() {
138 let mut p = PiercingDarkCloud::new();
139 assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), None);
142 assert_eq!(p.update(c(9.8, 11.8, 9.5, 11.5, 1)), Some(1.0));
143 }
144
145 #[test]
146 fn dark_cloud_cover_is_minus_one() {
147 let mut p = PiercingDarkCloud::new();
148 assert_eq!(p.update(c(10.0, 12.2, 9.5, 12.0, 0)), None);
151 assert_eq!(p.update(c(12.3, 12.4, 10.4, 10.5, 1)), Some(-1.0));
152 }
153
154 #[test]
155 fn close_below_midpoint_is_not_piercing() {
156 let mut p = PiercingDarkCloud::new();
157 p.update(c(12.0, 12.5, 10.0, 10.0, 0));
158 assert_eq!(p.update(c(9.8, 11.0, 9.5, 10.8, 1)), Some(0.0));
160 }
161
162 #[test]
163 fn full_engulf_is_not_piercing() {
164 let mut p = PiercingDarkCloud::new();
165 p.update(c(12.0, 12.5, 10.0, 10.0, 0));
166 assert_eq!(p.update(c(9.8, 13.0, 9.5, 12.5, 1)), Some(0.0));
168 }
169
170 #[test]
171 fn first_bar_returns_zero() {
172 let mut p = PiercingDarkCloud::new();
173 assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), None);
174 }
175
176 #[test]
177 fn batch_equals_streaming() {
178 let candles: Vec<Candle> = (0..40)
179 .map(|i| {
180 let base = 100.0 + i as f64;
181 if i % 2 == 0 {
182 c(base + 2.0, base + 2.5, base, base, i)
183 } else {
184 c(base - 0.2, base + 1.8, base - 0.5, base + 1.5, i)
185 }
186 })
187 .collect();
188 let mut a = PiercingDarkCloud::new();
189 let mut b = PiercingDarkCloud::new();
190 assert_eq!(
191 a.batch(&candles),
192 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
193 );
194 }
195
196 #[test]
197 fn reset_clears_state() {
198 let mut p = PiercingDarkCloud::new();
199 p.update(c(12.0, 12.5, 10.0, 10.0, 0));
200 p.update(c(9.8, 11.8, 9.5, 11.5, 1));
201 assert!(p.is_ready());
202 p.reset();
203 assert!(!p.is_ready());
204 assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), None);
205 }
206}