Skip to main content

wickra_core/indicators/
hammer.rs

1//! Hammer candlestick pattern.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// Hammer — a single-bar bullish reversal candidate.
7///
8/// A Hammer has a small real body sitting near the top of the bar, a long
9/// lower shadow at least twice the body, and a short or absent upper shadow.
10/// It is traditionally read as a rejection of lower prices.
11///
12/// ```text
13/// body         = |close − open|
14/// upper_shadow = high − max(open, close)
15/// lower_shadow = min(open, close) − low
16/// hammer       = lower_shadow >= 2 * body
17///               && upper_shadow <= body
18///               && body > 0
19/// ```
20///
21/// Output is `+1.0` when the shape matches, `0.0` otherwise. Pattern-shape
22/// check only — no trend filter is applied; combine with a trend indicator
23/// for actionable signals.
24///
25/// # Example
26///
27/// ```
28/// use wickra_core::{Candle, Hammer, Indicator};
29///
30/// let mut indicator = Hammer::new();
31/// // Open 10, close 10.5, low 5, high 10.6: long lower shadow, tiny upper.
32/// let candle = Candle::new(10.0, 10.6, 5.0, 10.5, 1.0, 0).unwrap();
33/// assert_eq!(indicator.update(candle), Some(1.0));
34/// ```
35#[derive(Debug, Clone, Default)]
36pub struct Hammer {
37    has_emitted: bool,
38}
39
40impl Hammer {
41    /// Construct a new Hammer detector.
42    pub const fn new() -> Self {
43        Self { has_emitted: false }
44    }
45}
46
47impl Indicator for Hammer {
48    type Input = Candle;
49    type Output = f64;
50
51    fn update(&mut self, candle: Candle) -> Option<f64> {
52        self.has_emitted = true;
53        let range = candle.high - candle.low;
54        if range <= 0.0 {
55            return Some(0.0);
56        }
57        let body = (candle.close - candle.open).abs();
58        if body <= 0.0 {
59            return Some(0.0);
60        }
61        let upper = candle.high - candle.open.max(candle.close);
62        let lower = candle.open.min(candle.close) - candle.low;
63        Some(if lower >= 2.0 * body && upper <= body {
64            1.0
65        } else {
66            0.0
67        })
68    }
69
70    fn reset(&mut self) {
71        self.has_emitted = false;
72    }
73
74    fn warmup_period(&self) -> usize {
75        1
76    }
77
78    fn is_ready(&self) -> bool {
79        self.has_emitted
80    }
81
82    fn name(&self) -> &'static str {
83        "Hammer"
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::traits::BatchExt;
91
92    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
93        Candle::new(open, high, low, close, 1.0, ts).unwrap()
94    }
95
96    #[test]
97    fn accessors_and_metadata() {
98        let h = Hammer::new();
99        assert_eq!(h.name(), "Hammer");
100        assert_eq!(h.warmup_period(), 1);
101        assert!(!h.is_ready());
102    }
103
104    #[test]
105    fn clean_hammer_is_one() {
106        let mut h = Hammer::new();
107        // body 0.5 (10 -> 10.5), lower shadow 5.0, upper shadow 0.1.
108        assert_eq!(h.update(c(10.0, 10.6, 5.0, 10.5, 0)), Some(1.0));
109    }
110
111    #[test]
112    fn marubozu_is_not_hammer() {
113        let mut h = Hammer::new();
114        assert_eq!(h.update(c(10.0, 12.0, 10.0, 12.0, 0)), Some(0.0));
115    }
116
117    #[test]
118    fn shooting_star_shape_is_not_hammer() {
119        // Long upper, short lower -> not a hammer.
120        let mut h = Hammer::new();
121        assert_eq!(h.update(c(10.5, 15.0, 10.0, 10.0, 0)), Some(0.0));
122    }
123
124    #[test]
125    fn doji_is_not_hammer() {
126        let mut h = Hammer::new();
127        assert_eq!(h.update(c(10.0, 11.0, 9.0, 10.0, 0)), Some(0.0));
128    }
129
130    #[test]
131    fn zero_range_yields_zero() {
132        let mut h = Hammer::new();
133        assert_eq!(h.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
134    }
135
136    #[test]
137    fn batch_equals_streaming() {
138        let candles: Vec<Candle> = (0..40)
139            .map(|i| {
140                let base = 100.0 + i as f64;
141                c(base, base + 2.0, base - 4.0, base + 0.5, i)
142            })
143            .collect();
144        let mut a = Hammer::new();
145        let mut b = Hammer::new();
146        assert_eq!(
147            a.batch(&candles),
148            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
149        );
150    }
151
152    #[test]
153    fn reset_clears_state() {
154        let mut h = Hammer::new();
155        h.update(c(10.0, 10.6, 5.0, 10.5, 0));
156        assert!(h.is_ready());
157        h.reset();
158        assert!(!h.is_ready());
159    }
160}