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