feat: TA-Lib candlestick patterns — gap-three-methods/stalled/stick-sandwich/takuri (part 8 of 9) (#140)
Adds five TA-Lib candlestick patterns, each a streaming `Indicator<Input = Candle, Output = f64>` emitting the family's uniform `±1.0 / 0.0` sign convention, fully wired across the Rust core, Python / Node / WASM bindings, fuzz target and reference tests. - **Upside Gap Three Methods** (`CDLXSIDEGAP3METHODS`) — a 3-bar bullish continuation: two white candles gap up, then a black candle opens within the second body and closes within the first; bullish +1. - **Downside Gap Three Methods** (`CDLXSIDEGAP3METHODS`) — the bearish mirror: two black candles gap down, then a white candle opens within the second body and closes within the first; bearish -1. - **Stalled Pattern** (`CDLSTALLEDPATTERN`) — a 3-bar bearish reversal warning: two long white candles then a small white candle riding the shoulder, signalling the rally is stalling; bearish -1. - **Stick Sandwich** (`CDLSTICKSANDWICH`) — a 3-bar bullish reversal: two black candles closing at the same level sandwich a white candle, marking a support floor; bullish +1. - **Takuri** (`CDLTAKURI`) — a single-bar bullish reversal, a strict Dragonfly Doji with a negligible upper shadow and very long lower shadow; bullish +1. Body and shadow thresholds follow the geometric house style (fixed fractions of the bar range) rather than TA-Lib's rolling averages. Upside / Downside Gap Three Methods share the `CDLXSIDEGAP3METHODS` code, so the second carries a manual CHANGELOG entry (as with Rising / Falling Three Methods). Counter 279 → 284 (mod-count == lib counted block; FAMILIES total 274 → 279). Stacked on #139 (`feat/cdl-lines`); base retargets to `main` once the predecessor merges.
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
//! Downside Gap Three Methods candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Downside Gap Three Methods — a 3-bar bearish continuation. Two black candles
|
||||
/// decline with a downside body gap between them, then a white candle opens inside
|
||||
/// the second body and closes inside the first body, partially filling the gap
|
||||
/// without erasing the prior decline.
|
||||
///
|
||||
/// ```text
|
||||
/// bar1 black, bar2 black
|
||||
/// downside body gap: open2 < close1 (bar2's body sits entirely below bar1's)
|
||||
/// bar3 white, opens within bar2's body and closes within bar1's body
|
||||
/// ```
|
||||
///
|
||||
/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Downside Gap
|
||||
/// Three Methods is a single-direction (bearish-only) continuation, so it never
|
||||
/// emits `+1.0`; its bullish mirror is [`crate::UpsideGapThreeMethods`]. 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, DownsideGapThreeMethods, Indicator};
|
||||
///
|
||||
/// let mut indicator = DownsideGapThreeMethods::new();
|
||||
/// indicator.update(Candle::new(13.0, 13.2, 11.8, 12.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(11.0, 11.1, 9.8, 10.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(10.5, 12.6, 10.4, 12.5, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DownsideGapThreeMethods {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl DownsideGapThreeMethods {
|
||||
/// Construct a new Downside Gap Three Methods detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
c1: None,
|
||||
c2: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for DownsideGapThreeMethods {
|
||||
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;
|
||||
self.c1 = self.c2;
|
||||
self.c2 = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
// bar1 and bar2 are both black.
|
||||
if bar1.close >= bar1.open || bar2.close >= bar2.open {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Downside body gap: bar2's body sits entirely below bar1's.
|
||||
if bar2.open >= bar1.close {
|
||||
return Some(0.0);
|
||||
}
|
||||
// bar3 is white.
|
||||
if candle.close <= candle.open {
|
||||
return Some(0.0);
|
||||
}
|
||||
// bar3 opens within bar2's body and closes within bar1's body.
|
||||
if candle.open > bar2.close
|
||||
&& candle.open < bar2.open
|
||||
&& candle.close > bar1.close
|
||||
&& candle.close < bar1.open
|
||||
{
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"DownsideGapThreeMethods"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = DownsideGapThreeMethods::new();
|
||||
assert_eq!(t.name(), "DownsideGapThreeMethods");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn downside_gap_three_methods_is_minus_one() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
assert_eq!(t.update(c(13.0, 13.2, 11.8, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 11.1, 9.8, 10.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(10.5, 12.6, 10.4, 12.5, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
assert_eq!(t.update(c(13.0, 13.2, 11.8, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 11.1, 9.8, 10.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_black_first_bars_yield_zero() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
// bar1 is white.
|
||||
t.update(c(11.0, 13.2, 10.8, 13.0, 0));
|
||||
t.update(c(11.0, 11.1, 9.8, 10.0, 1));
|
||||
assert_eq!(t.update(c(10.5, 12.6, 10.4, 12.5, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gap_yields_zero() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
t.update(c(13.0, 13.2, 11.8, 12.0, 0));
|
||||
// bar2 opens above bar1's close -> no downside body gap.
|
||||
t.update(c(12.5, 12.6, 11.4, 11.5, 1));
|
||||
assert_eq!(t.update(c(11.5, 12.6, 11.4, 12.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_bar_not_white_yields_zero() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
t.update(c(13.0, 13.2, 11.8, 12.0, 0));
|
||||
t.update(c(11.0, 11.1, 9.8, 10.0, 1));
|
||||
// bar3 black.
|
||||
assert_eq!(t.update(c(12.5, 12.6, 10.4, 10.5, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_bar_outside_bodies_yields_zero() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
t.update(c(13.0, 13.2, 11.8, 12.0, 0));
|
||||
t.update(c(11.0, 11.1, 9.8, 10.0, 1));
|
||||
// bar3 white but closes above bar1's body.
|
||||
assert_eq!(t.update(c(10.5, 14.0, 10.4, 13.5, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 200.0 - i as f64;
|
||||
c(base, base + 0.1, base - 5.2, base - 5.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = DownsideGapThreeMethods::new();
|
||||
let mut b = DownsideGapThreeMethods::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = DownsideGapThreeMethods::new();
|
||||
t.update(c(13.0, 13.2, 11.8, 12.0, 0));
|
||||
t.update(c(11.0, 11.1, 9.8, 10.0, 1));
|
||||
t.update(c(10.5, 12.6, 10.4, 12.5, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(13.0, 13.2, 11.8, 12.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,7 @@ mod doji_star;
|
||||
mod donchian;
|
||||
mod donchian_stop;
|
||||
mod double_bollinger;
|
||||
mod downside_gap_three_methods;
|
||||
mod dpo;
|
||||
mod dragonfly_doji;
|
||||
mod drawdown_duration;
|
||||
@@ -210,18 +211,21 @@ mod smma;
|
||||
mod sortino_ratio;
|
||||
mod spearman_correlation;
|
||||
mod spinning_top;
|
||||
mod stalled_pattern;
|
||||
mod standard_error;
|
||||
mod standard_error_bands;
|
||||
mod starc_bands;
|
||||
mod stc;
|
||||
mod std_dev;
|
||||
mod step_trailing_stop;
|
||||
mod stick_sandwich;
|
||||
mod stoch_rsi;
|
||||
mod stochastic;
|
||||
mod super_smoother;
|
||||
mod super_trend;
|
||||
mod t3;
|
||||
mod taker_buy_sell_ratio;
|
||||
mod takuri;
|
||||
mod td_combo;
|
||||
mod td_countdown;
|
||||
mod td_demarker;
|
||||
@@ -256,6 +260,7 @@ mod two_crows;
|
||||
mod typical_price;
|
||||
mod ulcer_index;
|
||||
mod ultimate_oscillator;
|
||||
mod upside_gap_three_methods;
|
||||
mod upside_gap_two_crows;
|
||||
mod value_area;
|
||||
mod value_at_risk;
|
||||
@@ -346,6 +351,7 @@ pub use doji_star::DojiStar;
|
||||
pub use donchian::{Donchian, DonchianOutput};
|
||||
pub use donchian_stop::{DonchianStop, DonchianStopOutput};
|
||||
pub use double_bollinger::{DoubleBollinger, DoubleBollingerOutput};
|
||||
pub use downside_gap_three_methods::DownsideGapThreeMethods;
|
||||
pub use dpo::Dpo;
|
||||
pub use dragonfly_doji::DragonflyDoji;
|
||||
pub use drawdown_duration::DrawdownDuration;
|
||||
@@ -489,18 +495,21 @@ pub use smma::Smma;
|
||||
pub use sortino_ratio::SortinoRatio;
|
||||
pub use spearman_correlation::SpearmanCorrelation;
|
||||
pub use spinning_top::SpinningTop;
|
||||
pub use stalled_pattern::StalledPattern;
|
||||
pub use standard_error::StandardError;
|
||||
pub use standard_error_bands::{StandardErrorBands, StandardErrorBandsOutput};
|
||||
pub use starc_bands::{StarcBands, StarcBandsOutput};
|
||||
pub use stc::Stc;
|
||||
pub use std_dev::StdDev;
|
||||
pub use step_trailing_stop::StepTrailingStop;
|
||||
pub use stick_sandwich::StickSandwich;
|
||||
pub use stoch_rsi::StochRsi;
|
||||
pub use stochastic::{Stochastic, StochasticOutput};
|
||||
pub use super_smoother::SuperSmoother;
|
||||
pub use super_trend::{SuperTrend, SuperTrendOutput};
|
||||
pub use t3::T3;
|
||||
pub use taker_buy_sell_ratio::TakerBuySellRatio;
|
||||
pub use takuri::Takuri;
|
||||
pub use td_combo::TdCombo;
|
||||
pub use td_countdown::TdCountdown;
|
||||
pub use td_demarker::TdDeMarker;
|
||||
@@ -535,6 +544,7 @@ pub use two_crows::TwoCrows;
|
||||
pub use typical_price::TypicalPrice;
|
||||
pub use ulcer_index::UlcerIndex;
|
||||
pub use ultimate_oscillator::UltimateOscillator;
|
||||
pub use upside_gap_three_methods::UpsideGapThreeMethods;
|
||||
pub use upside_gap_two_crows::UpsideGapTwoCrows;
|
||||
pub use value_area::{ValueArea, ValueAreaOutput};
|
||||
pub use value_at_risk::ValueAtRisk;
|
||||
@@ -860,6 +870,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"ShortLine",
|
||||
"RisingThreeMethods",
|
||||
"FallingThreeMethods",
|
||||
"UpsideGapThreeMethods",
|
||||
"DownsideGapThreeMethods",
|
||||
"StalledPattern",
|
||||
"StickSandwich",
|
||||
"Takuri",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -951,6 +966,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, 274, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 279, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
//! Stalled Pattern (Deliberation) candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Stalled Pattern (also called Deliberation) — a 3-bar bearish reversal warning.
|
||||
/// Two long white candles push higher, then a small-bodied white candle opens at
|
||||
/// or near the top of the second body and barely advances — the rally is running
|
||||
/// out of breath, hinting that buyers are losing control.
|
||||
///
|
||||
/// ```text
|
||||
/// long body = |close − open| >= 0.5 * (high − low)
|
||||
/// small body = |close − open| <= 0.3 * (high − low)
|
||||
/// bar1, bar2 long white; bar3 small white
|
||||
/// rising closes: close3 > close2 > close1
|
||||
/// bar3 rides the shoulder: open3 >= close2 − 0.1 * (high2 − low2)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Stalled Pattern
|
||||
/// is a single-direction (bearish-only) warning, so it never emits `+1.0`. The
|
||||
/// first two bars always return `0.0` because the three-bar window is not yet
|
||||
/// filled. Body thresholds follow the geometric house style 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, Indicator, StalledPattern};
|
||||
///
|
||||
/// let mut indicator = StalledPattern::new();
|
||||
/// indicator.update(Candle::new(10.0, 12.05, 9.9, 12.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(11.0, 14.05, 10.9, 14.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(14.0, 14.6, 13.95, 14.15, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StalledPattern {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl StalledPattern {
|
||||
/// Construct a new Stalled Pattern detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
c1: None,
|
||||
c2: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for StalledPattern {
|
||||
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;
|
||||
self.c1 = self.c2;
|
||||
self.c2 = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range1 = bar1.high - bar1.low;
|
||||
let range2 = bar2.high - bar2.low;
|
||||
let range3 = candle.high - candle.low;
|
||||
if range1 <= 0.0 || range2 <= 0.0 || range3 <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
// All three candles are white.
|
||||
if bar1.close <= bar1.open || bar2.close <= bar2.open || candle.close <= candle.open {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Rising closes.
|
||||
if candle.close <= bar2.close || bar2.close <= bar1.close {
|
||||
return Some(0.0);
|
||||
}
|
||||
// bar1 and bar2 are long bodies.
|
||||
if bar1.close - bar1.open < 0.5 * range1 || bar2.close - bar2.open < 0.5 * range2 {
|
||||
return Some(0.0);
|
||||
}
|
||||
// bar3 is a small body.
|
||||
if candle.close - candle.open > 0.3 * range3 {
|
||||
return Some(0.0);
|
||||
}
|
||||
// bar3 opens at or near the top of bar2's body (rides the shoulder).
|
||||
if candle.open >= bar2.close - 0.1 * range2 {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"StalledPattern"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = StalledPattern::new();
|
||||
assert_eq!(t.name(), "StalledPattern");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stalled_pattern_is_minus_one() {
|
||||
let mut t = StalledPattern::new();
|
||||
assert_eq!(t.update(c(10.0, 12.05, 9.9, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 14.05, 10.9, 14.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(14.0, 14.6, 13.95, 14.15, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = StalledPattern::new();
|
||||
assert_eq!(t.update(c(10.0, 12.05, 9.9, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 14.05, 10.9, 14.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = StalledPattern::new();
|
||||
t.update(c(10.0, 12.05, 9.9, 12.0, 0));
|
||||
t.update(c(11.0, 14.05, 10.9, 14.0, 1));
|
||||
// bar3 has zero range.
|
||||
assert_eq!(t.update(c(14.0, 14.0, 14.0, 14.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_white_yields_zero() {
|
||||
let mut t = StalledPattern::new();
|
||||
t.update(c(10.0, 12.05, 9.9, 12.0, 0));
|
||||
t.update(c(11.0, 14.05, 10.9, 14.0, 1));
|
||||
// bar3 is black.
|
||||
assert_eq!(t.update(c(14.2, 14.6, 13.95, 14.05, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_rising_closes_yield_zero() {
|
||||
let mut t = StalledPattern::new();
|
||||
t.update(c(10.0, 12.05, 9.9, 12.0, 0));
|
||||
t.update(c(11.0, 14.05, 10.9, 14.0, 1));
|
||||
// bar3 closes below bar2's close (white but not advancing).
|
||||
assert_eq!(t.update(c(13.5, 14.0, 13.45, 13.6, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_first_bodies_yield_zero() {
|
||||
let mut t = StalledPattern::new();
|
||||
// bar1 is white but its body is short relative to range.
|
||||
t.update(c(11.5, 14.0, 10.0, 12.0, 0));
|
||||
t.update(c(11.0, 14.05, 10.9, 14.0, 1));
|
||||
assert_eq!(t.update(c(14.0, 14.6, 13.95, 14.15, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_third_body_yields_zero() {
|
||||
let mut t = StalledPattern::new();
|
||||
t.update(c(10.0, 12.05, 9.9, 12.0, 0));
|
||||
t.update(c(11.0, 14.05, 10.9, 14.0, 1));
|
||||
// bar3 has a large body (not a small stalling candle).
|
||||
assert_eq!(t.update(c(14.0, 16.05, 13.95, 16.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_bar_off_shoulder_yields_zero() {
|
||||
let mut t = StalledPattern::new();
|
||||
t.update(c(10.0, 12.05, 9.9, 12.0, 0));
|
||||
t.update(c(11.0, 14.05, 10.9, 14.0, 1));
|
||||
// bar3 is a small white candle but opens well below bar2's close,
|
||||
// so it is not riding the shoulder.
|
||||
assert_eq!(t.update(c(13.6, 14.1, 12.55, 14.05, 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 + 2.05, base - 0.1, base + 2.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = StalledPattern::new();
|
||||
let mut b = StalledPattern::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = StalledPattern::new();
|
||||
t.update(c(10.0, 12.05, 9.9, 12.0, 0));
|
||||
t.update(c(11.0, 14.05, 10.9, 14.0, 1));
|
||||
t.update(c(14.0, 14.6, 13.95, 14.15, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 12.05, 9.9, 12.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
//! Stick Sandwich candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Stick Sandwich — a 3-bar bullish reversal. A black candle is followed by a
|
||||
/// white candle that trades entirely above the first close, then a second black
|
||||
/// candle drives price back down to close at the same level as the first. The
|
||||
/// matching closes "sandwich" the white candle and mark a support floor.
|
||||
///
|
||||
/// ```text
|
||||
/// bar1 black, bar2 white, bar3 black
|
||||
/// bar2 trades above bar1's close: low2 > close1
|
||||
/// matching closes: |close3 − close1| <= 0.1 * (high1 − low1)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Stick Sandwich
|
||||
/// is a single-direction (bullish-only) reversal, so it never emits `−1.0`. The
|
||||
/// first two bars always return `0.0` because the three-bar window is not yet
|
||||
/// filled. The matching-close tolerance follows the geometric house style (a fixed
|
||||
/// fraction of the first bar's 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, Indicator, StickSandwich};
|
||||
///
|
||||
/// let mut indicator = StickSandwich::new();
|
||||
/// indicator.update(Candle::new(12.0, 12.1, 9.9, 10.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(10.5, 11.6, 10.4, 11.5, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(11.5, 11.6, 9.9, 10.0, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StickSandwich {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl StickSandwich {
|
||||
/// Construct a new Stick Sandwich detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
c1: None,
|
||||
c2: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for StickSandwich {
|
||||
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;
|
||||
self.c1 = self.c2;
|
||||
self.c2 = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
// bar1 black, bar2 white, bar3 black.
|
||||
if bar1.close >= bar1.open || bar2.close <= bar2.open || candle.close >= candle.open {
|
||||
return Some(0.0);
|
||||
}
|
||||
// The white candle trades entirely above the first close.
|
||||
if bar2.low <= bar1.close {
|
||||
return Some(0.0);
|
||||
}
|
||||
// The two black candles close at the same level (the sandwich).
|
||||
let range1 = bar1.high - bar1.low;
|
||||
if (candle.close - bar1.close).abs() <= 0.1 * range1 {
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"StickSandwich"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = StickSandwich::new();
|
||||
assert_eq!(t.name(), "StickSandwich");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stick_sandwich_is_plus_one() {
|
||||
let mut t = StickSandwich::new();
|
||||
assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(10.5, 11.6, 10.4, 11.5, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.5, 11.6, 9.9, 10.0, 2)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = StickSandwich::new();
|
||||
assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(10.5, 11.6, 10.4, 11.5, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_candle_not_black_yields_zero() {
|
||||
let mut t = StickSandwich::new();
|
||||
// bar1 white.
|
||||
t.update(c(9.9, 12.1, 9.8, 10.0, 0));
|
||||
t.update(c(10.5, 11.6, 10.4, 11.5, 1));
|
||||
assert_eq!(t.update(c(11.5, 11.6, 9.9, 10.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_candle_not_white_yields_zero() {
|
||||
let mut t = StickSandwich::new();
|
||||
t.update(c(12.0, 12.1, 9.9, 10.0, 0));
|
||||
// bar2 black.
|
||||
t.update(c(11.5, 11.6, 10.4, 10.5, 1));
|
||||
assert_eq!(t.update(c(11.5, 11.6, 9.9, 10.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_candle_not_black_yields_zero() {
|
||||
let mut t = StickSandwich::new();
|
||||
t.update(c(12.0, 12.1, 9.9, 10.0, 0));
|
||||
t.update(c(10.5, 11.6, 10.4, 11.5, 1));
|
||||
// bar3 white.
|
||||
assert_eq!(t.update(c(9.9, 11.6, 9.8, 10.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_low_not_above_first_close_yields_zero() {
|
||||
let mut t = StickSandwich::new();
|
||||
t.update(c(12.0, 12.1, 9.9, 10.0, 0));
|
||||
// bar2 white but dips below bar1's close.
|
||||
t.update(c(10.5, 11.6, 9.0, 11.5, 1));
|
||||
assert_eq!(t.update(c(11.5, 11.6, 9.9, 10.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mismatched_closes_yield_zero() {
|
||||
let mut t = StickSandwich::new();
|
||||
t.update(c(12.0, 12.1, 9.9, 10.0, 0));
|
||||
t.update(c(10.5, 11.6, 10.4, 11.5, 1));
|
||||
// bar3 black but closes well away from bar1's close.
|
||||
assert_eq!(t.update(c(11.5, 11.6, 7.9, 8.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 + 2.0, base + 2.1, base - 0.1, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = StickSandwich::new();
|
||||
let mut b = StickSandwich::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = StickSandwich::new();
|
||||
t.update(c(12.0, 12.1, 9.9, 10.0, 0));
|
||||
t.update(c(10.5, 11.6, 10.4, 11.5, 1));
|
||||
t.update(c(11.5, 11.6, 9.9, 10.0, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(12.0, 12.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//! Takuri candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Takuri — a single-bar bullish reversal, a stricter Dragonfly Doji. Open, close,
|
||||
/// and high sit at the very top of the bar with a negligible upper shadow, while an
|
||||
/// exceptionally long lower shadow shows price was driven sharply down and then bid
|
||||
/// all the way back — an emphatic rejection of the lows.
|
||||
///
|
||||
/// ```text
|
||||
/// range = high − low
|
||||
/// doji = |close − open| <= 0.1 * range
|
||||
/// negligible upper = high − max(open, close) <= 0.05 * range
|
||||
/// very long lower = min(open, close) − low >= 0.7 * range
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the Takuri prints and `0.0` otherwise. Takuri is a
|
||||
/// single-direction (bullish-only) shape, so it never emits `−1.0`. Its tighter
|
||||
/// upper-shadow and longer lower-shadow thresholds make it a strict subset of
|
||||
/// [`crate::DragonflyDoji`]. 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, Indicator, Takuri};
|
||||
///
|
||||
/// let mut indicator = Takuri::new();
|
||||
/// // Body at the top, very long lower shadow.
|
||||
/// let candle = Candle::new(10.0, 10.05, 7.0, 10.0, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Takuri {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Takuri {
|
||||
/// Construct a new Takuri detector.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Takuri {
|
||||
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.05 * range && lower >= 0.7 * 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 {
|
||||
"Takuri"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = Takuri::new();
|
||||
assert_eq!(t.name(), "Takuri");
|
||||
assert_eq!(t.warmup_period(), 1);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn takuri_is_plus_one() {
|
||||
let mut t = Takuri::new();
|
||||
assert_eq!(t.update(c(10.0, 10.05, 7.0, 10.0, 0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_doji_body_yields_zero() {
|
||||
let mut t = Takuri::new();
|
||||
// Large body -> not a doji.
|
||||
assert_eq!(t.update(c(10.0, 12.0, 7.0, 11.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upper_shadow_yields_zero() {
|
||||
let mut t = Takuri::new();
|
||||
// Long upper shadow -> not a Takuri.
|
||||
assert_eq!(t.update(c(10.0, 14.0, 7.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragonfly_but_not_takuri_yields_zero() {
|
||||
let mut t = Takuri::new();
|
||||
// Upper shadow ~0.07 of range: a Dragonfly Doji, but exceeds Takuri's
|
||||
// tighter 0.05 ceiling.
|
||||
assert_eq!(t.update(c(10.0, 10.24, 7.0, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = Takuri::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.02, base - 4.0, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = Takuri::new();
|
||||
let mut b = Takuri::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = Takuri::new();
|
||||
t.update(c(10.0, 10.05, 7.0, 10.0, 0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
//! Upside Gap Three Methods candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Upside Gap Three Methods — a 3-bar bullish continuation. Two white candles
|
||||
/// advance with an upside body gap between them, then a black candle opens inside
|
||||
/// the second body and closes inside the first body, partially filling the gap
|
||||
/// without erasing the prior advance.
|
||||
///
|
||||
/// ```text
|
||||
/// bar1 white, bar2 white
|
||||
/// upside body gap: open2 > close1 (bar2's body sits entirely above bar1's)
|
||||
/// bar3 black, opens within bar2's body and closes within bar1's body
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Upside Gap
|
||||
/// Three Methods is a single-direction (bullish-only) continuation, so it never
|
||||
/// emits `−1.0`; its bearish mirror is [`crate::DownsideGapThreeMethods`]. 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` 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, UpsideGapThreeMethods};
|
||||
///
|
||||
/// let mut indicator = UpsideGapThreeMethods::new();
|
||||
/// indicator.update(Candle::new(10.0, 11.2, 9.8, 11.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(12.0, 13.2, 11.9, 13.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(12.5, 12.6, 10.4, 10.5, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpsideGapThreeMethods {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl UpsideGapThreeMethods {
|
||||
/// Construct a new Upside Gap Three Methods detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
c1: None,
|
||||
c2: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for UpsideGapThreeMethods {
|
||||
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;
|
||||
self.c1 = self.c2;
|
||||
self.c2 = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
// bar1 and bar2 are both white.
|
||||
if bar1.close <= bar1.open || bar2.close <= bar2.open {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Upside body gap: bar2's body sits entirely above bar1's.
|
||||
if bar2.open <= bar1.close {
|
||||
return Some(0.0);
|
||||
}
|
||||
// bar3 is black.
|
||||
if candle.close >= candle.open {
|
||||
return Some(0.0);
|
||||
}
|
||||
// bar3 opens within bar2's body and closes within bar1's body.
|
||||
if 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.c1 = None;
|
||||
self.c2 = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"UpsideGapThreeMethods"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = UpsideGapThreeMethods::new();
|
||||
assert_eq!(t.name(), "UpsideGapThreeMethods");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upside_gap_three_methods_is_plus_one() {
|
||||
let mut t = UpsideGapThreeMethods::new();
|
||||
assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.0, 13.2, 11.9, 13.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.5, 12.6, 10.4, 10.5, 2)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = UpsideGapThreeMethods::new();
|
||||
assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.0, 13.2, 11.9, 13.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_white_first_bars_yield_zero() {
|
||||
let mut t = UpsideGapThreeMethods::new();
|
||||
// bar1 is black.
|
||||
t.update(c(11.0, 11.2, 9.8, 10.0, 0));
|
||||
t.update(c(12.0, 13.2, 11.9, 13.0, 1));
|
||||
assert_eq!(t.update(c(12.5, 12.6, 10.4, 10.5, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gap_yields_zero() {
|
||||
let mut t = UpsideGapThreeMethods::new();
|
||||
t.update(c(10.0, 13.2, 9.8, 13.0, 0));
|
||||
// bar2 opens below bar1's close -> no upside body gap.
|
||||
t.update(c(11.0, 13.2, 10.9, 12.5, 1));
|
||||
assert_eq!(t.update(c(12.0, 12.6, 10.4, 10.5, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_bar_not_black_yields_zero() {
|
||||
let mut t = UpsideGapThreeMethods::new();
|
||||
t.update(c(10.0, 11.2, 9.8, 11.0, 0));
|
||||
t.update(c(12.0, 13.2, 11.9, 13.0, 1));
|
||||
// bar3 white.
|
||||
assert_eq!(t.update(c(10.5, 12.6, 10.4, 12.5, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_bar_outside_bodies_yields_zero() {
|
||||
let mut t = UpsideGapThreeMethods::new();
|
||||
t.update(c(10.0, 11.2, 9.8, 11.0, 0));
|
||||
t.update(c(12.0, 13.2, 11.9, 13.0, 1));
|
||||
// bar3 black but closes below bar1's body.
|
||||
assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.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 + 5.2, base - 0.1, base + 5.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = UpsideGapThreeMethods::new();
|
||||
let mut b = UpsideGapThreeMethods::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = UpsideGapThreeMethods::new();
|
||||
t.update(c(10.0, 11.2, 9.8, 11.0, 0));
|
||||
t.update(c(12.0, 13.2, 11.9, 13.0, 1));
|
||||
t.update(c(12.5, 12.6, 10.4, 10.5, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 11.2, 9.8, 11.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -65,19 +65,20 @@ pub use indicators::{
|
||||
ConditionalValueAtRisk, ConnorsRsi, Coppock, Counterattack, CumulativeVolumeDelta,
|
||||
CyberneticCycle, Decycler, DecyclerOscillator, Dema, DemandIndex, DemarkPivots,
|
||||
DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, DojiStar, Donchian, DonchianOutput,
|
||||
DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, Dpo, DragonflyDoji,
|
||||
DrawdownDuration, EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
||||
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, Fama,
|
||||
FibonacciPivots, FibonacciPivotsOutput, FisherTransform, Footprint, FootprintOutput,
|
||||
ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate,
|
||||
FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, GarmanKlassVolatility,
|
||||
GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput, HiLoActivator,
|
||||
HighWave, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
|
||||
HomingPigeon, HurstChannel, HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput,
|
||||
IdenticalThreeCrows, InNeck, Inertia, InformationRatio, InitialBalance, InitialBalanceOutput,
|
||||
InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma, Kama, KellyCriterion,
|
||||
Keltner, KeltnerOutput, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda,
|
||||
LadderBottom, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||
DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput,
|
||||
DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, EaseOfMovement, EffectiveSpread,
|
||||
EhlersStochastic, ElderImpulse, Ema, EmpiricalModeDecomposition, Engulfing, EveningDojiStar,
|
||||
Evwma, FallingThreeMethods, Fama, FibonacciPivots, FibonacciPivotsOutput, FisherTransform,
|
||||
Footprint, FootprintOutput, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama,
|
||||
FundingBasis, FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio,
|
||||
GapSideBySideWhite, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami,
|
||||
HeikinAshi, HeikinAshiOutput, HiLoActivator, HighWave, Hikkake, HikkakeModified,
|
||||
HilbertDominantCycle, HistoricalVolatility, Hma, HomingPigeon, HurstChannel,
|
||||
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
|
||||
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
|
||||
InverseFisherTransform, InvertedHammer, Jma, Kama, KellyCriterion, Keltner, KeltnerOutput,
|
||||
Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom,
|
||||
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||
LinRegChannel, LinRegChannelOutput, LinRegSlope, LinearRegression, LiquidationFeatures,
|
||||
LiquidationFeaturesOutput, LongLeggedDoji, LongLine, LongShortRatio, MaEnvelope,
|
||||
MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
|
||||
@@ -92,20 +93,21 @@ pub use indicators::{
|
||||
RisingThreeMethods, Roc, RogersSatchellVolatility, RollingVwap, RoofingFilter, Rsi, Rvi,
|
||||
RviVolatility, Rwi, RwiOutput, SeparatingLines, SharpeRatio, ShootingStar, ShortLine,
|
||||
SignedVolume, SineWave, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation,
|
||||
SpinningTop, StandardError, StandardErrorBands, StandardErrorBandsOutput, StarcBands,
|
||||
StarcBandsOutput, Stc, StdDev, StepTrailingStop, StochRsi, Stochastic, StochasticOutput,
|
||||
SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio, TdCombo, TdCountdown,
|
||||
TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
|
||||
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
|
||||
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside, ThreeLineStrike,
|
||||
ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, 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,
|
||||
WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility,
|
||||
YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
|
||||
SpinningTop, StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput,
|
||||
StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi,
|
||||
Stochastic, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio,
|
||||
Takuri, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen,
|
||||
TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput,
|
||||
TdSequential, TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeInside,
|
||||
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, Tii,
|
||||
TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput,
|
||||
Tweezer, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UpsideGapThreeMethods,
|
||||
UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance, VerticalHorizontalFilter,
|
||||
Vidya, VoltyStop, VolumeOscillator, VolumePriceTrend, Vortex, VortexOutput, Vwap,
|
||||
VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, WeightedClose,
|
||||
WilliamsFractals, WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput,
|
||||
YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput,
|
||||
Zlema, FAMILIES, T3,
|
||||
};
|
||||
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
|
||||
// line so the indicator-count tooling (which scans the braced block above and
|
||||
|
||||
Reference in New Issue
Block a user