Skip to main content

fin_primitives/signals/indicators/
hammer_pattern.rs

1//! Hammer Pattern indicator.
2
3use crate::error::FinError;
4use crate::signals::{BarInput, Signal, SignalValue};
5use rust_decimal::Decimal;
6
7/// Hammer Pattern — detects bullish hammer and bearish hanging man candlestick patterns.
8///
9/// A **hammer** (bullish reversal, output `+1`) has:
10/// - Small body in the upper portion of the range
11/// - Lower shadow ≥ 2× the body size
12/// - Little or no upper shadow (upper shadow ≤ 30% of total range)
13///
14/// A **hanging man** (bearish reversal, output `-1`) has the same shape but
15/// occurs in an uptrend context. Since this indicator is stateless (no trend
16/// context), it outputs `+1` for any hammer-shaped bar and `-1` if inverted
17/// (small body in lower portion, long upper shadow).
18///
19/// Outputs `0` if no pattern is detected.
20///
21/// Always ready from the first bar.
22///
23/// # Example
24/// ```rust
25/// use fin_primitives::signals::indicators::HammerPattern;
26/// use fin_primitives::signals::Signal;
27///
28/// let hp = HammerPattern::new("hp").unwrap();
29/// assert_eq!(hp.period(), 1);
30/// ```
31pub struct HammerPattern {
32    name: String,
33}
34
35impl HammerPattern {
36    /// # Errors
37    /// Never errors — provided for API consistency.
38    pub fn new(name: impl Into<String>) -> Result<Self, FinError> {
39        Ok(Self { name: name.into() })
40    }
41}
42
43impl Signal for HammerPattern {
44    fn name(&self) -> &str { &self.name }
45    fn period(&self) -> usize { 1 }
46    fn is_ready(&self) -> bool { true }
47
48    fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
49        let high  = bar.high;
50        let low   = bar.low;
51        let open  = bar.open;
52        let close = bar.close;
53        let range = high - low;
54
55        if range.is_zero() {
56            return Ok(SignalValue::Scalar(Decimal::ZERO));
57        }
58
59        let body    = (close - open).abs();
60        let body_hi = open.max(close);
61        let body_lo = open.min(close);
62        let upper_shadow = high - body_hi;
63        let lower_shadow = body_lo - low;
64
65        let two  = Decimal::TWO;
66        let ratio_30 = Decimal::new(30, 2);
67
68        // Hammer: long lower shadow, small upper shadow
69        let is_hammer = lower_shadow >= two * body
70            && upper_shadow <= ratio_30 * range;
71
72        // Inverted hammer / shooting star: long upper shadow, small lower shadow
73        let is_inverted = upper_shadow >= two * body
74            && lower_shadow <= ratio_30 * range;
75
76        let signal = if is_hammer { Decimal::ONE }
77            else if is_inverted { Decimal::NEGATIVE_ONE }
78            else { Decimal::ZERO };
79        Ok(SignalValue::Scalar(signal))
80    }
81
82    fn reset(&mut self) {}
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::ohlcv::OhlcvBar;
89    use crate::signals::Signal;
90    use crate::types::{NanoTimestamp, Price, Quantity, Symbol};
91    use rust_decimal_macros::dec;
92
93    fn bar(o: &str, h: &str, l: &str, c: &str) -> OhlcvBar {
94        let op = Price::new(o.parse().unwrap()).unwrap();
95        let hp = Price::new(h.parse().unwrap()).unwrap();
96        let lp = Price::new(l.parse().unwrap()).unwrap();
97        let cp = Price::new(c.parse().unwrap()).unwrap();
98        OhlcvBar {
99            symbol: Symbol::new("X").unwrap(),
100            open: op, high: hp, low: lp, close: cp,
101            volume: Quantity::zero(),
102            ts_open: NanoTimestamp::new(0),
103            ts_close: NanoTimestamp::new(1),
104            tick_count: 1,
105        }
106    }
107
108    #[test]
109    fn test_hp_always_ready() {
110        let hp = HammerPattern::new("hp").unwrap();
111        assert!(hp.is_ready());
112    }
113
114    #[test]
115    fn test_hp_detects_hammer() {
116        // open=98, high=100, low=80, close=99
117        // body=1, range=20, lower=18, upper=1
118        // lower(18) >= 2*body(2) ✓, upper(1) <= 30% of 20(6) ✓
119        let mut hp = HammerPattern::new("hp").unwrap();
120        let r = hp.update_bar(&bar("98","100","80","99")).unwrap();
121        assert_eq!(r, SignalValue::Scalar(dec!(1)));
122    }
123
124    #[test]
125    fn test_hp_detects_inverted_hammer() {
126        // open=100, high=120, low=99, close=101
127        // body=1, range=21, upper=19, lower=1
128        // upper(19) >= 2*body(2) ✓, lower(1) <= 30% of 21(6.3) ✓
129        let mut hp = HammerPattern::new("hp").unwrap();
130        let r = hp.update_bar(&bar("100","120","99","101")).unwrap();
131        assert_eq!(r, SignalValue::Scalar(dec!(-1)));
132    }
133
134    #[test]
135    fn test_hp_no_pattern_full_body() {
136        // Marubozu (all body, no shadows)
137        let mut hp = HammerPattern::new("hp").unwrap();
138        let r = hp.update_bar(&bar("90","110","90","110")).unwrap();
139        assert_eq!(r, SignalValue::Scalar(dec!(0)));
140    }
141
142    #[test]
143    fn test_hp_reset_is_noop() {
144        let mut hp = HammerPattern::new("hp").unwrap();
145        hp.reset(); // no state to reset
146        assert!(hp.is_ready());
147    }
148}