fin_primitives/signals/indicators/
candle_pattern.rs1use crate::error::FinError;
4use crate::signals::{BarInput, Signal, SignalValue};
5use rust_decimal::Decimal;
6
7pub struct CandlePattern {
27 name: String,
28 prev: Option<BarInput>,
29}
30
31impl CandlePattern {
32 pub fn new(name: impl Into<String>) -> Result<Self, FinError> {
37 Ok(Self { name: name.into(), prev: None })
38 }
39}
40
41impl Signal for CandlePattern {
42 fn name(&self) -> &str { &self.name }
43
44 fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
45 let range = bar.range();
46 let body = bar.body_size();
47
48 let prev = self.prev.replace(*bar);
49
50 let signal = if let Some(p) = prev {
51 let prev_body = (p.close - p.open).abs();
53 let curr_bull = bar.is_bullish();
54 let curr_bear = bar.is_bearish();
55 let prev_bull = p.close > p.open;
56 let prev_bear = p.close < p.open;
57
58 if curr_bull && prev_bear
59 && bar.open <= p.close
60 && bar.close >= p.open
61 && body > prev_body
62 {
63 Decimal::from(2i32) } else if curr_bear && prev_bull
65 && bar.open >= p.close
66 && bar.close <= p.open
67 && body > prev_body
68 {
69 Decimal::from(-2i32) } else {
71 let upper_wick = bar.upper_wick();
73 let lower_wick = bar.lower_wick();
74 let body_pct = if range.is_zero() { Decimal::ZERO } else { body / range };
75
76 if range.is_zero() {
77 Decimal::ZERO } else if body_pct < Decimal::new(3, 1) && lower_wick >= body * Decimal::TWO && upper_wick <= body && lower_wick > Decimal::ZERO {
79 Decimal::ONE } else if body_pct < Decimal::new(3, 1) && upper_wick >= body * Decimal::TWO && lower_wick <= body && upper_wick > Decimal::ZERO {
81 -Decimal::ONE } else {
83 Decimal::ZERO
84 }
85 }
86 } else {
87 Decimal::ZERO
88 };
89
90 if prev.is_none() {
91 return Ok(SignalValue::Unavailable);
92 }
93
94 Ok(SignalValue::Scalar(signal))
95 }
96
97 fn is_ready(&self) -> bool {
98 self.prev.is_some()
99 }
100
101 fn period(&self) -> usize {
102 2
103 }
104
105 fn reset(&mut self) {
106 self.prev = None;
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::ohlcv::OhlcvBar;
114 use crate::types::{NanoTimestamp, Price, Quantity, Symbol};
115 use rust_decimal_macros::dec;
116
117 fn bar_ohlc(o: &str, h: &str, l: &str, c: &str) -> OhlcvBar {
118 OhlcvBar {
119 symbol: Symbol::new("X").unwrap(),
120 open: Price::new(o.parse().unwrap()).unwrap(),
121 high: Price::new(h.parse().unwrap()).unwrap(),
122 low: Price::new(l.parse().unwrap()).unwrap(),
123 close: Price::new(c.parse().unwrap()).unwrap(),
124 volume: Quantity::zero(),
125 ts_open: NanoTimestamp::new(0),
126 ts_close: NanoTimestamp::new(1),
127 tick_count: 1,
128 }
129 }
130
131 fn flat_bar(c: &str) -> OhlcvBar { bar_ohlc(c, c, c, c) }
132
133 #[test]
134 fn test_candle_first_bar_unavailable() {
135 let mut cp = CandlePattern::new("cp").unwrap();
136 assert_eq!(cp.update_bar(&flat_bar("100")).unwrap(), SignalValue::Unavailable);
137 }
138
139 #[test]
140 fn test_candle_flat_is_zero() {
141 let mut cp = CandlePattern::new("cp").unwrap();
142 cp.update_bar(&flat_bar("100")).unwrap();
143 if let SignalValue::Scalar(v) = cp.update_bar(&flat_bar("100")).unwrap() {
144 assert_eq!(v, dec!(0));
145 } else { panic!("expected Scalar"); }
146 }
147
148 #[test]
149 fn test_bullish_engulfing() {
150 let mut cp = CandlePattern::new("cp").unwrap();
151 cp.update_bar(&bar_ohlc("110", "110", "100", "100")).unwrap();
153 if let SignalValue::Scalar(v) = cp.update_bar(&bar_ohlc("99", "111", "99", "111")).unwrap() {
155 assert_eq!(v, dec!(2), "bullish engulfing should be +2: {v}");
156 } else { panic!("expected Scalar"); }
157 }
158
159 #[test]
160 fn test_bearish_engulfing() {
161 let mut cp = CandlePattern::new("cp").unwrap();
162 cp.update_bar(&bar_ohlc("100", "110", "100", "110")).unwrap();
164 if let SignalValue::Scalar(v) = cp.update_bar(&bar_ohlc("111", "111", "99", "99")).unwrap() {
166 assert_eq!(v, dec!(-2), "bearish engulfing should be -2: {v}");
167 } else { panic!("expected Scalar"); }
168 }
169
170 #[test]
171 fn test_reset() {
172 let mut cp = CandlePattern::new("cp").unwrap();
173 cp.update_bar(&flat_bar("100")).unwrap();
174 assert!(cp.is_ready());
175 cp.reset();
176 assert!(!cp.is_ready());
177 assert_eq!(cp.update_bar(&flat_bar("100")).unwrap(), SignalValue::Unavailable);
178 }
179}