feat: TA-Lib candlestick patterns — Doji family (part 3 of 9) (#134)
* feat: add Doji-family candlestick patterns Five single-/two-bar Doji patterns, all `Input = Candle`, `Output = f64`: - Doji Star (CDLDOJISTAR) — a long body followed by a doji gapping away in the trend direction; bullish +1 (after a black bar), bearish -1 (after a white bar). - Dragonfly Doji (CDLDRAGONFLYDOJI) — a doji opening and closing at the high with a long lower shadow; bullish +1. - Gravestone Doji (CDLGRAVESTONEDOJI) — a doji opening and closing at the low with a long upper shadow; bearish -1. - Long-Legged Doji (CDLLONGLEGGEDDOJI) — a doji with long shadows on both sides; a non-directional indecision flag, +1 on detection. - Rickshaw Man (CDLRICKSHAWMAN) — a long-legged doji with the body centred in the range; a non-directional indecision flag, +1 on detection. Counter 254 -> 259 (mod-count == lib counted block; FAMILIES total 249 -> 254). * chore: sync indicator count to 259 --------- Co-authored-by: wickra-bot <wickra-bot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
//! Doji Star candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Doji Star — a 2-bar reversal warning. A long trending body is followed by a
|
||||
/// doji whose tiny body gaps away in the direction of the trend, the indecision
|
||||
/// hinting the move is about to turn.
|
||||
///
|
||||
/// ```text
|
||||
/// long body = |close − open| >= 0.5 * (high − low) (bar1)
|
||||
/// doji = |close − open| <= 0.1 * (high − low) (bar2)
|
||||
/// bullish (+1.0): bar1 black, doji body gaps DOWN below it (max(o2,c2) < close1)
|
||||
/// bearish (−1.0): bar1 white, doji body gaps UP above it (min(o2,c2) > close1)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (bullish star, after a black bar) or `−1.0` (bearish star,
|
||||
/// after a white bar) when the pattern completes, and `0.0` otherwise. The first
|
||||
/// bar always returns `0.0` because the two-bar window is not yet filled. Doji
|
||||
/// thresholds follow the geometric house style (fixed half-range body for the
|
||||
/// long bar, tenth-range body for the doji) rather than TA-Lib's rolling
|
||||
/// averages. 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, DojiStar, Indicator};
|
||||
///
|
||||
/// let mut indicator = DojiStar::new();
|
||||
/// // Long black bar, then a doji gapping down -> bullish star.
|
||||
/// indicator.update(Candle::new(20.0, 20.2, 14.8, 15.0, 1.0, 0).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(13.0, 13.1, 12.9, 13.0, 1.0, 1).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DojiStar {
|
||||
prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl DojiStar {
|
||||
/// Construct a new Doji Star detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for DojiStar {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let Some(bar1) = prev else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range1 = bar1.high - bar1.low;
|
||||
let range2 = candle.high - candle.low;
|
||||
if range1 <= 0.0 || range2 <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body1 = bar1.close - bar1.open;
|
||||
if body1.abs() < 0.5 * range1 {
|
||||
return Some(0.0);
|
||||
}
|
||||
if (candle.close - candle.open).abs() > 0.1 * range2 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let doji_top = candle.open.max(candle.close);
|
||||
let doji_bottom = candle.open.min(candle.close);
|
||||
// Bullish: long black bar, doji body gaps down below it.
|
||||
if body1 < 0.0 && doji_top < bar1.close {
|
||||
return Some(1.0);
|
||||
}
|
||||
// Bearish: long white bar, doji body gaps up above it.
|
||||
if body1 > 0.0 && doji_bottom > bar1.close {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DojiStar"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = DojiStar::new();
|
||||
assert_eq!(t.name(), "DojiStar");
|
||||
assert_eq!(t.warmup_period(), 2);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_doji_star_is_plus_one() {
|
||||
let mut t = DojiStar::new();
|
||||
assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 13.1, 12.9, 13.0, 1)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_doji_star_is_minus_one() {
|
||||
let mut t = DojiStar::new();
|
||||
assert_eq!(t.update(c(15.0, 20.2, 14.8, 20.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(22.0, 22.1, 21.9, 22.0, 1)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_bar_not_doji_yields_zero() {
|
||||
let mut t = DojiStar::new();
|
||||
t.update(c(20.0, 20.2, 14.8, 15.0, 0));
|
||||
// Wide body, not a doji.
|
||||
assert_eq!(t.update(c(13.0, 13.2, 11.0, 11.5, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gap_yields_zero() {
|
||||
let mut t = DojiStar::new();
|
||||
t.update(c(20.0, 20.2, 14.8, 15.0, 0));
|
||||
// Doji overlaps bar1's body (no gap down).
|
||||
assert_eq!(t.update(c(16.0, 16.1, 15.9, 16.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_first_body_yields_zero() {
|
||||
let mut t = DojiStar::new();
|
||||
// First bar body too short to be the "long" leg.
|
||||
t.update(c(20.0, 24.0, 16.0, 19.5, 0));
|
||||
assert_eq!(t.update(c(13.0, 13.1, 12.9, 13.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_returns_zero() {
|
||||
let mut t = DojiStar::new();
|
||||
assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), 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 % 2 == 0 {
|
||||
c(base + 5.0, base + 5.2, base - 0.2, base, i)
|
||||
} else {
|
||||
c(base - 3.0, base - 2.9, base - 3.1, base - 3.0, i)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut a = DojiStar::new();
|
||||
let mut b = DojiStar::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = DojiStar::new();
|
||||
t.update(c(20.0, 20.2, 14.8, 15.0, 0));
|
||||
t.update(c(13.0, 13.1, 12.9, 13.0, 1));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Dragonfly Doji candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Dragonfly Doji — a single-bar bullish reversal. Open, close, and high sit at
|
||||
/// the top of the bar while a long lower shadow shows price was driven down hard
|
||||
/// and then bid all the way back to the open — buyers rejecting the lows.
|
||||
///
|
||||
/// ```text
|
||||
/// range = high − low
|
||||
/// doji = |close − open| <= 0.1 * range
|
||||
/// no upper wick = high − max(open, close) <= 0.1 * range
|
||||
/// long lower = min(open, close) − low >= 0.5 * range
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the dragonfly prints and `0.0` otherwise. Dragonfly Doji
|
||||
/// is a single-direction (bullish-only) shape, so it never emits `−1.0`. Body and
|
||||
/// shadow thresholds follow the geometric house style (fixed fractions of the bar
|
||||
/// range) rather than TA-Lib's rolling averages. 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, DragonflyDoji, Indicator};
|
||||
///
|
||||
/// let mut indicator = DragonflyDoji::new();
|
||||
/// // Body at the top, long lower shadow.
|
||||
/// let candle = Candle::new(10.0, 10.05, 6.0, 10.0, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DragonflyDoji {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl DragonflyDoji {
|
||||
/// Construct a new Dragonfly Doji detector.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for DragonflyDoji {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let range = candle.high - candle.low;
|
||||
if range <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
if (candle.close - candle.open).abs() > 0.1 * range {
|
||||
return Some(0.0);
|
||||
}
|
||||
let upper = candle.high - candle.open.max(candle.close);
|
||||
let lower = candle.open.min(candle.close) - candle.low;
|
||||
if upper <= 0.1 * range && lower >= 0.5 * range {
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DragonflyDoji"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = DragonflyDoji::new();
|
||||
assert_eq!(t.name(), "DragonflyDoji");
|
||||
assert_eq!(t.warmup_period(), 1);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragonfly_is_plus_one() {
|
||||
let mut t = DragonflyDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 10.05, 6.0, 10.0, 0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upper_shadow_yields_zero() {
|
||||
let mut t = DragonflyDoji::new();
|
||||
// Long upper shadow -> not a dragonfly (this is a gravestone shape).
|
||||
assert_eq!(t.update(c(10.0, 14.0, 9.95, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_lower_shadow_yields_zero() {
|
||||
let mut t = DragonflyDoji::new();
|
||||
// Body at the top but the lower shadow is too short.
|
||||
assert_eq!(t.update(c(10.0, 10.05, 9.6, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_doji_yields_zero() {
|
||||
let mut t = DragonflyDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 12.0, 6.0, 11.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = DragonflyDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), 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.05, base - 4.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = DragonflyDoji::new();
|
||||
let mut b = DragonflyDoji::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = DragonflyDoji::new();
|
||||
t.update(c(10.0, 10.05, 6.0, 10.0, 0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Gravestone Doji candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Gravestone Doji — a single-bar bearish reversal. Open, close, and low sit at
|
||||
/// the bottom of the bar while a long upper shadow shows price was pushed up hard
|
||||
/// and then sold all the way back to the open — sellers rejecting the highs.
|
||||
///
|
||||
/// ```text
|
||||
/// range = high − low
|
||||
/// doji = |close − open| <= 0.1 * range
|
||||
/// no lower wick = min(open, close) − low <= 0.1 * range
|
||||
/// long upper = high − max(open, close) >= 0.5 * range
|
||||
/// ```
|
||||
///
|
||||
/// Output is `−1.0` when the gravestone prints and `0.0` otherwise. Gravestone
|
||||
/// Doji is a single-direction (bearish-only) shape, so it never emits `+1.0`.
|
||||
/// Body and shadow thresholds follow the geometric house style (fixed fractions
|
||||
/// of the bar range) rather than TA-Lib's rolling averages. 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, GravestoneDoji, Indicator};
|
||||
///
|
||||
/// let mut indicator = GravestoneDoji::new();
|
||||
/// // Body at the bottom, long upper shadow.
|
||||
/// let candle = Candle::new(10.0, 14.0, 9.95, 10.0, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct GravestoneDoji {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl GravestoneDoji {
|
||||
/// Construct a new Gravestone Doji detector.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for GravestoneDoji {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let range = candle.high - candle.low;
|
||||
if range <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
if (candle.close - candle.open).abs() > 0.1 * range {
|
||||
return Some(0.0);
|
||||
}
|
||||
let upper = candle.high - candle.open.max(candle.close);
|
||||
let lower = candle.open.min(candle.close) - candle.low;
|
||||
if lower <= 0.1 * range && upper >= 0.5 * range {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"GravestoneDoji"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = GravestoneDoji::new();
|
||||
assert_eq!(t.name(), "GravestoneDoji");
|
||||
assert_eq!(t.warmup_period(), 1);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gravestone_is_minus_one() {
|
||||
let mut t = GravestoneDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 14.0, 9.95, 10.0, 0)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lower_shadow_yields_zero() {
|
||||
let mut t = GravestoneDoji::new();
|
||||
// Long lower shadow -> not a gravestone (this is a dragonfly shape).
|
||||
assert_eq!(t.update(c(10.0, 10.05, 6.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_upper_shadow_yields_zero() {
|
||||
let mut t = GravestoneDoji::new();
|
||||
// Body at the bottom but the upper shadow is too short.
|
||||
assert_eq!(t.update(c(10.0, 10.4, 9.95, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_doji_yields_zero() {
|
||||
let mut t = GravestoneDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 14.0, 9.5, 13.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = GravestoneDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), 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 + 4.0, base - 0.05, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = GravestoneDoji::new();
|
||||
let mut b = GravestoneDoji::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = GravestoneDoji::new();
|
||||
t.update(c(10.0, 14.0, 9.95, 10.0, 0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//! Long-Legged Doji candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Long-Legged Doji — a single-bar indecision signal. A doji with long shadows on
|
||||
/// *both* sides: price ranged widely up and down yet closed essentially where it
|
||||
/// opened, a tug-of-war that often precedes a turn.
|
||||
///
|
||||
/// ```text
|
||||
/// range = high − low
|
||||
/// doji = |close − open| <= 0.1 * range
|
||||
/// long upper = high − max(open, close) >= 0.3 * range
|
||||
/// long lower = min(open, close) − low >= 0.3 * range
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the long-legged doji prints and `0.0` otherwise. This is
|
||||
/// a non-directional indecision flag — it never emits `−1.0` (use
|
||||
/// `DragonflyDoji` / `GravestoneDoji` for the directional single-shadow variants).
|
||||
/// Body and shadow thresholds follow the geometric house style (fixed fractions
|
||||
/// of the bar range) rather than TA-Lib's rolling averages. 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` detected, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, LongLeggedDoji, Indicator};
|
||||
///
|
||||
/// let mut indicator = LongLeggedDoji::new();
|
||||
/// // Tiny body, long shadows on both sides.
|
||||
/// let candle = Candle::new(10.0, 12.0, 8.0, 10.05, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LongLeggedDoji {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl LongLeggedDoji {
|
||||
/// Construct a new Long-Legged Doji detector.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for LongLeggedDoji {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let range = candle.high - candle.low;
|
||||
if range <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
if (candle.close - candle.open).abs() > 0.1 * range {
|
||||
return Some(0.0);
|
||||
}
|
||||
let upper = candle.high - candle.open.max(candle.close);
|
||||
let lower = candle.open.min(candle.close) - candle.low;
|
||||
if upper >= 0.3 * range && lower >= 0.3 * range {
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"LongLeggedDoji"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = LongLeggedDoji::new();
|
||||
assert_eq!(t.name(), "LongLeggedDoji");
|
||||
assert_eq!(t.warmup_period(), 1);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_legged_is_plus_one() {
|
||||
let mut t = LongLeggedDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 12.0, 8.0, 10.05, 0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_sided_shadow_yields_zero() {
|
||||
let mut t = LongLeggedDoji::new();
|
||||
// Dragonfly shape: long lower shadow but no upper -> not long-legged.
|
||||
assert_eq!(t.update(c(10.0, 10.05, 6.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_doji_yields_zero() {
|
||||
let mut t = LongLeggedDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 12.0, 8.0, 11.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = LongLeggedDoji::new();
|
||||
assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), 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 + 3.0, base - 3.0, base + 0.05, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = LongLeggedDoji::new();
|
||||
let mut b = LongLeggedDoji::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = LongLeggedDoji::new();
|
||||
t.update(c(10.0, 12.0, 8.0, 10.05, 0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -63,10 +63,12 @@ mod demark_pivots;
|
||||
mod depth_slope;
|
||||
mod detrended_std_dev;
|
||||
mod doji;
|
||||
mod doji_star;
|
||||
mod donchian;
|
||||
mod donchian_stop;
|
||||
mod double_bollinger;
|
||||
mod dpo;
|
||||
mod dragonfly_doji;
|
||||
mod drawdown_duration;
|
||||
mod ease_of_movement;
|
||||
mod effective_spread;
|
||||
@@ -89,6 +91,7 @@ mod funding_rate_mean;
|
||||
mod funding_rate_zscore;
|
||||
mod gain_loss_ratio;
|
||||
mod garman_klass;
|
||||
mod gravestone_doji;
|
||||
mod hammer;
|
||||
mod hanging_man;
|
||||
mod harami;
|
||||
@@ -122,6 +125,7 @@ mod linreg_angle;
|
||||
mod linreg_channel;
|
||||
mod linreg_slope;
|
||||
mod liquidation_features;
|
||||
mod long_legged_doji;
|
||||
mod long_short_ratio;
|
||||
mod ma_envelope;
|
||||
mod macd;
|
||||
@@ -168,6 +172,7 @@ mod realized_spread;
|
||||
mod recovery_factor;
|
||||
mod relative_strength_ab;
|
||||
mod renko_trailing_stop;
|
||||
mod rickshaw_man;
|
||||
mod roc;
|
||||
mod rogers_satchell;
|
||||
mod roofing_filter;
|
||||
@@ -317,10 +322,12 @@ pub use demark_pivots::{DemarkPivots, DemarkPivotsOutput};
|
||||
pub use depth_slope::DepthSlope;
|
||||
pub use detrended_std_dev::DetrendedStdDev;
|
||||
pub use doji::Doji;
|
||||
pub use doji_star::DojiStar;
|
||||
pub use donchian::{Donchian, DonchianOutput};
|
||||
pub use donchian_stop::{DonchianStop, DonchianStopOutput};
|
||||
pub use double_bollinger::{DoubleBollinger, DoubleBollingerOutput};
|
||||
pub use dpo::Dpo;
|
||||
pub use dragonfly_doji::DragonflyDoji;
|
||||
pub use drawdown_duration::DrawdownDuration;
|
||||
pub use ease_of_movement::EaseOfMovement;
|
||||
pub use effective_spread::EffectiveSpread;
|
||||
@@ -343,6 +350,7 @@ pub use funding_rate_mean::FundingRateMean;
|
||||
pub use funding_rate_zscore::FundingRateZScore;
|
||||
pub use gain_loss_ratio::GainLossRatio;
|
||||
pub use garman_klass::GarmanKlassVolatility;
|
||||
pub use gravestone_doji::GravestoneDoji;
|
||||
pub use hammer::Hammer;
|
||||
pub use hanging_man::HangingMan;
|
||||
pub use harami::Harami;
|
||||
@@ -376,6 +384,7 @@ pub use linreg_angle::LinRegAngle;
|
||||
pub use linreg_channel::{LinRegChannel, LinRegChannelOutput};
|
||||
pub use linreg_slope::LinRegSlope;
|
||||
pub use liquidation_features::{LiquidationFeatures, LiquidationFeaturesOutput};
|
||||
pub use long_legged_doji::LongLeggedDoji;
|
||||
pub use long_short_ratio::LongShortRatio;
|
||||
pub use ma_envelope::{MaEnvelope, MaEnvelopeOutput};
|
||||
pub use macd::{MacdIndicator, MacdOutput};
|
||||
@@ -422,6 +431,7 @@ pub use realized_spread::RealizedSpread;
|
||||
pub use recovery_factor::RecoveryFactor;
|
||||
pub use relative_strength_ab::{RelativeStrengthAB, RelativeStrengthOutput};
|
||||
pub use renko_trailing_stop::RenkoTrailingStop;
|
||||
pub use rickshaw_man::RickshawMan;
|
||||
pub use roc::Roc;
|
||||
pub use rogers_satchell::RogersSatchellVolatility;
|
||||
pub use roofing_filter::RoofingFilter;
|
||||
@@ -785,6 +795,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"BeltHold",
|
||||
"Breakaway",
|
||||
"Counterattack",
|
||||
"DojiStar",
|
||||
"DragonflyDoji",
|
||||
"GravestoneDoji",
|
||||
"LongLeggedDoji",
|
||||
"RickshawMan",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -876,6 +891,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, 249, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 254, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
//! Rickshaw Man candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Rickshaw Man — a single-bar indecision signal. A long-legged doji whose tiny
|
||||
/// body sits near the *middle* of a wide range, the most balanced form of
|
||||
/// indecision: neither side controlled the close and the midpoint pins it.
|
||||
///
|
||||
/// ```text
|
||||
/// range = high − low
|
||||
/// doji = |close − open| <= 0.1 * range
|
||||
/// long upper = high − max(open, close) >= 0.3 * range
|
||||
/// long lower = min(open, close) − low >= 0.3 * range
|
||||
/// centred body = body midpoint within the central 40–60 % of the range
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the rickshaw man prints and `0.0` otherwise. This is a
|
||||
/// non-directional indecision flag — it never emits `−1.0`. A rickshaw man is a
|
||||
/// special case of a long-legged doji (the body additionally sits at the centre),
|
||||
/// so both detectors may flag the same bar. Body and shadow thresholds follow the
|
||||
/// geometric house style (fixed fractions of the bar range) rather than TA-Lib's
|
||||
/// rolling averages. 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` detected, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, RickshawMan};
|
||||
///
|
||||
/// let mut indicator = RickshawMan::new();
|
||||
/// // Tiny body centred in a wide range, long shadows both sides.
|
||||
/// let candle = Candle::new(10.0, 12.0, 8.0, 10.0, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RickshawMan {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl RickshawMan {
|
||||
/// Construct a new Rickshaw Man detector.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for RickshawMan {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let range = candle.high - candle.low;
|
||||
if range <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
if (candle.close - candle.open).abs() > 0.1 * range {
|
||||
return Some(0.0);
|
||||
}
|
||||
let upper = candle.high - candle.open.max(candle.close);
|
||||
let lower = candle.open.min(candle.close) - candle.low;
|
||||
let body_mid = 0.5 * (candle.open + candle.close);
|
||||
let pos = (body_mid - candle.low) / range;
|
||||
if upper >= 0.3 * range && lower >= 0.3 * range && (0.4..=0.6).contains(&pos) {
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"RickshawMan"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = RickshawMan::new();
|
||||
assert_eq!(t.name(), "RickshawMan");
|
||||
assert_eq!(t.warmup_period(), 1);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rickshaw_is_plus_one() {
|
||||
let mut t = RickshawMan::new();
|
||||
assert_eq!(t.update(c(10.0, 12.0, 8.0, 10.0, 0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn off_centre_body_yields_zero() {
|
||||
let mut t = RickshawMan::new();
|
||||
// Long-legged but the body sits near the top, not the middle.
|
||||
assert_eq!(t.update(c(11.4, 12.0, 8.0, 11.45, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_sided_shadow_yields_zero() {
|
||||
let mut t = RickshawMan::new();
|
||||
// Dragonfly shape: no upper shadow -> not a rickshaw man.
|
||||
assert_eq!(t.update(c(10.0, 10.05, 6.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_doji_yields_zero() {
|
||||
let mut t = RickshawMan::new();
|
||||
assert_eq!(t.update(c(9.0, 12.0, 8.0, 11.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = RickshawMan::new();
|
||||
assert_eq!(t.update(c(10.0, 10.0, 10.0, 10.0, 0)), 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 + 2.0, base - 2.0, base + 0.05, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = RickshawMan::new();
|
||||
let mut b = RickshawMan::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = RickshawMan::new();
|
||||
t.update(c(10.0, 12.0, 8.0, 10.0, 0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user