use crate::Candle;
use serde::{Deserialize, Serialize};
const DOJI_BODY_RATIO: f64 = 0.05;
const SPINNING_TOP_BODY_RATIO: f64 = 0.30;
const MARUBOZU_BODY_RATIO: f64 = 0.90;
const LONG_WICK_RATIO: f64 = 2.0;
const SHORT_WICK_RATIO: f64 = 0.50;
const TREND_LOOKBACK: usize = 3;
const TWEEZER_TOLERANCE: f64 = 0.001;
const MIN_BODY: f64 = 1e-9;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum PatternSentiment {
Bullish,
Bearish,
Neutral,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CandlePattern {
MorningStar,
EveningStar,
ThreeWhiteSoldiers,
ThreeBlackCrows,
BullishEngulfing,
BearishEngulfing,
BullishHarami,
BearishHarami,
PiercingLine,
DarkCloudCover,
TweezerTop,
TweezerBottom,
Doji,
SpinningTop,
BullishMarubozu,
BearishMarubozu,
Hammer,
InvertedHammer,
HangingMan,
ShootingStar,
}
impl CandlePattern {
pub fn sentiment(self) -> PatternSentiment {
match self {
Self::MorningStar
| Self::ThreeWhiteSoldiers
| Self::BullishEngulfing
| Self::BullishHarami
| Self::PiercingLine
| Self::TweezerBottom
| Self::BullishMarubozu
| Self::Hammer
| Self::InvertedHammer => PatternSentiment::Bullish,
Self::EveningStar
| Self::ThreeBlackCrows
| Self::BearishEngulfing
| Self::BearishHarami
| Self::DarkCloudCover
| Self::TweezerTop
| Self::BearishMarubozu
| Self::HangingMan
| Self::ShootingStar => PatternSentiment::Bearish,
Self::Doji | Self::SpinningTop => PatternSentiment::Neutral,
}
}
}
#[inline]
fn body(c: &Candle) -> f64 {
(c.close - c.open).abs()
}
#[inline]
fn range(c: &Candle) -> f64 {
c.high - c.low
}
#[inline]
fn upper_wick(c: &Candle) -> f64 {
c.high - c.open.max(c.close)
}
#[inline]
fn lower_wick(c: &Candle) -> f64 {
c.open.min(c.close) - c.low
}
#[inline]
fn is_bullish(c: &Candle) -> bool {
c.close > c.open
}
#[inline]
fn is_bearish(c: &Candle) -> bool {
c.close < c.open
}
#[inline]
fn body_mid(c: &Candle) -> f64 {
(c.open + c.close) / 2.0
}
#[inline]
fn prior_downtrend(candles: &[Candle], i: usize) -> bool {
i > TREND_LOOKBACK && candles[i - 1 - TREND_LOOKBACK].close > candles[i - 1].close
}
#[inline]
fn prior_uptrend(candles: &[Candle], i: usize) -> bool {
i > TREND_LOOKBACK && candles[i - 1 - TREND_LOOKBACK].close < candles[i - 1].close
}
fn is_doji(c: &Candle) -> bool {
let r = range(c);
r == 0.0 || body(c) <= r * DOJI_BODY_RATIO
}
fn is_spinning_top(c: &Candle) -> bool {
let r = range(c);
let b = body(c);
!is_doji(c)
&& r > 0.0
&& b <= r * SPINNING_TOP_BODY_RATIO
&& upper_wick(c) >= b
&& lower_wick(c) >= b
}
fn is_bullish_marubozu(c: &Candle) -> bool {
let r = range(c);
r > 0.0 && is_bullish(c) && body(c) >= r * MARUBOZU_BODY_RATIO
}
fn is_bearish_marubozu(c: &Candle) -> bool {
let r = range(c);
r > 0.0 && is_bearish(c) && body(c) >= r * MARUBOZU_BODY_RATIO
}
fn is_hammer_shape(c: &Candle) -> bool {
let b = body(c).max(MIN_BODY);
range(c) > 0.0 && lower_wick(c) >= b * LONG_WICK_RATIO && upper_wick(c) <= b * SHORT_WICK_RATIO
}
fn is_inverted_hammer_shape(c: &Candle) -> bool {
let b = body(c).max(MIN_BODY);
range(c) > 0.0 && upper_wick(c) >= b * LONG_WICK_RATIO && lower_wick(c) <= b * SHORT_WICK_RATIO
}
fn detect_three_bar(candles: &[Candle], i: usize) -> Option<CandlePattern> {
if i < 2 {
return None;
}
let (a, b, c) = (&candles[i - 2], &candles[i - 1], &candles[i]);
if is_bullish(a)
&& is_bullish(b)
&& is_bullish(c)
&& b.close > a.close
&& c.close > b.close
&& b.open > a.open
&& b.open < a.close
&& c.open > b.open
&& c.open < b.close
{
return Some(CandlePattern::ThreeWhiteSoldiers);
}
if is_bearish(a)
&& is_bearish(b)
&& is_bearish(c)
&& b.close < a.close
&& c.close < b.close
&& b.open < a.open
&& b.open > a.close
&& c.open < b.open
&& c.open > b.close
{
return Some(CandlePattern::ThreeBlackCrows);
}
let b_is_small = body(b) <= range(b).max(MIN_BODY) * SPINNING_TOP_BODY_RATIO;
if is_bearish(a)
&& body(a) >= range(a) * 0.5
&& b_is_small
&& b.open.max(b.close) <= a.close
&& is_bullish(c)
&& c.close > body_mid(a)
{
return Some(CandlePattern::MorningStar);
}
if is_bullish(a)
&& body(a) >= range(a) * 0.5
&& b_is_small
&& b.open.min(b.close) >= a.close
&& is_bearish(c)
&& c.close < body_mid(a)
{
return Some(CandlePattern::EveningStar);
}
None
}
fn detect_two_bar(candles: &[Candle], i: usize) -> Option<CandlePattern> {
if i < 1 {
return None;
}
let (prev, curr) = (&candles[i - 1], &candles[i]);
if (curr.high - prev.high).abs() <= prev.high * TWEEZER_TOLERANCE
&& is_bullish(prev)
&& is_bearish(curr)
{
return Some(CandlePattern::TweezerTop);
}
if (curr.low - prev.low).abs() <= prev.low * TWEEZER_TOLERANCE
&& is_bearish(prev)
&& is_bullish(curr)
{
return Some(CandlePattern::TweezerBottom);
}
if is_bearish(prev)
&& is_bullish(curr)
&& curr.open <= prev.close
&& curr.close >= prev.open
&& body(curr) > body(prev)
{
return Some(CandlePattern::BullishEngulfing);
}
if is_bullish(prev)
&& is_bearish(curr)
&& curr.open >= prev.close
&& curr.close <= prev.open
&& body(curr) > body(prev)
{
return Some(CandlePattern::BearishEngulfing);
}
let curr_hi = curr.open.max(curr.close);
let curr_lo = curr.open.min(curr.close);
if is_bearish(prev) && curr_lo >= prev.close && curr_hi <= prev.open && body(curr) < body(prev)
{
return Some(CandlePattern::BullishHarami);
}
if is_bullish(prev) && curr_lo >= prev.open && curr_hi <= prev.close && body(curr) < body(prev)
{
return Some(CandlePattern::BearishHarami);
}
if is_bearish(prev)
&& is_bullish(curr)
&& curr.open < prev.close
&& curr.close > body_mid(prev)
&& curr.close < prev.open
{
return Some(CandlePattern::PiercingLine);
}
if is_bullish(prev)
&& is_bearish(curr)
&& curr.open > prev.close
&& curr.close < body_mid(prev)
&& curr.close > prev.open
{
return Some(CandlePattern::DarkCloudCover);
}
None
}
fn detect_one_bar(candles: &[Candle], i: usize) -> Option<CandlePattern> {
let c = &candles[i];
if is_doji(c) {
return Some(CandlePattern::Doji);
}
if is_bullish_marubozu(c) {
return Some(CandlePattern::BullishMarubozu);
}
if is_bearish_marubozu(c) {
return Some(CandlePattern::BearishMarubozu);
}
if is_hammer_shape(c) {
if prior_downtrend(candles, i) {
return Some(CandlePattern::Hammer);
}
if prior_uptrend(candles, i) {
return Some(CandlePattern::HangingMan);
}
}
if is_inverted_hammer_shape(c) {
if prior_downtrend(candles, i) {
return Some(CandlePattern::InvertedHammer);
}
if prior_uptrend(candles, i) {
return Some(CandlePattern::ShootingStar);
}
}
if is_spinning_top(c) {
return Some(CandlePattern::SpinningTop);
}
None
}
pub fn patterns(candles: &[Candle]) -> Vec<Option<CandlePattern>> {
candles
.iter()
.enumerate()
.map(|(i, _)| {
detect_three_bar(candles, i)
.or_else(|| detect_two_bar(candles, i))
.or_else(|| detect_one_bar(candles, i))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn c(o: f64, h: f64, l: f64, close: f64) -> Candle {
Candle {
timestamp: 0,
open: o,
high: h,
low: l,
close,
volume: 0,
adj_close: None,
}
}
#[test]
fn test_empty_input_returns_empty() {
assert!(patterns(&[]).is_empty());
}
#[test]
fn test_output_length_matches_input() {
let candles: Vec<Candle> = (0..15)
.map(|i| c(i as f64 + 0.5, i as f64 + 1.0, i as f64, i as f64 + 0.6))
.collect();
assert_eq!(patterns(&candles).len(), candles.len());
}
#[test]
fn test_doji_detected() {
let candles = [c(10.0, 12.0, 8.0, 10.1)];
assert_eq!(patterns(&candles)[0], Some(CandlePattern::Doji));
}
#[test]
fn test_four_price_doji() {
let candles = [c(10.0, 10.0, 10.0, 10.0)];
assert_eq!(patterns(&candles)[0], Some(CandlePattern::Doji));
}
#[test]
fn test_doji_not_on_normal_candle() {
let candles = [c(10.0, 12.0, 9.0, 11.5)];
assert_ne!(patterns(&candles)[0], Some(CandlePattern::Doji));
}
#[test]
fn test_bullish_marubozu() {
let candles = [c(10.0, 20.05, 9.95, 20.0)];
assert_eq!(patterns(&candles)[0], Some(CandlePattern::BullishMarubozu));
}
#[test]
fn test_bearish_marubozu() {
let candles = [c(20.0, 20.05, 9.95, 10.0)];
assert_eq!(patterns(&candles)[0], Some(CandlePattern::BearishMarubozu));
}
#[test]
fn test_hammer_in_downtrend() {
let prior = [
c(16.0, 17.0, 15.0, 16.0),
c(15.5, 16.0, 14.5, 15.5),
c(15.0, 15.5, 13.5, 14.5),
c(14.0, 14.5, 12.5, 13.5),
];
let hammer = c(12.0, 13.5, 4.5, 13.0);
let mut candles = prior.to_vec();
candles.push(hammer);
assert_eq!(patterns(&candles)[4], Some(CandlePattern::Hammer));
}
#[test]
fn test_shooting_star_in_uptrend() {
let prior = [
c(7.0, 8.0, 6.5, 7.5),
c(7.5, 8.5, 7.0, 8.0),
c(8.0, 9.0, 7.5, 8.5),
c(8.5, 9.5, 8.0, 9.5),
];
let star = c(9.5, 18.5, 9.0, 10.5);
let mut candles = prior.to_vec();
candles.push(star);
assert_eq!(patterns(&candles)[4], Some(CandlePattern::ShootingStar));
}
#[test]
fn test_bullish_engulfing() {
let prev = c(11.0, 11.5, 9.5, 10.0); let curr = c(9.8, 12.0, 9.7, 11.2); let result = patterns(&[prev, curr]);
assert_eq!(result[1], Some(CandlePattern::BullishEngulfing));
}
#[test]
fn test_bearish_engulfing() {
let prev = c(10.0, 11.5, 9.5, 11.0); let curr = c(11.2, 11.3, 9.0, 9.5); let result = patterns(&[prev, curr]);
assert_eq!(result[1], Some(CandlePattern::BearishEngulfing));
}
#[test]
fn test_bullish_harami() {
let prev = c(12.0, 12.5, 9.0, 10.0); let curr = c(10.5, 11.0, 10.4, 10.8); let result = patterns(&[prev, curr]);
assert_eq!(result[1], Some(CandlePattern::BullishHarami));
}
#[test]
fn test_bearish_harami() {
let prev = c(10.0, 12.5, 9.0, 12.0); let curr = c(11.5, 12.0, 11.2, 11.3); let result = patterns(&[prev, curr]);
assert_eq!(result[1], Some(CandlePattern::BearishHarami));
}
#[test]
fn test_bullish_harami_cross_doji_inside() {
let prev = c(12.0, 12.5, 9.0, 10.0); let doji = c(11.0, 11.5, 10.5, 11.05); let result = patterns(&[prev, doji]);
assert_eq!(result[1], Some(CandlePattern::BullishHarami));
}
#[test]
fn test_bearish_harami_cross_doji_inside() {
let prev = c(10.0, 12.5, 9.0, 12.0); let doji = c(11.0, 11.5, 10.5, 11.05); let result = patterns(&[prev, doji]);
assert_eq!(result[1], Some(CandlePattern::BearishHarami));
}
#[test]
fn test_piercing_line() {
let prev = c(14.0, 15.0, 9.0, 10.0);
let curr = c(9.5, 13.0, 9.4, 12.5); let result = patterns(&[prev, curr]);
assert_eq!(result[1], Some(CandlePattern::PiercingLine));
}
#[test]
fn test_dark_cloud_cover() {
let prev = c(10.0, 15.0, 9.0, 14.0);
let curr = c(14.5, 16.0, 10.5, 11.5); let result = patterns(&[prev, curr]);
assert_eq!(result[1], Some(CandlePattern::DarkCloudCover));
}
#[test]
fn test_tweezer_top() {
let prev = c(10.0, 12.0, 9.5, 11.5); let curr = c(11.6, 12.0, 10.8, 11.0); let result = patterns(&[prev, curr]);
assert_eq!(result[1], Some(CandlePattern::TweezerTop));
}
#[test]
fn test_tweezer_bottom() {
let prev = c(11.5, 12.0, 9.5, 10.0); let curr = c(9.8, 11.0, 9.5, 10.5); let result = patterns(&[prev, curr]);
assert_eq!(result[1], Some(CandlePattern::TweezerBottom));
}
#[test]
fn test_three_white_soldiers() {
let c1 = c(10.0, 11.2, 9.8, 11.0);
let c2 = c(10.5, 12.2, 10.4, 12.0); let c3 = c(11.5, 13.2, 11.4, 13.0); let result = patterns(&[c1, c2, c3]);
assert_eq!(result[2], Some(CandlePattern::ThreeWhiteSoldiers));
}
#[test]
fn test_three_black_crows() {
let c1 = c(13.0, 13.2, 11.8, 12.0);
let c2 = c(12.5, 12.6, 10.8, 11.0); let c3 = c(11.5, 11.6, 9.8, 10.0); let result = patterns(&[c1, c2, c3]);
assert_eq!(result[2], Some(CandlePattern::ThreeBlackCrows));
}
#[test]
fn test_morning_star() {
let a = c(110.0, 112.0, 100.0, 102.0);
let b = c(100.5, 101.0, 99.5, 100.8); let cc = c(101.0, 112.0, 100.0, 108.0); let result = patterns(&[a, b, cc]);
assert_eq!(result[2], Some(CandlePattern::MorningStar));
}
#[test]
fn test_evening_star() {
let a = c(100.0, 111.0, 99.0, 110.0);
let b = c(111.0, 112.5, 110.8, 111.3); let cc = c(110.5, 111.0, 102.0, 103.0); let result = patterns(&[a, b, cc]);
assert_eq!(result[2], Some(CandlePattern::EveningStar));
}
#[test]
fn test_sentiment_coverage() {
assert_eq!(CandlePattern::Hammer.sentiment(), PatternSentiment::Bullish);
assert_eq!(
CandlePattern::MorningStar.sentiment(),
PatternSentiment::Bullish
);
assert_eq!(
CandlePattern::BullishEngulfing.sentiment(),
PatternSentiment::Bullish
);
assert_eq!(
CandlePattern::ShootingStar.sentiment(),
PatternSentiment::Bearish
);
assert_eq!(
CandlePattern::EveningStar.sentiment(),
PatternSentiment::Bearish
);
assert_eq!(
CandlePattern::BearishEngulfing.sentiment(),
PatternSentiment::Bearish
);
assert_eq!(CandlePattern::Doji.sentiment(), PatternSentiment::Neutral);
assert_eq!(
CandlePattern::SpinningTop.sentiment(),
PatternSentiment::Neutral
);
}
#[test]
fn test_three_bar_takes_priority_over_two_bar() {
let a = c(110.0, 112.0, 100.0, 102.0);
let b = c(100.5, 101.0, 99.5, 100.8);
let cc = c(99.0, 112.0, 98.0, 108.0); let result = patterns(&[a, b, cc]);
assert_eq!(result[2], Some(CandlePattern::MorningStar));
}
}