use crate::error::FinError;
use crate::signals::{BarInput, Signal, SignalValue};
use rust_decimal::Decimal;
pub struct CandlePattern {
name: String,
prev: Option<BarInput>,
}
impl CandlePattern {
pub fn new(name: impl Into<String>) -> Result<Self, FinError> {
Ok(Self { name: name.into(), prev: None })
}
}
impl Signal for CandlePattern {
fn name(&self) -> &str { &self.name }
fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
let range = bar.range();
let body = bar.body_size();
let prev = self.prev.replace(*bar);
let signal = if let Some(p) = prev {
let prev_body = (p.close - p.open).abs();
let curr_bull = bar.is_bullish();
let curr_bear = bar.is_bearish();
let prev_bull = p.close > p.open;
let prev_bear = p.close < p.open;
if curr_bull && prev_bear
&& bar.open <= p.close
&& bar.close >= p.open
&& body > prev_body
{
Decimal::from(2i32) } else if curr_bear && prev_bull
&& bar.open >= p.close
&& bar.close <= p.open
&& body > prev_body
{
Decimal::from(-2i32) } else {
let upper_wick = bar.upper_wick();
let lower_wick = bar.lower_wick();
let body_pct = if range.is_zero() { Decimal::ZERO } else { body / range };
if range.is_zero() {
Decimal::ZERO } else if body_pct < Decimal::new(3, 1) && lower_wick >= body * Decimal::TWO && upper_wick <= body && lower_wick > Decimal::ZERO {
Decimal::ONE } else if body_pct < Decimal::new(3, 1) && upper_wick >= body * Decimal::TWO && lower_wick <= body && upper_wick > Decimal::ZERO {
-Decimal::ONE } else {
Decimal::ZERO
}
}
} else {
Decimal::ZERO
};
if prev.is_none() {
return Ok(SignalValue::Unavailable);
}
Ok(SignalValue::Scalar(signal))
}
fn is_ready(&self) -> bool {
self.prev.is_some()
}
fn period(&self) -> usize {
2
}
fn reset(&mut self) {
self.prev = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ohlcv::OhlcvBar;
use crate::types::{NanoTimestamp, Price, Quantity, Symbol};
use rust_decimal_macros::dec;
fn bar_ohlc(o: &str, h: &str, l: &str, c: &str) -> OhlcvBar {
OhlcvBar {
symbol: Symbol::new("X").unwrap(),
open: Price::new(o.parse().unwrap()).unwrap(),
high: Price::new(h.parse().unwrap()).unwrap(),
low: Price::new(l.parse().unwrap()).unwrap(),
close: Price::new(c.parse().unwrap()).unwrap(),
volume: Quantity::zero(),
ts_open: NanoTimestamp::new(0),
ts_close: NanoTimestamp::new(1),
tick_count: 1,
}
}
fn flat_bar(c: &str) -> OhlcvBar { bar_ohlc(c, c, c, c) }
#[test]
fn test_candle_first_bar_unavailable() {
let mut cp = CandlePattern::new("cp").unwrap();
assert_eq!(cp.update_bar(&flat_bar("100")).unwrap(), SignalValue::Unavailable);
}
#[test]
fn test_candle_flat_is_zero() {
let mut cp = CandlePattern::new("cp").unwrap();
cp.update_bar(&flat_bar("100")).unwrap();
if let SignalValue::Scalar(v) = cp.update_bar(&flat_bar("100")).unwrap() {
assert_eq!(v, dec!(0));
} else { panic!("expected Scalar"); }
}
#[test]
fn test_bullish_engulfing() {
let mut cp = CandlePattern::new("cp").unwrap();
cp.update_bar(&bar_ohlc("110", "110", "100", "100")).unwrap();
if let SignalValue::Scalar(v) = cp.update_bar(&bar_ohlc("99", "111", "99", "111")).unwrap() {
assert_eq!(v, dec!(2), "bullish engulfing should be +2: {v}");
} else { panic!("expected Scalar"); }
}
#[test]
fn test_bearish_engulfing() {
let mut cp = CandlePattern::new("cp").unwrap();
cp.update_bar(&bar_ohlc("100", "110", "100", "110")).unwrap();
if let SignalValue::Scalar(v) = cp.update_bar(&bar_ohlc("111", "111", "99", "99")).unwrap() {
assert_eq!(v, dec!(-2), "bearish engulfing should be -2: {v}");
} else { panic!("expected Scalar"); }
}
#[test]
fn test_reset() {
let mut cp = CandlePattern::new("cp").unwrap();
cp.update_bar(&flat_bar("100")).unwrap();
assert!(cp.is_ready());
cp.reset();
assert!(!cp.is_ready());
assert_eq!(cp.update_bar(&flat_bar("100")).unwrap(), SignalValue::Unavailable);
}
}