feat: TA-Lib candlestick patterns — crows & three-line (part 1 of 9) (#130)

* feat: add Two Crows candlestick pattern (CDL2CROWS)

* feat: add Upside Gap Two Crows candlestick pattern (CDLUPSIDEGAP2CROWS)

* feat: add Identical Three Crows candlestick pattern (CDLIDENTICAL3CROWS)

* feat: add Three Line Strike candlestick pattern (CDL3LINESTRIKE)

* feat: add Three Stars in the South candlestick pattern (CDL3STARSINSOUTH)
This commit is contained in:
kingchenc
2026-06-01 23:20:10 +02:00
committed by GitHub
parent 458ef2385e
commit f09057aaf1
18 changed files with 1302 additions and 22 deletions
@@ -0,0 +1,230 @@
//! Identical Three Crows candlestick pattern.
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Identical Three Crows — a 3-bar bearish reversal: three consecutive red
/// candles with steadily lower closes where each candle opens at (or very near)
/// the prior candle's close, so the bodies stack in an identical staircase.
///
/// ```text
/// tol_n = tolerance * max(|open|, |prev.close|)
/// all three red (close < open)
/// declining closes (bar2.close < bar1.close, bar3.close < bar2.close)
/// bar2 opens at bar1's close (|bar2.open bar1.close| <= tol_2)
/// bar3 opens at bar2's close (|bar3.open bar2.close| <= tol_3)
/// ```
///
/// Output is `1.0` when the pattern completes and `0.0` otherwise. Identical
/// Three Crows is a single-direction (bearish-only) pattern, so it never emits
/// `+1.0`. The first two bars always return `0.0` because the three-bar window
/// is not yet filled. `tolerance` defaults to `0.001` (10 bps relative) and must
/// lie in `[0, 1)`. Pattern-shape check only — no trend filter is applied;
/// combine with a trend indicator for actionable signals.
///
/// # Signed ±1 encoding
///
/// This detector emits the uniform candlestick sign convention shared across the
/// pattern family — `1.0` bearish, `0.0` no pattern — so it drops straight into
/// a machine-learning feature matrix as a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, IdenticalThreeCrows, Indicator};
///
/// let mut indicator = IdenticalThreeCrows::new();
/// indicator.update(Candle::new(13.0, 13.1, 11.9, 12.0, 1.0, 0).unwrap());
/// indicator.update(Candle::new(12.0, 12.1, 10.9, 11.0, 1.0, 1).unwrap());
/// let out = indicator
/// .update(Candle::new(11.0, 11.1, 9.9, 10.0, 1.0, 2).unwrap());
/// assert_eq!(out, Some(-1.0));
/// ```
#[derive(Debug, Clone)]
pub struct IdenticalThreeCrows {
tolerance: f64,
prev: Option<Candle>,
prev_prev: Option<Candle>,
has_emitted: bool,
}
impl Default for IdenticalThreeCrows {
fn default() -> Self {
Self::new()
}
}
impl IdenticalThreeCrows {
/// Construct a detector with the default relative tolerance (1e-3).
pub const fn new() -> Self {
Self {
tolerance: 0.001,
prev: None,
prev_prev: None,
has_emitted: false,
}
}
/// Construct a detector with a custom relative tolerance.
///
/// `tolerance` must lie in `[0, 1)`.
pub fn with_tolerance(tolerance: f64) -> Result<Self> {
if !(0.0..1.0).contains(&tolerance) {
return Err(Error::InvalidPeriod {
message: "identical three crows tolerance must lie in [0, 1)",
});
}
Ok(Self {
tolerance,
prev: None,
prev_prev: None,
has_emitted: false,
})
}
/// Configured relative tolerance.
pub fn tolerance(&self) -> f64 {
self.tolerance
}
}
impl Indicator for IdenticalThreeCrows {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let pp = self.prev_prev;
let p = self.prev;
self.prev_prev = self.prev;
self.prev = Some(candle);
let (Some(bar1), Some(bar2)) = (pp, p) else {
return Some(0.0);
};
let tol2 = self.tolerance * bar2.open.abs().max(bar1.close.abs());
let tol3 = self.tolerance * candle.open.abs().max(bar2.close.abs());
if bar1.close < bar1.open
&& bar2.close < bar2.open
&& candle.close < candle.open
&& bar2.close < bar1.close
&& candle.close < bar2.close
&& (bar2.open - bar1.close).abs() <= tol2
&& (candle.open - bar2.close).abs() <= tol3
{
return Some(-1.0);
}
Some(0.0)
}
fn reset(&mut self) {
self.prev = None;
self.prev_prev = None;
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
3
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"IdenticalThreeCrows"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
Candle::new(open, high, low, close, 1.0, ts).unwrap()
}
#[test]
fn rejects_invalid_tolerance() {
assert!(IdenticalThreeCrows::with_tolerance(-0.01).is_err());
assert!(IdenticalThreeCrows::with_tolerance(1.0).is_err());
}
#[test]
fn accepts_valid_tolerance() {
let t = IdenticalThreeCrows::with_tolerance(0.0).unwrap();
assert!((t.tolerance() - 0.0).abs() < 1e-12);
}
#[test]
fn accessors_and_metadata() {
let t = IdenticalThreeCrows::default();
assert_eq!(t.name(), "IdenticalThreeCrows");
assert_eq!(t.warmup_period(), 3);
assert!(!t.is_ready());
assert!((t.tolerance() - 0.001).abs() < 1e-12);
}
#[test]
fn identical_three_crows_is_minus_one() {
let mut t = IdenticalThreeCrows::new();
// Three red candles, each opening at the prior close, declining.
assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), Some(0.0));
assert_eq!(t.update(c(12.0, 12.1, 10.9, 11.0, 1)), Some(0.0));
assert_eq!(t.update(c(11.0, 11.1, 9.9, 10.0, 2)), Some(-1.0));
}
#[test]
fn non_identical_opens_yield_zero() {
let mut t = IdenticalThreeCrows::new();
t.update(c(13.0, 13.1, 11.9, 12.0, 0));
t.update(c(12.0, 12.1, 10.9, 11.0, 1));
// bar3 opens at 10.0, far from bar2's close (11.0) -> not identical.
assert_eq!(t.update(c(10.0, 10.1, 8.9, 9.0, 2)), Some(0.0));
}
#[test]
fn rising_close_yields_zero() {
let mut t = IdenticalThreeCrows::new();
t.update(c(13.0, 13.1, 11.9, 12.0, 0));
t.update(c(12.0, 12.1, 10.9, 11.0, 1));
// bar3 is green -> not three crows.
assert_eq!(t.update(c(11.0, 12.2, 10.9, 12.0, 2)), Some(0.0));
}
#[test]
fn first_two_bars_return_zero() {
let mut t = IdenticalThreeCrows::new();
assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), Some(0.0));
assert_eq!(t.update(c(12.0, 12.1, 10.9, 11.0, 1)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 - i as f64;
c(base, base + 0.1, base - 1.1, base - 1.0, i)
})
.collect();
let mut a = IdenticalThreeCrows::new();
let mut b = IdenticalThreeCrows::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = IdenticalThreeCrows::new();
t.update(c(13.0, 13.1, 11.9, 12.0, 0));
t.update(c(12.0, 12.1, 10.9, 11.0, 1));
t.update(c(11.0, 11.1, 9.9, 10.0, 2));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), Some(0.0));
}
}
+16 -1
View File
@@ -95,6 +95,7 @@ mod hma;
mod hurst_channel;
mod hurst_exponent;
mod ichimoku;
mod identical_three_crows;
mod inertia;
mod information_ratio;
mod initial_balance;
@@ -207,8 +208,10 @@ mod td_setup;
mod tema;
mod term_structure_basis;
mod three_inside;
mod three_line_strike;
mod three_outside;
mod three_soldiers_or_crows;
mod three_stars_in_south;
mod tii;
mod trade_imbalance;
mod treynor_ratio;
@@ -219,9 +222,11 @@ mod tsi;
mod tsv;
mod ttm_squeeze;
mod tweezer;
mod two_crows;
mod typical_price;
mod ulcer_index;
mod ultimate_oscillator;
mod upside_gap_two_crows;
mod value_area;
mod value_at_risk;
mod variance;
@@ -339,6 +344,7 @@ pub use hma::Hma;
pub use hurst_channel::{HurstChannel, HurstChannelOutput};
pub use hurst_exponent::HurstExponent;
pub use ichimoku::{Ichimoku, IchimokuOutput};
pub use identical_three_crows::IdenticalThreeCrows;
pub use inertia::Inertia;
pub use information_ratio::InformationRatio;
pub use initial_balance::{InitialBalance, InitialBalanceOutput};
@@ -451,8 +457,10 @@ pub use td_setup::TdSetup;
pub use tema::Tema;
pub use term_structure_basis::TermStructureBasis;
pub use three_inside::ThreeInside;
pub use three_line_strike::ThreeLineStrike;
pub use three_outside::ThreeOutside;
pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
pub use three_stars_in_south::ThreeStarsInSouth;
pub use tii::Tii;
pub use trade_imbalance::TradeImbalance;
pub use treynor_ratio::TreynorRatio;
@@ -463,9 +471,11 @@ pub use tsi::Tsi;
pub use tsv::Tsv;
pub use ttm_squeeze::{TtmSqueeze, TtmSqueezeOutput};
pub use tweezer::Tweezer;
pub use two_crows::TwoCrows;
pub use typical_price::TypicalPrice;
pub use ulcer_index::UlcerIndex;
pub use ultimate_oscillator::UltimateOscillator;
pub use upside_gap_two_crows::UpsideGapTwoCrows;
pub use value_area::{ValueArea, ValueAreaOutput};
pub use value_at_risk::ValueAtRisk;
pub use variance::Variance;
@@ -755,6 +765,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"SpinningTop",
"ThreeInside",
"ThreeOutside",
"TwoCrows",
"UpsideGapTwoCrows",
"IdenticalThreeCrows",
"ThreeLineStrike",
"ThreeStarsInSouth",
],
),
(
@@ -846,6 +861,6 @@ mod family_tests {
// the actual indicator count is the early-warning signal that an
// indicator was added without being assigned a family.
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
assert_eq!(total, 239, "FAMILIES total drifted from indicator count");
assert_eq!(total, 244, "FAMILIES total drifted from indicator count");
}
}
@@ -0,0 +1,215 @@
//! Three Line Strike candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Three Line Strike — a 4-bar pattern: three candles marching in one direction
/// (a three-soldiers / three-crows advance) followed by a fourth candle of the
/// opposite colour that opens beyond the third candle and closes back past the
/// first candle's open, "striking" through the whole run.
///
/// **Bullish** (`+1.0`):
/// ```text
/// bar1..bar3 green, each opening inside the prior body and closing higher
/// bar4 red & opens above bar3's close & closes below bar1's open
/// ```
///
/// **Bearish** (`1.0`): the mirror — three falling red candles struck by a
/// green bar4 that opens below bar3's close and closes above bar1's open.
///
/// Output is `0.0` otherwise. The first three bars always return `0.0` because
/// the four-bar window is not yet filled. Pattern-shape check only — no trend
/// filter is applied; combine with a trend indicator for actionable signals.
///
/// # Signed ±1 encoding
///
/// This detector emits the uniform candlestick sign convention shared across the
/// pattern family — `+1.0` bullish, `1.0` bearish, `0.0` no pattern — so it
/// drops straight into a machine-learning feature matrix where the bullish and
/// bearish variants occupy a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, ThreeLineStrike};
///
/// let mut indicator = ThreeLineStrike::new();
/// indicator.update(Candle::new(10.0, 11.1, 9.9, 11.0, 1.0, 0).unwrap());
/// indicator.update(Candle::new(10.5, 12.1, 10.4, 12.0, 1.0, 1).unwrap());
/// indicator.update(Candle::new(11.5, 13.1, 11.4, 13.0, 1.0, 2).unwrap());
/// let out = indicator
/// .update(Candle::new(13.5, 13.6, 9.4, 9.5, 1.0, 3).unwrap());
/// assert_eq!(out, Some(1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct ThreeLineStrike {
c1: Option<Candle>,
c2: Option<Candle>,
c3: Option<Candle>,
has_emitted: bool,
}
impl ThreeLineStrike {
/// Construct a new Three Line Strike detector.
pub const fn new() -> Self {
Self {
c1: None,
c2: None,
c3: None,
has_emitted: false,
}
}
}
impl Indicator for ThreeLineStrike {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let bar1 = self.c1;
let bar2 = self.c2;
let bar3 = self.c3;
self.c1 = self.c2;
self.c2 = self.c3;
self.c3 = Some(candle);
let (Some(bar1), Some(bar2), Some(bar3)) = (bar1, bar2, bar3) else {
return Some(0.0);
};
// Bullish: three rising green candles struck by a red bar4.
if bar1.close > bar1.open
&& bar2.close > bar2.open
&& bar3.close > bar3.open
&& bar2.open >= bar1.open
&& bar2.open <= bar1.close
&& bar2.close > bar1.close
&& bar3.open >= bar2.open
&& bar3.open <= bar2.close
&& bar3.close > bar2.close
&& candle.close < candle.open
&& candle.open > bar3.close
&& candle.close < bar1.open
{
return Some(1.0);
}
// Bearish: three falling red candles struck by a green bar4.
if bar1.close < bar1.open
&& bar2.close < bar2.open
&& bar3.close < bar3.open
&& bar2.open <= bar1.open
&& bar2.open >= bar1.close
&& bar2.close < bar1.close
&& bar3.open <= bar2.open
&& bar3.open >= bar2.close
&& bar3.close < bar2.close
&& candle.close > candle.open
&& candle.open < bar3.close
&& candle.close > bar1.open
{
return Some(-1.0);
}
Some(0.0)
}
fn reset(&mut self) {
self.c1 = None;
self.c2 = None;
self.c3 = None;
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
4
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"ThreeLineStrike"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
Candle::new(open, high, low, close, 1.0, ts).unwrap()
}
#[test]
fn accessors_and_metadata() {
let t = ThreeLineStrike::new();
assert_eq!(t.name(), "ThreeLineStrike");
assert_eq!(t.warmup_period(), 4);
assert!(!t.is_ready());
}
#[test]
fn bullish_three_line_strike_is_plus_one() {
let mut t = ThreeLineStrike::new();
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
assert_eq!(t.update(c(10.5, 12.1, 10.4, 12.0, 1)), Some(0.0));
assert_eq!(t.update(c(11.5, 13.1, 11.4, 13.0, 2)), Some(0.0));
assert_eq!(t.update(c(13.5, 13.6, 9.4, 9.5, 3)), Some(1.0));
}
#[test]
fn bearish_three_line_strike_is_minus_one() {
let mut t = ThreeLineStrike::new();
assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), Some(0.0));
assert_eq!(t.update(c(12.5, 12.6, 10.9, 11.0, 1)), Some(0.0));
assert_eq!(t.update(c(11.5, 11.6, 9.9, 10.0, 2)), Some(0.0));
assert_eq!(t.update(c(9.5, 13.6, 9.4, 13.5, 3)), Some(-1.0));
}
#[test]
fn strike_not_clearing_first_open_yields_zero() {
let mut t = ThreeLineStrike::new();
t.update(c(10.0, 11.1, 9.9, 11.0, 0));
t.update(c(10.5, 12.1, 10.4, 12.0, 1));
t.update(c(11.5, 13.1, 11.4, 13.0, 2));
// bar4 closes 10.5, above bar1's open (10.0) -> does not strike through.
assert_eq!(t.update(c(13.5, 13.6, 10.4, 10.5, 3)), Some(0.0));
}
#[test]
fn first_three_bars_return_zero() {
let mut t = ThreeLineStrike::new();
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
assert_eq!(t.update(c(10.5, 12.1, 10.4, 12.0, 1)), Some(0.0));
assert_eq!(t.update(c(11.5, 13.1, 11.4, 13.0, 2)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64;
c(base, base + 1.5, base - 0.2, base + 1.0, i)
})
.collect();
let mut a = ThreeLineStrike::new();
let mut b = ThreeLineStrike::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = ThreeLineStrike::new();
t.update(c(10.0, 11.1, 9.9, 11.0, 0));
t.update(c(10.5, 12.1, 10.4, 12.0, 1));
t.update(c(11.5, 13.1, 11.4, 13.0, 2));
t.update(c(13.5, 13.6, 9.4, 9.5, 3));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
}
}
@@ -0,0 +1,239 @@
//! Three Stars in the South candlestick pattern.
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Three Stars in the South — a rare 3-bar bullish reversal: three shrinking red
/// candles where each session carves out a higher low and contracts toward a
/// tiny black marubozu, signalling exhausted selling at the bottom of a decline.
///
/// ```text
/// tol = tolerance * max(|bar3.high|, |bar3.low|)
/// all three red (close < open)
/// bar1 long lower shadow (bar1.close bar1.low) >= (bar1.open bar1.close)
/// bar2 opens inside bar1's body, higher low, smaller body, closes above bar1.close
/// bar3 small black marubozu (upper & lower shadow <= tol) inside bar2's range
/// ```
///
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Three Stars
/// in the South is a single-direction (bullish-only) pattern, so it never emits
/// `1.0`. The first two bars always return `0.0` because the three-bar window
/// is not yet filled. `tolerance` defaults to `0.001` (10 bps relative) and must
/// lie in `[0, 1)`. Pattern-shape check only — no trend filter is applied;
/// combine with a trend indicator for actionable signals.
///
/// # Signed ±1 encoding
///
/// This detector emits the uniform candlestick sign convention shared across the
/// pattern family — `+1.0` bullish, `0.0` no pattern — so it drops straight into
/// a machine-learning feature matrix as a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, ThreeStarsInSouth};
///
/// let mut indicator = ThreeStarsInSouth::new();
/// indicator.update(Candle::new(20.0, 20.1, 8.0, 15.0, 1.0, 0).unwrap());
/// indicator.update(Candle::new(18.0, 18.1, 12.0, 16.0, 1.0, 1).unwrap());
/// let out = indicator
/// .update(Candle::new(15.0, 15.0, 14.0, 14.0, 1.0, 2).unwrap());
/// assert_eq!(out, Some(1.0));
/// ```
#[derive(Debug, Clone)]
pub struct ThreeStarsInSouth {
tolerance: f64,
prev: Option<Candle>,
prev_prev: Option<Candle>,
has_emitted: bool,
}
impl Default for ThreeStarsInSouth {
fn default() -> Self {
Self::new()
}
}
impl ThreeStarsInSouth {
/// Construct a detector with the default relative tolerance (1e-3).
pub const fn new() -> Self {
Self {
tolerance: 0.001,
prev: None,
prev_prev: None,
has_emitted: false,
}
}
/// Construct a detector with a custom relative tolerance.
///
/// `tolerance` must lie in `[0, 1)`.
pub fn with_tolerance(tolerance: f64) -> Result<Self> {
if !(0.0..1.0).contains(&tolerance) {
return Err(Error::InvalidPeriod {
message: "three stars in the south tolerance must lie in [0, 1)",
});
}
Ok(Self {
tolerance,
prev: None,
prev_prev: None,
has_emitted: false,
})
}
/// Configured relative tolerance.
pub fn tolerance(&self) -> f64 {
self.tolerance
}
}
impl Indicator for ThreeStarsInSouth {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let pp = self.prev_prev;
let p = self.prev;
self.prev_prev = self.prev;
self.prev = Some(candle);
let (Some(bar1), Some(bar2)) = (pp, p) else {
return Some(0.0);
};
let tol = self.tolerance * candle.high.abs().max(candle.low.abs());
let bar1_body = bar1.open - bar1.close;
let bar1_lower_shadow = bar1.close - bar1.low;
let bar2_body = bar2.open - bar2.close;
let bar3_upper_shadow = candle.high - candle.open;
let bar3_lower_shadow = candle.close - candle.low;
if bar1.close < bar1.open
&& bar2.close < bar2.open
&& candle.close < candle.open
&& bar1_lower_shadow >= bar1_body
&& bar2.open <= bar1.open
&& bar2.open >= bar1.close
&& bar2.low > bar1.low
&& bar2.close > bar1.close
&& bar2_body < bar1_body
&& bar3_upper_shadow <= tol
&& bar3_lower_shadow <= tol
&& candle.high < bar2.high
&& candle.low > bar2.low
{
return Some(1.0);
}
Some(0.0)
}
fn reset(&mut self) {
self.prev = None;
self.prev_prev = None;
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
3
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"ThreeStarsInSouth"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
Candle::new(open, high, low, close, 1.0, ts).unwrap()
}
#[test]
fn rejects_invalid_tolerance() {
assert!(ThreeStarsInSouth::with_tolerance(-0.01).is_err());
assert!(ThreeStarsInSouth::with_tolerance(1.0).is_err());
}
#[test]
fn accepts_valid_tolerance() {
let t = ThreeStarsInSouth::with_tolerance(0.0).unwrap();
assert!((t.tolerance() - 0.0).abs() < 1e-12);
}
#[test]
fn accessors_and_metadata() {
let t = ThreeStarsInSouth::default();
assert_eq!(t.name(), "ThreeStarsInSouth");
assert_eq!(t.warmup_period(), 3);
assert!(!t.is_ready());
assert!((t.tolerance() - 0.001).abs() < 1e-12);
}
#[test]
fn three_stars_in_south_is_plus_one() {
let mut t = ThreeStarsInSouth::new();
assert_eq!(t.update(c(20.0, 20.1, 8.0, 15.0, 0)), Some(0.0));
assert_eq!(t.update(c(18.0, 18.1, 12.0, 16.0, 1)), Some(0.0));
assert_eq!(t.update(c(15.0, 15.0, 14.0, 14.0, 2)), Some(1.0));
}
#[test]
fn third_with_shadow_yields_zero() {
let mut t = ThreeStarsInSouth::new();
t.update(c(20.0, 20.1, 8.0, 15.0, 0));
t.update(c(18.0, 18.1, 12.0, 16.0, 1));
// bar3 has a long lower shadow -> not a marubozu.
assert_eq!(t.update(c(15.0, 15.0, 12.5, 14.0, 2)), Some(0.0));
}
#[test]
fn lower_low_yields_zero() {
let mut t = ThreeStarsInSouth::new();
t.update(c(20.0, 20.1, 8.0, 15.0, 0));
// bar2 dips below bar1's low -> no higher low.
assert_eq!(t.update(c(18.0, 18.1, 7.0, 16.0, 1)), Some(0.0));
assert_eq!(t.update(c(15.0, 15.0, 14.0, 14.0, 2)), Some(0.0));
}
#[test]
fn first_two_bars_return_zero() {
let mut t = ThreeStarsInSouth::new();
assert_eq!(t.update(c(20.0, 20.1, 8.0, 15.0, 0)), Some(0.0));
assert_eq!(t.update(c(18.0, 18.1, 12.0, 16.0, 1)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64;
c(base + 2.0, base + 2.1, base - 3.0, base, i)
})
.collect();
let mut a = ThreeStarsInSouth::new();
let mut b = ThreeStarsInSouth::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = ThreeStarsInSouth::new();
t.update(c(20.0, 20.1, 8.0, 15.0, 0));
t.update(c(18.0, 18.1, 12.0, 16.0, 1));
t.update(c(15.0, 15.0, 14.0, 14.0, 2));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(20.0, 20.1, 8.0, 15.0, 0)), Some(0.0));
}
}
@@ -0,0 +1,186 @@
//! Two Crows candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Two Crows — a 3-bar bearish reversal pattern that appears after an advance.
///
/// ```text
/// bar1 green (long white)
/// bar2 red & its body gaps up above bar1's body (bar2.close > bar1.close)
/// bar3 red & opens inside bar2's body (bar2.close < bar3.open < bar2.open)
/// & closes inside bar1's body (bar1.open < bar3.close < bar1.close)
/// ```
///
/// Output is `1.0` when the pattern completes and `0.0` otherwise. Two Crows is
/// a single-direction (bearish-only) pattern, so it never emits `+1.0`. The
/// first two bars always return `0.0` because the three-bar window is not yet
/// filled. Pattern-shape check only — no trend filter is applied; combine with a
/// trend indicator for actionable signals.
///
/// # Signed ±1 encoding
///
/// This detector emits the uniform candlestick sign convention shared across the
/// pattern family — `1.0` bearish, `0.0` no pattern — so it drops straight into
/// a machine-learning feature matrix as a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, TwoCrows};
///
/// let mut indicator = TwoCrows::new();
/// indicator.update(Candle::new(10.0, 12.2, 9.9, 12.0, 1.0, 0).unwrap());
/// indicator.update(Candle::new(14.0, 14.2, 12.9, 13.0, 1.0, 1).unwrap());
/// let out = indicator
/// .update(Candle::new(13.5, 13.6, 10.9, 11.0, 1.0, 2).unwrap());
/// assert_eq!(out, Some(-1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct TwoCrows {
prev: Option<Candle>,
prev_prev: Option<Candle>,
has_emitted: bool,
}
impl TwoCrows {
/// Construct a new Two Crows detector.
pub const fn new() -> Self {
Self {
prev: None,
prev_prev: None,
has_emitted: false,
}
}
}
impl Indicator for TwoCrows {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let pp = self.prev_prev;
let p = self.prev;
self.prev_prev = self.prev;
self.prev = Some(candle);
let (Some(bar1), Some(bar2)) = (pp, p) else {
return Some(0.0);
};
if bar1.close > bar1.open
&& bar2.close < bar2.open
&& bar2.close > bar1.close
&& candle.close < candle.open
&& candle.open < bar2.open
&& candle.open > bar2.close
&& candle.close > bar1.open
&& candle.close < bar1.close
{
return Some(-1.0);
}
Some(0.0)
}
fn reset(&mut self) {
self.prev = None;
self.prev_prev = None;
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
3
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"TwoCrows"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
Candle::new(open, high, low, close, 1.0, ts).unwrap()
}
#[test]
fn accessors_and_metadata() {
let t = TwoCrows::new();
assert_eq!(t.name(), "TwoCrows");
assert_eq!(t.warmup_period(), 3);
assert!(!t.is_ready());
}
#[test]
fn two_crows_is_minus_one() {
let mut t = TwoCrows::new();
// bar1 green 10->12; bar2 red 14->13 (body above bar1); bar3 red
// opens 13.5 (inside [13,14]) and closes 11 (inside [10,12]).
assert_eq!(t.update(c(10.0, 12.2, 9.9, 12.0, 0)), Some(0.0));
assert_eq!(t.update(c(14.0, 14.2, 12.9, 13.0, 1)), Some(0.0));
assert_eq!(t.update(c(13.5, 13.6, 10.9, 11.0, 2)), Some(-1.0));
}
#[test]
fn no_gap_up_yields_zero() {
let mut t = TwoCrows::new();
// bar2 red but its body does not gap above bar1's body.
t.update(c(10.0, 12.2, 9.9, 12.0, 0));
t.update(c(11.5, 12.0, 10.4, 11.0, 1));
assert_eq!(t.update(c(11.0, 11.2, 9.9, 10.5, 2)), Some(0.0));
}
#[test]
fn third_close_below_first_body_yields_zero() {
let mut t = TwoCrows::new();
t.update(c(10.0, 12.2, 9.9, 12.0, 0));
t.update(c(14.0, 14.2, 12.9, 13.0, 1));
// bar3 closes 9.5, below bar1's body low (10) -> not Two Crows.
assert_eq!(t.update(c(13.5, 13.6, 9.4, 9.5, 2)), Some(0.0));
}
#[test]
fn first_two_bars_return_zero() {
let mut t = TwoCrows::new();
assert_eq!(t.update(c(10.0, 12.2, 9.9, 12.0, 0)), Some(0.0));
assert_eq!(t.update(c(14.0, 14.2, 12.9, 13.0, 1)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64;
if i % 3 == 0 {
c(base, base + 0.5, base - 1.0, base + 0.4, i)
} else {
c(base + 1.5, base + 1.7, base - 0.2, base + 0.6, i)
}
})
.collect();
let mut a = TwoCrows::new();
let mut b = TwoCrows::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut t = TwoCrows::new();
t.update(c(10.0, 12.2, 9.9, 12.0, 0));
t.update(c(14.0, 14.2, 12.9, 13.0, 1));
t.update(c(13.5, 13.6, 10.9, 11.0, 2));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(c(10.0, 12.2, 9.9, 12.0, 0)), Some(0.0));
}
}
@@ -0,0 +1,189 @@
//! Upside Gap Two Crows candlestick pattern.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Upside Gap Two Crows — a 3-bar bearish reversal that appears after an
/// advance. Two black candles gap up above a long white candle; the second
/// black candle engulfs the first crow yet still closes above the white body,
/// leaving the upside gap open.
///
/// ```text
/// bar1 green (long white)
/// bar2 red & its body gaps up above bar1's body (bar2.close > bar1.close)
/// bar3 red & opens above bar2's open (bar3.open > bar2.open)
/// & closes below bar2's close (bar3.close < bar2.close)
/// & closes above bar1's close (bar3.close > bar1.close)
/// ```
///
/// Output is `1.0` when the pattern completes and `0.0` otherwise. Upside Gap
/// Two Crows is a single-direction (bearish-only) pattern, so it never emits
/// `+1.0`. The first two bars always return `0.0` because the three-bar window
/// is not yet filled. Pattern-shape check only — no trend filter is applied;
/// combine with a trend indicator for actionable signals.
///
/// # Signed ±1 encoding
///
/// This detector emits the uniform candlestick sign convention shared across the
/// pattern family — `1.0` bearish, `0.0` no pattern — so it drops straight into
/// a machine-learning feature matrix as a single dimension.
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, UpsideGapTwoCrows};
///
/// let mut indicator = UpsideGapTwoCrows::new();
/// indicator.update(Candle::new(10.0, 12.2, 9.9, 12.0, 1.0, 0).unwrap());
/// indicator.update(Candle::new(14.0, 14.2, 12.9, 13.0, 1.0, 1).unwrap());
/// let out = indicator
/// .update(Candle::new(15.0, 15.2, 12.4, 12.5, 1.0, 2).unwrap());
/// assert_eq!(out, Some(-1.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct UpsideGapTwoCrows {
prev: Option<Candle>,
prev_prev: Option<Candle>,
has_emitted: bool,
}
impl UpsideGapTwoCrows {
/// Construct a new Upside Gap Two Crows detector.
pub const fn new() -> Self {
Self {
prev: None,
prev_prev: None,
has_emitted: false,
}
}
}
impl Indicator for UpsideGapTwoCrows {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
self.has_emitted = true;
let pp = self.prev_prev;
let p = self.prev;
self.prev_prev = self.prev;
self.prev = Some(candle);
let (Some(bar1), Some(bar2)) = (pp, p) else {
return Some(0.0);
};
if bar1.close > bar1.open
&& bar2.close < bar2.open
&& bar2.close > bar1.close
&& candle.close < candle.open
&& candle.open > bar2.open
&& candle.close < bar2.close
&& candle.close > bar1.close
{
return Some(-1.0);
}
Some(0.0)
}
fn reset(&mut self) {
self.prev = None;
self.prev_prev = None;
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
3
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"UpsideGapTwoCrows"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
Candle::new(open, high, low, close, 1.0, ts).unwrap()
}
#[test]
fn accessors_and_metadata() {
let u = UpsideGapTwoCrows::new();
assert_eq!(u.name(), "UpsideGapTwoCrows");
assert_eq!(u.warmup_period(), 3);
assert!(!u.is_ready());
}
#[test]
fn upside_gap_two_crows_is_minus_one() {
let mut u = UpsideGapTwoCrows::new();
// bar1 green 10->12; bar2 red 14->13 gapping up; bar3 red opens 15
// (above bar2 open) and closes 12.5 (below bar2 close, above bar1 close).
assert_eq!(u.update(c(10.0, 12.2, 9.9, 12.0, 0)), Some(0.0));
assert_eq!(u.update(c(14.0, 14.2, 12.9, 13.0, 1)), Some(0.0));
assert_eq!(u.update(c(15.0, 15.2, 12.4, 12.5, 2)), Some(-1.0));
}
#[test]
fn closing_the_first_gap_yields_zero() {
let mut u = UpsideGapTwoCrows::new();
u.update(c(10.0, 12.2, 9.9, 12.0, 0));
u.update(c(14.0, 14.2, 12.9, 13.0, 1));
// bar3 closes 11.5, below bar1's close (12) -> gap closed, not the pattern.
assert_eq!(u.update(c(15.0, 15.2, 11.4, 11.5, 2)), Some(0.0));
}
#[test]
fn no_gap_up_yields_zero() {
let mut u = UpsideGapTwoCrows::new();
u.update(c(10.0, 12.2, 9.9, 12.0, 0));
// bar2's body does not gap above bar1's body.
u.update(c(11.5, 12.0, 10.4, 11.0, 1));
assert_eq!(u.update(c(12.0, 12.2, 10.9, 11.5, 2)), Some(0.0));
}
#[test]
fn first_two_bars_return_zero() {
let mut u = UpsideGapTwoCrows::new();
assert_eq!(u.update(c(10.0, 12.2, 9.9, 12.0, 0)), Some(0.0));
assert_eq!(u.update(c(14.0, 14.2, 12.9, 13.0, 1)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let base = 100.0 + i as f64;
if i % 3 == 0 {
c(base, base + 0.5, base - 1.0, base + 0.4, i)
} else {
c(base + 1.5, base + 1.7, base - 0.2, base + 0.6, i)
}
})
.collect();
let mut a = UpsideGapTwoCrows::new();
let mut b = UpsideGapTwoCrows::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_clears_state() {
let mut u = UpsideGapTwoCrows::new();
u.update(c(10.0, 12.2, 9.9, 12.0, 0));
u.update(c(14.0, 14.2, 12.9, 13.0, 1));
u.update(c(15.0, 15.2, 12.4, 12.5, 2));
assert!(u.is_ready());
u.reset();
assert!(!u.is_ready());
assert_eq!(u.update(c(10.0, 12.2, 9.9, 12.0, 0)), Some(0.0));
}
}
+13 -12
View File
@@ -65,15 +65,15 @@ pub use indicators::{
FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean, FundingRateZScore,
GainLossRatio, GarmanKlassVolatility, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput,
HiLoActivator, HilbertDominantCycle, HistoricalVolatility, Hma, HurstChannel,
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, Inertia, InformationRatio,
InitialBalance, InitialBalanceOutput, InstantaneousTrendline, InverseFisherTransform,
InvertedHammer, Jma, Kama, KellyCriterion, Keltner, KeltnerOutput, Kst, KstOutput, Kurtosis,
Kvo, KylesLambda, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput,
LinRegAngle, LinRegChannel, LinRegChannelOutput, LinRegSlope, LinearRegression,
LiquidationFeatures, LiquidationFeaturesOutput, LongShortRatio, MaEnvelope, MaEnvelopeOutput,
MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, Mom,
MorningEveningStar, Natr, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio,
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, Inertia,
InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
InverseFisherTransform, InvertedHammer, Jma, Kama, KellyCriterion, Keltner, KeltnerOutput, Kst,
KstOutput, Kurtosis, Kvo, KylesLambda, LaguerreRsi, LeadLagCrossCorrelation,
LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput, LinRegSlope,
LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput, LongShortRatio, MaEnvelope,
MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
Marubozu, MassIndex, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi,
Microprice, Mom, MorningEveningStar, Natr, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio,
OpenInterestDelta, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex, PairSpreadZScore, PairwiseBeta,
ParkinsonVolatility, PearsonCorrelation, PercentB, PercentageTrailingStop, Pgo,
@@ -86,9 +86,10 @@ pub use indicators::{
StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio, TdCombo,
TdCountdown, TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure,
TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput,
TdSequential, TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside, ThreeOutside,
ThreeSoldiersOrCrows, Tii, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv,
TtmSqueeze, TtmSqueezeOutput, Tweezer, TypicalPrice, UlcerIndex, UltimateOscillator, ValueArea,
TdSequential, TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside,
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Tii, TradeImbalance,
TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, Tweezer,
TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UpsideGapTwoCrows, ValueArea,
ValueAreaOutput, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VoltyStop,
VolumeOscillator, VolumePriceTrend, Vortex, VortexOutput, Vwap, VwapStdDevBands,
VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, WeightedClose, WilliamsFractals,