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)]
30pub struct HaramiCross {
31 prev: Option<Candle>,
32 last_value: Option<f64>,
33}
34
35impl HaramiCross {
36 #[must_use]
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub const fn value(&self) -> Option<f64> {
44 self.last_value
45 }
46}
47
48impl Indicator for HaramiCross {
49 type Input = Candle;
50 type Output = f64;
51
52 fn update(&mut self, candle: Candle) -> Option<f64> {
53 let Some(prev) = self.prev else {
54 self.prev = Some(candle);
55 self.last_value = Some(0.0);
56 return Some(0.0);
57 };
58 let prev_body_low = prev.open.min(prev.close);
59 let prev_body_high = prev.open.max(prev.close);
60 let prev_is_solid = !is_doji(prev);
61 let curr_is_doji = is_doji(candle);
62 let contained = candle.open >= prev_body_low
63 && candle.open <= prev_body_high
64 && candle.close >= prev_body_low
65 && candle.close <= prev_body_high;
66
67 let v = if prev_is_solid && curr_is_doji && contained {
68 if prev.close < prev.open {
69 1.0
70 } else {
71 -1.0
72 }
73 } else {
74 0.0
75 };
76 self.prev = Some(candle);
77 self.last_value = Some(v);
78 Some(v)
79 }
80
81 fn reset(&mut self) {
82 self.prev = None;
83 self.last_value = None;
84 }
85
86 fn warmup_period(&self) -> usize {
87 2
88 }
89
90 fn is_ready(&self) -> bool {
91 self.last_value.is_some()
92 }
93
94 fn name(&self) -> &'static str {
95 "HaramiCross"
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::traits::BatchExt;
103
104 fn solid(open: f64, close: f64) -> Candle {
105 Candle::new_unchecked(
106 open,
107 open.max(close) + 0.2,
108 open.min(close) - 0.2,
109 close,
110 0.0,
111 0,
112 )
113 }
114
115 fn doji(mid: f64) -> Candle {
116 Candle::new_unchecked(mid, mid + 1.0, mid - 1.0, mid + 0.02, 0.0, 0)
117 }
118
119 #[test]
120 fn accessors_and_metadata() {
121 let h = HaramiCross::new();
122 assert_eq!(h.warmup_period(), 2);
123 assert_eq!(h.name(), "HaramiCross");
124 assert!(!h.is_ready());
125 assert_eq!(h.value(), None);
126 }
127
128 #[test]
129 fn first_bar_seeds_without_signal() {
130 let mut h = HaramiCross::new();
131 assert_eq!(h.update(solid(110.0, 100.0)), Some(0.0));
132 assert!(h.update(doji(105.0)).is_some());
133 }
134
135 #[test]
136 fn bullish_harami_cross() {
137 let mut h = HaramiCross::new();
139 h.update(solid(110.0, 100.0));
140 assert_eq!(h.update(doji(105.0)), Some(1.0));
141 }
142
143 #[test]
144 fn bearish_harami_cross() {
145 let mut h = HaramiCross::new();
147 h.update(solid(100.0, 110.0));
148 assert_eq!(h.update(doji(105.0)), Some(-1.0));
149 }
150
151 #[test]
152 fn doji_outside_body_is_zero() {
153 let mut h = HaramiCross::new();
154 h.update(solid(110.0, 100.0));
155 assert_eq!(h.update(doji(120.0)), Some(0.0));
157 }
158
159 #[test]
160 fn non_doji_second_is_zero() {
161 let mut h = HaramiCross::new();
162 h.update(solid(110.0, 100.0));
163 assert_eq!(h.update(solid(104.0, 106.0)), Some(0.0));
164 }
165
166 #[test]
167 fn reset_clears_state() {
168 let mut h = HaramiCross::new();
169 h.update(solid(110.0, 100.0));
170 h.update(doji(105.0));
171 assert!(h.is_ready());
172 h.reset();
173 assert!(!h.is_ready());
174 assert_eq!(h.update(solid(110.0, 100.0)), Some(0.0));
175 }
176
177 #[test]
178 fn batch_equals_streaming() {
179 let candles: Vec<Candle> = (0..40)
180 .map(|i| {
181 if i % 2 == 0 {
182 solid(110.0, 100.0)
183 } else {
184 doji(105.0)
185 }
186 })
187 .collect();
188 let batch = HaramiCross::new().batch(&candles);
189 let mut b = HaramiCross::new();
190 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
191 assert_eq!(batch, streamed);
192 }
193}