Skip to main content

wickra_core/indicators/
inverted_hammer.rs

1//! Inverted Hammer candlestick pattern.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// Inverted Hammer — a single-bar bullish reversal candidate.
7///
8/// An Inverted Hammer is the mirror of a Hammer: small real body near the
9/// bottom of the bar, a long upper shadow at least twice the body and a
10/// short or absent lower shadow.
11///
12/// ```text
13/// body         = |close − open|
14/// upper_shadow = high − max(open, close)
15/// lower_shadow = min(open, close) − low
16/// inverted     = upper_shadow >= 2 * body
17///               && lower_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, Indicator, InvertedHammer};
29///
30/// let mut indicator = InvertedHammer::new();
31/// // Open 10, close 10.5, low 9.9, high 15: long upper shadow, tiny lower.
32/// let candle = Candle::new(10.0, 15.0, 9.9, 10.5, 1.0, 0).unwrap();
33/// assert_eq!(indicator.update(candle), Some(1.0));
34/// ```
35#[derive(Debug, Clone, Default)]
36pub struct InvertedHammer {
37    has_emitted: bool,
38}
39
40impl InvertedHammer {
41    /// Construct a new Inverted Hammer detector.
42    pub const fn new() -> Self {
43        Self { has_emitted: false }
44    }
45}
46
47impl Indicator for InvertedHammer {
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 upper >= 2.0 * body && lower <= 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        "InvertedHammer"
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 = InvertedHammer::new();
99        assert_eq!(h.name(), "InvertedHammer");
100        assert_eq!(h.warmup_period(), 1);
101        assert!(!h.is_ready());
102    }
103
104    #[test]
105    fn clean_inverted_hammer_is_one() {
106        let mut h = InvertedHammer::new();
107        // body 0.5, upper shadow 4.5, lower shadow 0.1.
108        assert_eq!(h.update(c(10.0, 15.0, 9.9, 10.5, 0)), Some(1.0));
109    }
110
111    #[test]
112    fn hammer_shape_is_not_inverted() {
113        let mut h = InvertedHammer::new();
114        assert_eq!(h.update(c(10.0, 10.6, 5.0, 10.5, 0)), Some(0.0));
115    }
116
117    #[test]
118    fn doji_is_not_inverted_hammer() {
119        let mut h = InvertedHammer::new();
120        assert_eq!(h.update(c(10.0, 11.0, 9.0, 10.0, 0)), Some(0.0));
121    }
122
123    #[test]
124    fn zero_range_yields_zero() {
125        let mut h = InvertedHammer::new();
126        assert_eq!(h.update(c(10.0, 10.0, 10.0, 10.0, 0)), Some(0.0));
127    }
128
129    #[test]
130    fn batch_equals_streaming() {
131        let candles: Vec<Candle> = (0..40)
132            .map(|i| {
133                let base = 100.0 + i as f64;
134                c(base, base + 4.0, base - 0.1, base + 0.5, i)
135            })
136            .collect();
137        let mut a = InvertedHammer::new();
138        let mut b = InvertedHammer::new();
139        assert_eq!(
140            a.batch(&candles),
141            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
142        );
143    }
144
145    #[test]
146    fn reset_clears_state() {
147        let mut h = InvertedHammer::new();
148        h.update(c(10.0, 15.0, 9.9, 10.5, 0));
149        assert!(h.is_ready());
150        h.reset();
151        assert!(!h.is_ready());
152    }
153}