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 fn update(&mut self, candle: Candle) -> Option<f64> {
69 self.has_emitted = true;
70 let prev = self.prev;
71 self.prev = Some(candle);
72 let Some(p) = prev else {
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 let mid = f64::midpoint(p.open, p.close);
80 if prev_red
81 && curr_green
82 && candle.open < p.low
83 && candle.close > mid
84 && candle.close < p.open
85 {
86 Some(1.0)
87 } else if prev_green
88 && curr_red
89 && candle.open > p.high
90 && candle.close < mid
91 && candle.close > p.open
92 {
93 Some(-1.0)
94 } else {
95 Some(0.0)
96 }
97 }
98
99 fn reset(&mut self) {
100 self.prev = None;
101 self.has_emitted = false;
102 }
103
104 fn warmup_period(&self) -> usize {
105 2
106 }
107
108 fn is_ready(&self) -> bool {
109 self.has_emitted
110 }
111
112 fn name(&self) -> &'static str {
113 "PiercingDarkCloud"
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::traits::BatchExt;
121
122 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
123 Candle::new(open, high, low, close, 1.0, ts).unwrap()
124 }
125
126 #[test]
127 fn accessors_and_metadata() {
128 let p = PiercingDarkCloud::new();
129 assert_eq!(p.name(), "PiercingDarkCloud");
130 assert_eq!(p.warmup_period(), 2);
131 assert!(!p.is_ready());
132 }
133
134 #[test]
135 fn piercing_line_is_plus_one() {
136 let mut p = PiercingDarkCloud::new();
137 assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), Some(0.0));
140 assert_eq!(p.update(c(9.8, 11.8, 9.5, 11.5, 1)), Some(1.0));
141 }
142
143 #[test]
144 fn dark_cloud_cover_is_minus_one() {
145 let mut p = PiercingDarkCloud::new();
146 assert_eq!(p.update(c(10.0, 12.2, 9.5, 12.0, 0)), Some(0.0));
149 assert_eq!(p.update(c(12.3, 12.4, 10.4, 10.5, 1)), Some(-1.0));
150 }
151
152 #[test]
153 fn close_below_midpoint_is_not_piercing() {
154 let mut p = PiercingDarkCloud::new();
155 p.update(c(12.0, 12.5, 10.0, 10.0, 0));
156 assert_eq!(p.update(c(9.8, 11.0, 9.5, 10.8, 1)), Some(0.0));
158 }
159
160 #[test]
161 fn full_engulf_is_not_piercing() {
162 let mut p = PiercingDarkCloud::new();
163 p.update(c(12.0, 12.5, 10.0, 10.0, 0));
164 assert_eq!(p.update(c(9.8, 13.0, 9.5, 12.5, 1)), Some(0.0));
166 }
167
168 #[test]
169 fn first_bar_returns_zero() {
170 let mut p = PiercingDarkCloud::new();
171 assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), Some(0.0));
172 }
173
174 #[test]
175 fn batch_equals_streaming() {
176 let candles: Vec<Candle> = (0..40)
177 .map(|i| {
178 let base = 100.0 + i as f64;
179 if i % 2 == 0 {
180 c(base + 2.0, base + 2.5, base, base, i)
181 } else {
182 c(base - 0.2, base + 1.8, base - 0.5, base + 1.5, i)
183 }
184 })
185 .collect();
186 let mut a = PiercingDarkCloud::new();
187 let mut b = PiercingDarkCloud::new();
188 assert_eq!(
189 a.batch(&candles),
190 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
191 );
192 }
193
194 #[test]
195 fn reset_clears_state() {
196 let mut p = PiercingDarkCloud::new();
197 p.update(c(12.0, 12.5, 10.0, 10.0, 0));
198 p.update(c(9.8, 11.8, 9.5, 11.5, 1));
199 assert!(p.is_ready());
200 p.reset();
201 assert!(!p.is_ready());
202 assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), Some(0.0));
203 }
204}