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