fin_primitives/signals/indicators/
hammer_pattern.rs1use crate::error::FinError;
4use crate::signals::{BarInput, Signal, SignalValue};
5use rust_decimal::Decimal;
6
7pub struct HammerPattern {
32 name: String,
33}
34
35impl HammerPattern {
36 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 let is_hammer = lower_shadow >= two * body
70 && upper_shadow <= ratio_30 * range;
71
72 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 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 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 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(); assert!(hp.is_ready());
147 }
148}