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 fn update(&mut self, candle: Candle) -> Option<f64> {
68 let Some(prev) = self.prev else {
69 self.prev = Some(candle);
70 self.last_value = Some(0.0);
71 return Some(0.0);
72 };
73 let prev_body_low = prev.open.min(prev.close);
74 let prev_body_high = prev.open.max(prev.close);
75 let prev_is_solid = !is_doji(prev);
76 let curr_is_doji = is_doji(candle);
77 let contained = candle.open >= prev_body_low
78 && candle.open <= prev_body_high
79 && candle.close >= prev_body_low
80 && candle.close <= prev_body_high;
81
82 let v = if prev_is_solid && curr_is_doji && contained {
83 if prev.close < prev.open {
84 1.0
85 } else {
86 -1.0
87 }
88 } else {
89 0.0
90 };
91 self.prev = Some(candle);
92 self.last_value = Some(v);
93 Some(v)
94 }
95
96 fn reset(&mut self) {
97 self.prev = None;
98 self.last_value = None;
99 }
100
101 fn warmup_period(&self) -> usize {
102 2
103 }
104
105 fn is_ready(&self) -> bool {
106 self.last_value.is_some()
107 }
108
109 fn name(&self) -> &'static str {
110 "HaramiCross"
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::traits::BatchExt;
118
119 fn solid(open: f64, close: f64) -> Candle {
120 Candle::new_unchecked(
121 open,
122 open.max(close) + 0.2,
123 open.min(close) - 0.2,
124 close,
125 0.0,
126 0,
127 )
128 }
129
130 fn doji(mid: f64) -> Candle {
131 Candle::new_unchecked(mid, mid + 1.0, mid - 1.0, mid + 0.02, 0.0, 0)
132 }
133
134 #[test]
135 fn accessors_and_metadata() {
136 let h = HaramiCross::new();
137 assert_eq!(h.warmup_period(), 2);
138 assert_eq!(h.name(), "HaramiCross");
139 assert!(!h.is_ready());
140 assert_eq!(h.value(), None);
141 }
142
143 #[test]
144 fn first_bar_seeds_without_signal() {
145 let mut h = HaramiCross::new();
146 assert_eq!(h.update(solid(110.0, 100.0)), Some(0.0));
147 assert!(h.update(doji(105.0)).is_some());
148 }
149
150 #[test]
151 fn bullish_harami_cross() {
152 let mut h = HaramiCross::new();
154 h.update(solid(110.0, 100.0));
155 assert_eq!(h.update(doji(105.0)), Some(1.0));
156 }
157
158 #[test]
159 fn bearish_harami_cross() {
160 let mut h = HaramiCross::new();
162 h.update(solid(100.0, 110.0));
163 assert_eq!(h.update(doji(105.0)), Some(-1.0));
164 }
165
166 #[test]
167 fn doji_outside_body_is_zero() {
168 let mut h = HaramiCross::new();
169 h.update(solid(110.0, 100.0));
170 assert_eq!(h.update(doji(120.0)), Some(0.0));
172 }
173
174 #[test]
175 fn non_doji_second_is_zero() {
176 let mut h = HaramiCross::new();
177 h.update(solid(110.0, 100.0));
178 assert_eq!(h.update(solid(104.0, 106.0)), Some(0.0));
179 }
180
181 #[test]
182 fn reset_clears_state() {
183 let mut h = HaramiCross::new();
184 h.update(solid(110.0, 100.0));
185 h.update(doji(105.0));
186 assert!(h.is_ready());
187 h.reset();
188 assert!(!h.is_ready());
189 assert_eq!(h.update(solid(110.0, 100.0)), Some(0.0));
190 }
191
192 #[test]
193 fn batch_equals_streaming() {
194 let candles: Vec<Candle> = (0..40)
195 .map(|i| {
196 if i % 2 == 0 {
197 solid(110.0, 100.0)
198 } else {
199 doji(105.0)
200 }
201 })
202 .collect();
203 let batch = HaramiCross::new().batch(&candles);
204 let mut b = HaramiCross::new();
205 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
206 assert_eq!(batch, streamed);
207 }
208}