wickra_core/indicators/
harami_cross.rs1#![allow(clippy::doc_markdown)]
2
3use crate::ohlcv::Candle;
20use crate::traits::Indicator;
21
22fn is_doji(candle: Candle) -> bool {
23 let body = (candle.close - candle.open).abs();
24 let range = candle.high - candle.low;
25 range > 0.0 && body <= 0.1 * range
26}
27
28#[derive(Debug, Clone, Default)]
45pub struct HaramiCross {
46 prev: Option<Candle>,
47 last_value: Option<f64>,
48}
49
50impl HaramiCross {
51 #[must_use]
53 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub const fn value(&self) -> Option<f64> {
59 self.last_value
60 }
61}
62
63impl Indicator for HaramiCross {
64 type Input = Candle;
65 type Output = f64;
66
67 #[inline]
68 fn update(&mut self, candle: Candle) -> Option<f64> {
69 let Some(prev) = self.prev else {
70 self.prev = Some(candle);
71 self.last_value = None;
72 return None;
73 };
74 let prev_body_low = prev.open.min(prev.close);
75 let prev_body_high = prev.open.max(prev.close);
76 let prev_is_solid = !is_doji(prev);
77 let curr_is_doji = is_doji(candle);
78 let contained = candle.open >= prev_body_low
79 && candle.open <= prev_body_high
80 && candle.close >= prev_body_low
81 && candle.close <= prev_body_high;
82
83 let v = if prev_is_solid && curr_is_doji && contained {
84 if prev.close < prev.open {
85 1.0
86 } else {
87 -1.0
88 }
89 } else {
90 0.0
91 };
92 self.prev = Some(candle);
93 self.last_value = Some(v);
94 Some(v)
95 }
96
97 fn reset(&mut self) {
98 self.prev = None;
99 self.last_value = None;
100 }
101
102 #[inline]
103 fn warmup_period(&self) -> usize {
104 2
105 }
106
107 #[inline]
108 fn is_ready(&self) -> bool {
109 self.last_value.is_some()
110 }
111
112 #[inline]
113 fn name(&self) -> &'static str {
114 "HaramiCross"
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::traits::BatchExt;
122
123 fn solid(open: f64, close: f64) -> Candle {
124 Candle::new_unchecked(
125 open,
126 open.max(close) + 0.2,
127 open.min(close) - 0.2,
128 close,
129 0.0,
130 0,
131 )
132 }
133
134 fn doji(mid: f64) -> Candle {
135 Candle::new_unchecked(mid, mid + 1.0, mid - 1.0, mid + 0.02, 0.0, 0)
136 }
137
138 #[test]
139 fn accessors_and_metadata() {
140 let h = HaramiCross::new();
141 assert_eq!(h.warmup_period(), 2);
142 assert_eq!(h.name(), "HaramiCross");
143 assert!(!h.is_ready());
144 assert_eq!(h.value(), None);
145 }
146
147 #[test]
148 fn first_bar_seeds_without_signal() {
149 let mut h = HaramiCross::new();
150 assert_eq!(h.update(solid(110.0, 100.0)), None);
151 assert!(h.update(doji(105.0)).is_some());
152 }
153
154 #[test]
155 fn bullish_harami_cross() {
156 let mut h = HaramiCross::new();
158 h.update(solid(110.0, 100.0));
159 assert_eq!(h.update(doji(105.0)), Some(1.0));
160 }
161
162 #[test]
163 fn bearish_harami_cross() {
164 let mut h = HaramiCross::new();
166 h.update(solid(100.0, 110.0));
167 assert_eq!(h.update(doji(105.0)), Some(-1.0));
168 }
169
170 #[test]
171 fn doji_outside_body_is_zero() {
172 let mut h = HaramiCross::new();
173 h.update(solid(110.0, 100.0));
174 assert_eq!(h.update(doji(120.0)), Some(0.0));
176 }
177
178 #[test]
179 fn non_doji_second_is_zero() {
180 let mut h = HaramiCross::new();
181 h.update(solid(110.0, 100.0));
182 assert_eq!(h.update(solid(104.0, 106.0)), Some(0.0));
183 }
184
185 #[test]
186 fn reset_clears_state() {
187 let mut h = HaramiCross::new();
188 h.update(solid(110.0, 100.0));
189 h.update(doji(105.0));
190 assert!(h.is_ready());
191 h.reset();
192 assert!(!h.is_ready());
193 assert_eq!(h.update(solid(110.0, 100.0)), None);
194 }
195
196 #[test]
197 fn batch_equals_streaming() {
198 let candles: Vec<Candle> = (0..40)
199 .map(|i| {
200 if i % 2 == 0 {
201 solid(110.0, 100.0)
202 } else {
203 doji(105.0)
204 }
205 })
206 .collect();
207 let batch = HaramiCross::new().batch(&candles);
208 let mut b = HaramiCross::new();
209 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
210 assert_eq!(batch, streamed);
211 }
212}