fin_primitives/signals/indicators/
engulfing_pattern.rs1use crate::error::FinError;
4use crate::signals::{BarInput, Signal, SignalValue};
5use rust_decimal::Decimal;
6
7pub struct EngulfingPattern {
33 name: String,
34 prev_open: Option<Decimal>,
35 prev_close: Option<Decimal>,
36}
37
38impl EngulfingPattern {
39 pub fn new(name: impl Into<String>) -> Result<Self, FinError> {
42 Ok(Self { name: name.into(), prev_open: None, prev_close: None })
43 }
44}
45
46impl Signal for EngulfingPattern {
47 fn name(&self) -> &str { &self.name }
48 fn period(&self) -> usize { 2 }
49 fn is_ready(&self) -> bool { self.prev_open.is_some() }
50
51 fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
52 let result = match (self.prev_open, self.prev_close) {
53 (Some(po), Some(pc)) => {
54 let prev_bearish = pc < po;
55 let prev_bullish = pc > po;
56 let curr_bullish = bar.is_bullish();
57 let curr_bearish = bar.is_bearish();
58
59 if prev_bearish && curr_bullish
60 && bar.open <= pc && bar.close >= po
61 {
62 SignalValue::Scalar(Decimal::ONE)
63 } else if prev_bullish && curr_bearish
64 && bar.open >= pc && bar.close <= po
65 {
66 SignalValue::Scalar(Decimal::NEGATIVE_ONE)
67 } else {
68 SignalValue::Scalar(Decimal::ZERO)
69 }
70 }
71 _ => SignalValue::Unavailable,
72 };
73 self.prev_open = Some(bar.open);
74 self.prev_close = Some(bar.close);
75 Ok(result)
76 }
77
78 fn reset(&mut self) {
79 self.prev_open = None;
80 self.prev_close = None;
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::ohlcv::OhlcvBar;
88 use crate::signals::Signal;
89 use crate::types::{NanoTimestamp, Price, Quantity, Symbol};
90 use rust_decimal_macros::dec;
91
92 fn bar(o: &str, h: &str, l: &str, c: &str) -> OhlcvBar {
93 let op = Price::new(o.parse().unwrap()).unwrap();
94 let hp = Price::new(h.parse().unwrap()).unwrap();
95 let lp = Price::new(l.parse().unwrap()).unwrap();
96 let cp = Price::new(c.parse().unwrap()).unwrap();
97 OhlcvBar {
98 symbol: Symbol::new("X").unwrap(),
99 open: op, high: hp, low: lp, close: cp,
100 volume: Quantity::zero(),
101 ts_open: NanoTimestamp::new(0),
102 ts_close: NanoTimestamp::new(1),
103 tick_count: 1,
104 }
105 }
106
107 #[test]
108 fn test_ep_first_bar_unavailable() {
109 let mut ep = EngulfingPattern::new("ep").unwrap();
110 assert_eq!(ep.update_bar(&bar("105","110","95","100")).unwrap(), SignalValue::Unavailable);
111 }
112
113 #[test]
114 fn test_ep_bullish_engulfing() {
115 let mut ep = EngulfingPattern::new("ep").unwrap();
116 ep.update_bar(&bar("105","110","90","95")).unwrap();
118 let r = ep.update_bar(&bar("94","110","90","106")).unwrap();
120 assert_eq!(r, SignalValue::Scalar(dec!(1)));
121 }
122
123 #[test]
124 fn test_ep_bearish_engulfing() {
125 let mut ep = EngulfingPattern::new("ep").unwrap();
126 ep.update_bar(&bar("95","110","90","105")).unwrap();
128 let r = ep.update_bar(&bar("106","110","90","94")).unwrap();
130 assert_eq!(r, SignalValue::Scalar(dec!(-1)));
131 }
132
133 #[test]
134 fn test_ep_no_pattern() {
135 let mut ep = EngulfingPattern::new("ep").unwrap();
136 ep.update_bar(&bar("100","110","90","105")).unwrap();
137 let r = ep.update_bar(&bar("103","110","90","107")).unwrap();
138 assert_eq!(r, SignalValue::Scalar(dec!(0)));
139 }
140
141 #[test]
142 fn test_ep_reset() {
143 let mut ep = EngulfingPattern::new("ep").unwrap();
144 ep.update_bar(&bar("100","110","90","95")).unwrap();
145 assert!(ep.is_ready());
146 ep.reset();
147 assert!(!ep.is_ready());
148 }
149}