feat: TA-Lib candlestick patterns — doji-star/gap/high-wave/hikkake (part 4 of 9) (#135)
* feat: add doji-star, gap, high-wave and hikkake candlestick patterns Five patterns, all `Input = Candle`, `Output = f64`: - Evening Doji Star (CDLEVENINGDOJISTAR) — bearish top reversal: long white bar, a doji gapping up, then a black bar closing deep into the first body; -1 (penetration configurable, default 0.3). - Morning Doji Star (CDLMORNINGDOJISTAR) — bullish bottom reversal mirror; +1. - Gap Side-by-Side White (CDLGAPSIDESIDEWHITE) — two similar white candles opening side by side after a gap, a continuation; gap up +1, gap down -1. - High-Wave (CDLHIGHWAVE) — a small body with very long shadows on both sides, an extreme indecision flag; +1 on detection. - Hikkake (CDLHIKKAKE) — an inside bar followed by a failed breakout (a trap); bullish +1, bearish -1. Counter 259 -> 264 (mod-count == lib counted block; FAMILIES total 254 -> 259). * chore: sync indicator count to 264 --------- Co-authored-by: wickra-bot <wickra-bot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
//! Evening Doji Star candlestick pattern.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Evening Doji Star — a 3-bar bearish top reversal. A long white bar extends the
|
||||
/// advance, a doji gaps up above it (the star of indecision), then a black bar
|
||||
/// gaps back down and closes deep into the first body, confirming the turn.
|
||||
///
|
||||
/// ```text
|
||||
/// long body = |close − open| >= 0.5 * (high − low)
|
||||
/// doji = |close − open| <= 0.1 * (high − low)
|
||||
/// bar1 white & long
|
||||
/// bar2 doji, body gaps UP above bar1 body (min(o2,c2) > close1)
|
||||
/// bar3 black, body gaps DOWN below the doji (max(o3,c3) < min(o2,c2))
|
||||
/// bar3 closes deep into bar1 body (close3 < close1 − penetration·body1)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Evening Doji
|
||||
/// Star is a single-direction (bearish-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. `penetration` is how far into the first body the third bar must close;
|
||||
/// it defaults to `0.3` (TA-Lib's `CDLEVENINGDOJISTAR` default) and must lie in
|
||||
/// `[0, 1)`. Body and doji 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, EveningDojiStar, Indicator};
|
||||
///
|
||||
/// let mut indicator = EveningDojiStar::new();
|
||||
/// indicator.update(Candle::new(10.0, 15.1, 9.9, 15.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(17.0, 17.1, 16.9, 17.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(16.0, 16.1, 11.9, 12.0, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EveningDojiStar {
|
||||
penetration: f64,
|
||||
prev: Option<Candle>,
|
||||
prev_prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Default for EveningDojiStar {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl EveningDojiStar {
|
||||
/// Construct an Evening Doji Star detector with the default 0.3 penetration.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
penetration: 0.3,
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an Evening Doji Star detector with a custom penetration fraction.
|
||||
///
|
||||
/// `penetration` must lie in `[0, 1)`.
|
||||
pub fn with_penetration(penetration: f64) -> Result<Self> {
|
||||
if !(0.0..1.0).contains(&penetration) {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "evening doji star penetration must lie in [0, 1)",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
penetration,
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured penetration fraction.
|
||||
pub fn penetration(&self) -> f64 {
|
||||
self.penetration
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for EveningDojiStar {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.prev_prev;
|
||||
let bar2 = self.prev;
|
||||
self.prev_prev = self.prev;
|
||||
self.prev = 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;
|
||||
if range1 <= 0.0 || range2 <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body1 = bar1.close - bar1.open;
|
||||
if body1 < 0.5 * range1 {
|
||||
return Some(0.0); // bar1 must be a long white body
|
||||
}
|
||||
if (bar2.close - bar2.open).abs() > 0.1 * range2 {
|
||||
return Some(0.0); // bar2 must be a doji
|
||||
}
|
||||
let star_bottom = bar2.open.min(bar2.close);
|
||||
let bar3_top = candle.open.max(candle.close);
|
||||
if star_bottom > bar1.close
|
||||
&& candle.close < candle.open
|
||||
&& bar3_top < star_bottom
|
||||
&& candle.close < bar1.close - self.penetration * body1
|
||||
{
|
||||
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 {
|
||||
"EveningDojiStar"
|
||||
}
|
||||
}
|
||||
|
||||
#[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_penetration() {
|
||||
assert!(EveningDojiStar::with_penetration(-0.01).is_err());
|
||||
assert!(EveningDojiStar::with_penetration(1.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_penetration() {
|
||||
let t = EveningDojiStar::with_penetration(0.5).unwrap();
|
||||
assert!((t.penetration() - 0.5).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = EveningDojiStar::default();
|
||||
assert_eq!(t.name(), "EveningDojiStar");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
assert!((t.penetration() - 0.3).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evening_doji_star_is_minus_one() {
|
||||
let mut t = EveningDojiStar::new();
|
||||
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(17.0, 17.1, 16.9, 17.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_not_doji_yields_zero() {
|
||||
let mut t = EveningDojiStar::new();
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
// Wide-bodied star, not a doji.
|
||||
t.update(c(16.0, 18.1, 15.9, 18.0, 1));
|
||||
assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shallow_close_yields_zero() {
|
||||
let mut t = EveningDojiStar::new();
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
t.update(c(17.0, 17.1, 16.9, 17.0, 1));
|
||||
// bar3 black but closes at 14.0 -> only 1.0 into the 5.0 body (< 0.3·5).
|
||||
assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = EveningDojiStar::new();
|
||||
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(17.0, 17.1, 16.9, 17.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 + 5.2, base - 0.1, base + 5.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = EveningDojiStar::new();
|
||||
let mut b = EveningDojiStar::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = EveningDojiStar::new();
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
t.update(c(17.0, 17.1, 16.9, 17.0, 1));
|
||||
t.update(c(16.0, 16.1, 11.9, 12.0, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
//! Gap Side-by-Side White Lines candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Gap Side-by-Side White Lines — a 3-bar continuation. After a gap away from the
|
||||
/// first bar, two white candles of similar size open at roughly the same level
|
||||
/// (side by side) and hold the gap open, signalling the trend resumes in the gap
|
||||
/// direction.
|
||||
///
|
||||
/// ```text
|
||||
/// bar2, bar3 both white
|
||||
/// bar2 body gaps away from bar1 body (up or down)
|
||||
/// bar3 opens beside bar2 (|open3 − open2| <= 0.1 · range2)
|
||||
/// bar3 body is similar in size to bar2 (neither more than twice the other)
|
||||
/// gap up -> +1.0 (bullish continuation)
|
||||
/// gap down -> −1.0 (bearish continuation — "downside" gap side-by-side white)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (gap up) or `−1.0` (gap down) when the pattern completes and
|
||||
/// `0.0` otherwise. The first two bars always return `0.0` because the three-bar
|
||||
/// window is not yet filled. Open-equality and body-similarity 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` bullish, `−1.0` bearish, `0.0` no pattern — so it
|
||||
/// drops straight into a machine-learning feature matrix where the two gap
|
||||
/// directions occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, GapSideBySideWhite, Indicator};
|
||||
///
|
||||
/// let mut indicator = GapSideBySideWhite::new();
|
||||
/// indicator.update(Candle::new(10.0, 11.1, 9.9, 11.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(13.0, 14.1, 12.9, 14.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(13.0, 14.1, 12.9, 14.0, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct GapSideBySideWhite {
|
||||
prev: Option<Candle>,
|
||||
prev_prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl GapSideBySideWhite {
|
||||
/// Construct a new Gap Side-by-Side White Lines detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for GapSideBySideWhite {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.prev_prev;
|
||||
let bar2 = self.prev;
|
||||
self.prev_prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range2 = bar2.high - bar2.low;
|
||||
if range2 <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Both of the side-by-side bars must be white.
|
||||
if bar2.close <= bar2.open || candle.close <= candle.open {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Side by side: opens level and bodies of comparable size.
|
||||
if (candle.open - bar2.open).abs() > 0.1 * range2 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body2 = bar2.close - bar2.open;
|
||||
let body3 = candle.close - candle.open;
|
||||
if body2 > 2.0 * body3 || body3 > 2.0 * body2 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let bar1_top = bar1.open.max(bar1.close);
|
||||
let bar1_bottom = bar1.open.min(bar1.close);
|
||||
let bar2_bottom = bar2.open.min(bar2.close);
|
||||
let bar2_top = bar2.open.max(bar2.close);
|
||||
if bar2_bottom > bar1_top {
|
||||
return Some(1.0); // gap up -> bullish continuation
|
||||
}
|
||||
if bar2_top < bar1_bottom {
|
||||
return Some(-1.0); // gap down -> bearish continuation
|
||||
}
|
||||
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 {
|
||||
"GapSideBySideWhite"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = GapSideBySideWhite::new();
|
||||
assert_eq!(t.name(), "GapSideBySideWhite");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gap_up_is_plus_one() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 14.1, 12.9, 14.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 14.1, 12.9, 14.0, 2)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gap_down_is_minus_one() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
assert_eq!(t.update(c(14.0, 14.1, 12.9, 13.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_bar_black_yields_zero() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
t.update(c(10.0, 11.1, 9.9, 11.0, 0));
|
||||
// bar3 is black -> not two white lines.
|
||||
t.update(c(13.0, 14.1, 12.9, 14.0, 1));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 12.9, 13.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_side_by_side_yields_zero() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
t.update(c(10.0, 11.1, 9.9, 11.0, 0));
|
||||
t.update(c(13.0, 14.1, 12.9, 14.0, 1));
|
||||
// bar3 opens far from bar2's open -> not side by side.
|
||||
assert_eq!(t.update(c(16.0, 17.1, 15.9, 17.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_gap_yields_zero() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
t.update(c(10.0, 13.1, 9.9, 13.0, 0));
|
||||
// bar2 overlaps bar1 (no gap).
|
||||
t.update(c(12.0, 13.1, 11.9, 13.0, 1));
|
||||
assert_eq!(t.update(c(12.0, 13.1, 11.9, 13.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 14.1, 12.9, 14.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 * 3.0;
|
||||
c(base, base + 1.1, base - 0.1, base + 1.0, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = GapSideBySideWhite::new();
|
||||
let mut b = GapSideBySideWhite::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = GapSideBySideWhite::new();
|
||||
t.update(c(10.0, 11.1, 9.9, 11.0, 0));
|
||||
t.update(c(13.0, 14.1, 12.9, 14.0, 1));
|
||||
t.update(c(13.0, 14.1, 12.9, 14.0, 2));
|
||||
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,160 @@
|
||||
//! High-Wave candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// High-Wave — a single-bar extreme-indecision signal. A small body with very
|
||||
/// long shadows on *both* sides: price swung far up and far down yet finished
|
||||
/// near the open, a sign that trend conviction has evaporated.
|
||||
///
|
||||
/// ```text
|
||||
/// range = high − low
|
||||
/// long upper = high − max(open, close) >= 0.4 * range
|
||||
/// long lower = min(open, close) − low >= 0.4 * range
|
||||
/// ```
|
||||
///
|
||||
/// The two long-shadow conditions force the body below `0.2 * range`, so no
|
||||
/// separate body test is needed. Output is `+1.0` when the high-wave prints and
|
||||
/// `0.0` otherwise — a non-directional indecision flag, it never emits `−1.0`.
|
||||
/// Shadow 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` detected, `0.0` no pattern — so it drops straight into
|
||||
/// a machine-learning feature matrix as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, HighWave, Indicator};
|
||||
///
|
||||
/// let mut indicator = HighWave::new();
|
||||
/// // Small body, long shadows both sides.
|
||||
/// let candle = Candle::new(10.0, 12.0, 8.0, 10.3, 1.0, 0).unwrap();
|
||||
/// assert_eq!(indicator.update(candle), Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HighWave {
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl HighWave {
|
||||
/// Construct a new High-Wave detector.
|
||||
pub const fn new() -> Self {
|
||||
Self { has_emitted: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for HighWave {
|
||||
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);
|
||||
}
|
||||
let upper = candle.high - candle.open.max(candle.close);
|
||||
let lower = candle.open.min(candle.close) - candle.low;
|
||||
if upper >= 0.4 * range && lower >= 0.4 * 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 {
|
||||
"HighWave"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = HighWave::new();
|
||||
assert_eq!(t.name(), "HighWave");
|
||||
assert_eq!(t.warmup_period(), 1);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_wave_is_plus_one() {
|
||||
let mut t = HighWave::new();
|
||||
assert_eq!(t.update(c(10.0, 12.0, 8.0, 10.3, 0)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_upper_shadow_yields_zero() {
|
||||
let mut t = HighWave::new();
|
||||
// Long lower shadow but short upper -> not a high-wave.
|
||||
assert_eq!(t.update(c(11.5, 12.0, 8.0, 11.7, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_lower_shadow_yields_zero() {
|
||||
let mut t = HighWave::new();
|
||||
// Long upper shadow but short lower -> not a high-wave.
|
||||
assert_eq!(t.update(c(8.3, 12.0, 8.0, 8.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_body_yields_zero() {
|
||||
let mut t = HighWave::new();
|
||||
// A large body cannot leave both shadows long.
|
||||
assert_eq!(t.update(c(8.5, 12.0, 8.0, 11.5, 0)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_range_yields_zero() {
|
||||
let mut t = HighWave::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.2, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = HighWave::new();
|
||||
let mut b = HighWave::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = HighWave::new();
|
||||
t.update(c(10.0, 12.0, 8.0, 10.3, 0));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
//! Hikkake candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Hikkake — a 3-bar trap. An inside bar (bar2 fully contained by bar1) sets up a
|
||||
/// breakout that immediately fails on bar3, trapping breakout traders and pointing
|
||||
/// the opposite way.
|
||||
///
|
||||
/// ```text
|
||||
/// inside bar : bar2.high < bar1.high && bar2.low > bar1.low
|
||||
/// bullish (+1.0): bar3 makes a LOWER high AND LOWER low than bar2
|
||||
/// (a false downside break -> expect a move up)
|
||||
/// bearish (−1.0): bar3 makes a HIGHER high AND HIGHER low than bar2
|
||||
/// (a false upside break -> expect a move down)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (bullish setup), `−1.0` (bearish setup), or `0.0` otherwise.
|
||||
/// The detector fires when the three-bar setup completes on bar3; it does not
|
||||
/// separately flag the optional later confirmation bar. The first two bars always
|
||||
/// return `0.0` because the 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 setups occupy a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Hikkake, Indicator};
|
||||
///
|
||||
/// let mut indicator = Hikkake::new();
|
||||
/// indicator.update(Candle::new(10.0, 15.0, 5.0, 12.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(11.0, 13.0, 8.0, 12.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(9.0, 12.0, 6.0, 7.0, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Hikkake {
|
||||
prev: Option<Candle>,
|
||||
prev_prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Hikkake {
|
||||
/// Construct a new Hikkake detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for Hikkake {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.prev_prev;
|
||||
let bar2 = self.prev;
|
||||
self.prev_prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let (Some(bar1), Some(bar2)) = (bar1, bar2) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
// bar2 must be an inside bar of bar1.
|
||||
if !(bar2.high < bar1.high && bar2.low > bar1.low) {
|
||||
return Some(0.0);
|
||||
}
|
||||
// Bullish: bar3 breaks below the inside bar (lower high and lower low).
|
||||
if candle.high < bar2.high && candle.low < bar2.low {
|
||||
return Some(1.0);
|
||||
}
|
||||
// Bearish: bar3 breaks above the inside bar (higher high and higher low).
|
||||
if 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 {
|
||||
"Hikkake"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = Hikkake::new();
|
||||
assert_eq!(t.name(), "Hikkake");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bullish_hikkake_is_plus_one() {
|
||||
let mut t = Hikkake::new();
|
||||
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(9.0, 12.0, 6.0, 7.0, 2)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bearish_hikkake_is_minus_one() {
|
||||
let mut t = Hikkake::new();
|
||||
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.0, 14.0, 9.0, 13.0, 2)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_inside_bar_yields_zero() {
|
||||
let mut t = Hikkake::new();
|
||||
t.update(c(10.0, 15.0, 5.0, 12.0, 0));
|
||||
// bar2 is not contained by bar1 (higher high).
|
||||
t.update(c(11.0, 16.0, 8.0, 12.0, 1));
|
||||
assert_eq!(t.update(c(9.0, 12.0, 6.0, 7.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outside_bar3_yields_zero() {
|
||||
let mut t = Hikkake::new();
|
||||
t.update(c(10.0, 15.0, 5.0, 12.0, 0));
|
||||
t.update(c(11.0, 13.0, 8.0, 12.0, 1));
|
||||
// bar3 engulfs bar2 (higher high and lower low) -> neither direction.
|
||||
assert_eq!(t.update(c(11.0, 14.0, 7.0, 9.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = Hikkake::new();
|
||||
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 13.0, 8.0, 12.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;
|
||||
match i % 3 {
|
||||
0 => c(base, base + 6.0, base - 6.0, base, i),
|
||||
1 => c(base, base + 2.0, base - 2.0, base, i),
|
||||
_ => c(base, base + 1.0, base - 5.0, base - 4.0, i),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut a = Hikkake::new();
|
||||
let mut b = Hikkake::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = Hikkake::new();
|
||||
t.update(c(10.0, 15.0, 5.0, 12.0, 0));
|
||||
t.update(c(11.0, 13.0, 8.0, 12.0, 1));
|
||||
t.update(c(9.0, 12.0, 6.0, 7.0, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@ mod elder_impulse;
|
||||
mod ema;
|
||||
mod empirical_mode_decomposition;
|
||||
mod engulfing;
|
||||
mod evening_doji_star;
|
||||
mod evwma;
|
||||
mod fama;
|
||||
mod fibonacci_pivots;
|
||||
@@ -90,12 +91,15 @@ mod funding_rate;
|
||||
mod funding_rate_mean;
|
||||
mod funding_rate_zscore;
|
||||
mod gain_loss_ratio;
|
||||
mod gap_side_by_side_white;
|
||||
mod garman_klass;
|
||||
mod gravestone_doji;
|
||||
mod hammer;
|
||||
mod hanging_man;
|
||||
mod harami;
|
||||
mod heikin_ashi;
|
||||
mod high_wave;
|
||||
mod hikkake;
|
||||
mod hilbert_dominant_cycle;
|
||||
mod hilo_activator;
|
||||
mod historical_volatility;
|
||||
@@ -140,6 +144,7 @@ mod median_price;
|
||||
mod mfi;
|
||||
mod microprice;
|
||||
mod mom;
|
||||
mod morning_doji_star;
|
||||
mod morning_evening_star;
|
||||
mod natr;
|
||||
mod nvi;
|
||||
@@ -336,6 +341,7 @@ pub use elder_impulse::ElderImpulse;
|
||||
pub use ema::Ema;
|
||||
pub use empirical_mode_decomposition::EmpiricalModeDecomposition;
|
||||
pub use engulfing::Engulfing;
|
||||
pub use evening_doji_star::EveningDojiStar;
|
||||
pub use evwma::Evwma;
|
||||
pub use fama::Fama;
|
||||
pub use fibonacci_pivots::{FibonacciPivots, FibonacciPivotsOutput};
|
||||
@@ -349,12 +355,15 @@ pub use funding_rate::FundingRate;
|
||||
pub use funding_rate_mean::FundingRateMean;
|
||||
pub use funding_rate_zscore::FundingRateZScore;
|
||||
pub use gain_loss_ratio::GainLossRatio;
|
||||
pub use gap_side_by_side_white::GapSideBySideWhite;
|
||||
pub use garman_klass::GarmanKlassVolatility;
|
||||
pub use gravestone_doji::GravestoneDoji;
|
||||
pub use hammer::Hammer;
|
||||
pub use hanging_man::HangingMan;
|
||||
pub use harami::Harami;
|
||||
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
|
||||
pub use high_wave::HighWave;
|
||||
pub use hikkake::Hikkake;
|
||||
pub use hilbert_dominant_cycle::HilbertDominantCycle;
|
||||
pub use hilo_activator::HiLoActivator;
|
||||
pub use historical_volatility::HistoricalVolatility;
|
||||
@@ -399,6 +408,7 @@ pub use median_price::MedianPrice;
|
||||
pub use mfi::Mfi;
|
||||
pub use microprice::Microprice;
|
||||
pub use mom::Mom;
|
||||
pub use morning_doji_star::MorningDojiStar;
|
||||
pub use morning_evening_star::MorningEveningStar;
|
||||
pub use natr::Natr;
|
||||
pub use nvi::Nvi;
|
||||
@@ -800,6 +810,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"GravestoneDoji",
|
||||
"LongLeggedDoji",
|
||||
"RickshawMan",
|
||||
"EveningDojiStar",
|
||||
"MorningDojiStar",
|
||||
"GapSideBySideWhite",
|
||||
"HighWave",
|
||||
"Hikkake",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -891,6 +906,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, 254, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 259, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
//! Morning Doji Star candlestick pattern.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Morning Doji Star — a 3-bar bullish bottom reversal. A long black bar extends
|
||||
/// the decline, a doji gaps down below it (the star of indecision), then a white
|
||||
/// bar gaps back up and closes deep into the first body, confirming the turn.
|
||||
///
|
||||
/// ```text
|
||||
/// long body = |close − open| >= 0.5 * (high − low)
|
||||
/// doji = |close − open| <= 0.1 * (high − low)
|
||||
/// bar1 black & long
|
||||
/// bar2 doji, body gaps DOWN below bar1 body (max(o2,c2) < close1)
|
||||
/// bar3 white, body gaps UP above the doji (min(o3,c3) > max(o2,c2))
|
||||
/// bar3 closes deep into bar1 body (close3 > close1 + penetration·body1)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Morning Doji
|
||||
/// Star 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. `penetration` is how far into the first body the third bar must close;
|
||||
/// it defaults to `0.3` (TA-Lib's `CDLMORNINGDOJISTAR` default) and must lie in
|
||||
/// `[0, 1)`. Body and doji 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` 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, MorningDojiStar};
|
||||
///
|
||||
/// let mut indicator = MorningDojiStar::new();
|
||||
/// indicator.update(Candle::new(15.0, 15.1, 9.9, 10.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(8.0, 8.1, 7.9, 8.0, 1.0, 1).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(9.0, 13.1, 8.9, 13.0, 1.0, 2).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MorningDojiStar {
|
||||
penetration: f64,
|
||||
prev: Option<Candle>,
|
||||
prev_prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl Default for MorningDojiStar {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MorningDojiStar {
|
||||
/// Construct a Morning Doji Star detector with the default 0.3 penetration.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
penetration: 0.3,
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a Morning Doji Star detector with a custom penetration fraction.
|
||||
///
|
||||
/// `penetration` must lie in `[0, 1)`.
|
||||
pub fn with_penetration(penetration: f64) -> Result<Self> {
|
||||
if !(0.0..1.0).contains(&penetration) {
|
||||
return Err(Error::InvalidPeriod {
|
||||
message: "morning doji star penetration must lie in [0, 1)",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
penetration,
|
||||
prev: None,
|
||||
prev_prev: None,
|
||||
has_emitted: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured penetration fraction.
|
||||
pub fn penetration(&self) -> f64 {
|
||||
self.penetration
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for MorningDojiStar {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.prev_prev;
|
||||
let bar2 = self.prev;
|
||||
self.prev_prev = self.prev;
|
||||
self.prev = 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;
|
||||
if range1 <= 0.0 || range2 <= 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let body1 = bar1.open - bar1.close;
|
||||
if body1 < 0.5 * range1 {
|
||||
return Some(0.0); // bar1 must be a long black body
|
||||
}
|
||||
if (bar2.close - bar2.open).abs() > 0.1 * range2 {
|
||||
return Some(0.0); // bar2 must be a doji
|
||||
}
|
||||
let star_top = bar2.open.max(bar2.close);
|
||||
let bar3_bottom = candle.open.min(candle.close);
|
||||
if star_top < bar1.close
|
||||
&& candle.close > candle.open
|
||||
&& bar3_bottom > star_top
|
||||
&& candle.close > bar1.close + self.penetration * body1
|
||||
{
|
||||
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 {
|
||||
"MorningDojiStar"
|
||||
}
|
||||
}
|
||||
|
||||
#[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_penetration() {
|
||||
assert!(MorningDojiStar::with_penetration(-0.01).is_err());
|
||||
assert!(MorningDojiStar::with_penetration(1.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_penetration() {
|
||||
let t = MorningDojiStar::with_penetration(0.5).unwrap();
|
||||
assert!((t.penetration() - 0.5).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = MorningDojiStar::default();
|
||||
assert_eq!(t.name(), "MorningDojiStar");
|
||||
assert_eq!(t.warmup_period(), 3);
|
||||
assert!(!t.is_ready());
|
||||
assert!((t.penetration() - 0.3).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn morning_doji_star_is_plus_one() {
|
||||
let mut t = MorningDojiStar::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(8.0, 8.1, 7.9, 8.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(9.0, 13.1, 8.9, 13.0, 2)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_not_doji_yields_zero() {
|
||||
let mut t = MorningDojiStar::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
// Wide-bodied star, not a doji.
|
||||
t.update(c(8.0, 10.1, 7.9, 10.0, 1));
|
||||
assert_eq!(t.update(c(9.0, 13.1, 8.9, 13.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shallow_close_yields_zero() {
|
||||
let mut t = MorningDojiStar::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(8.0, 8.1, 7.9, 8.0, 1));
|
||||
// bar3 white but closes at 11.0 -> only 1.0 into the 5.0 body (< 0.3·5).
|
||||
assert_eq!(t.update(c(9.0, 11.1, 8.9, 11.0, 2)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_two_bars_return_zero() {
|
||||
let mut t = MorningDojiStar::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(8.0, 8.1, 7.9, 8.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 + 5.0, base + 5.1, base - 0.1, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = MorningDojiStar::new();
|
||||
let mut b = MorningDojiStar::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = MorningDojiStar::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(8.0, 8.1, 7.9, 8.0, 1));
|
||||
t.update(c(9.0, 13.1, 8.9, 13.0, 2));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -67,42 +67,42 @@ pub use indicators::{
|
||||
DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, DojiStar, Donchian, DonchianOutput,
|
||||
DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, Dpo, DragonflyDoji,
|
||||
DrawdownDuration, EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
||||
EmpiricalModeDecomposition, Engulfing, Evwma, Fama, FibonacciPivots, FibonacciPivotsOutput,
|
||||
FisherTransform, Footprint, FootprintOutput, ForceIndex, FractalChaosBands,
|
||||
FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean, FundingRateZScore,
|
||||
GainLossRatio, GarmanKlassVolatility, GravestoneDoji, Hammer, HangingMan, Harami, HeikinAshi,
|
||||
HeikinAshiOutput, HiLoActivator, HilbertDominantCycle, HistoricalVolatility, Hma, HurstChannel,
|
||||
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, LongLeggedDoji,
|
||||
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, PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi,
|
||||
QuotedSpread, RSquared, RealizedSpread, RecoveryFactor, RelativeStrengthAB,
|
||||
RelativeStrengthOutput, RenkoTrailingStop, RickshawMan, Roc, RogersSatchellVolatility,
|
||||
RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SharpeRatio, ShootingStar,
|
||||
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, 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,
|
||||
EmpiricalModeDecomposition, Engulfing, EveningDojiStar, Evwma, 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,
|
||||
HilbertDominantCycle, HistoricalVolatility, Hma, HurstChannel, 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, LongLeggedDoji, LongShortRatio, MaEnvelope,
|
||||
MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
|
||||
Marubozu, MassIndex, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi,
|
||||
Microprice, Mom, MorningDojiStar, MorningEveningStar, Natr, Nvi, OIPriceDivergence, OIWeighted,
|
||||
Obv, OmegaRatio, OpenInterestDelta, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull,
|
||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex, PairSpreadZScore, PairwiseBeta,
|
||||
ParkinsonVolatility, PearsonCorrelation, PercentB, PercentageTrailingStop, Pgo,
|
||||
PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi, QuotedSpread, RSquared, RealizedSpread,
|
||||
RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput, RenkoTrailingStop, RickshawMan,
|
||||
Roc, RogersSatchellVolatility, RollingVwap, RoofingFilter, Rsi, Rvi, RviVolatility, Rwi,
|
||||
RwiOutput, SharpeRatio, ShootingStar, 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, 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,
|
||||
};
|
||||
// `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