feat: TA-Lib candlestick patterns — matching-low/lines/three-methods (part 7 of 9) (#139)
Adds five TA-Lib candlestick patterns, all `Input = Candle`, `Output = f64` (`+1.0` bullish / `-1.0` bearish / `0.0` no pattern), wired across core, Python/Node/WASM bindings, fuzz, and tests. - **Matching Low** (`CDLMATCHINGLOW`) — 2-bar bullish reversal: two black candles in a decline share the same close, signalling selling pressure is exhausting; bullish +1. - **Long Line** (`CDLLONGLINE`) — a candle whose range beats a rolling average of recent ranges with a body-dominated range; bullish +1 (white) / bearish -1 (black). - **Short Line** (`CDLSHORTLINE`) — a compact candle whose range falls below the rolling average with a body-dominated range; bullish +1 (white) / bearish -1 (black). - **Rising Three Methods** (`CDLRISEFALL3METHODS`) — 5-bar bullish continuation: a long white candle, three small bars holding within its range, then a white breakout to new highs; bullish +1. - **Falling Three Methods** (`CDLRISEFALL3METHODS`) — the bearish mirror: a long black candle, three small bars within its range, then a black breakdown to new lows; bearish -1. Counter 274 → 279 (mod-count == lib counted block; FAMILIES total 269 → 274). Stacked on #138 (part 6 of 9); base retargets to `main` as the chain merges.
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
//! Falling Three Methods candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Falling Three Methods — a 5-bar bearish continuation. A long black candle is
|
||||
/// followed by three small bars that drift up but stay inside its range (a brief
|
||||
/// rest), then a second long black candle closes below the first, resuming the
|
||||
/// decline.
|
||||
///
|
||||
/// ```text
|
||||
/// long body = |close − open| >= 0.5 * (high − low)
|
||||
/// bar1 black & long
|
||||
/// bar2, bar3, bar4 small bodies, each contained within bar1's high/low range
|
||||
/// bar5 black, closing below bar1's close
|
||||
/// ```
|
||||
///
|
||||
/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Falling Three
|
||||
/// Methods is a single-direction (bearish-only) continuation, so it never emits
|
||||
/// `+1.0`. The first four bars always return `0.0` because the five-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, FallingThreeMethods, Indicator};
|
||||
///
|
||||
/// let mut indicator = FallingThreeMethods::new();
|
||||
/// indicator.update(Candle::new(15.0, 15.1, 9.9, 10.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(11.0, 12.1, 10.9, 12.0, 1.0, 1).unwrap());
|
||||
/// indicator.update(Candle::new(11.5, 12.6, 11.4, 12.5, 1.0, 2).unwrap());
|
||||
/// indicator.update(Candle::new(12.0, 13.1, 11.9, 13.0, 1.0, 3).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(12.5, 12.6, 8.9, 9.0, 1.0, 4).unwrap());
|
||||
/// assert_eq!(out, Some(-1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FallingThreeMethods {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
c3: Option<Candle>,
|
||||
c4: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl FallingThreeMethods {
|
||||
/// Construct a new Falling Three Methods detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
c1: None,
|
||||
c2: None,
|
||||
c3: None,
|
||||
c4: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for FallingThreeMethods {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.c1;
|
||||
let bar2 = self.c2;
|
||||
let bar3 = self.c3;
|
||||
let bar4 = self.c4;
|
||||
self.c1 = self.c2;
|
||||
self.c2 = self.c3;
|
||||
self.c3 = self.c4;
|
||||
self.c4 = Some(candle);
|
||||
let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range1 = bar1.high - bar1.low;
|
||||
if range1 <= 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
|
||||
}
|
||||
// The three middle bars stay within bar1's range with smaller bodies.
|
||||
for mid in [bar2, bar3, bar4] {
|
||||
if (mid.close - mid.open).abs() >= body1 || mid.high > bar1.high || mid.low < bar1.low {
|
||||
return Some(0.0);
|
||||
}
|
||||
}
|
||||
// bar5 is a black candle closing below bar1's close.
|
||||
if candle.close < candle.open && candle.close < bar1.close {
|
||||
return Some(-1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.c3 = None;
|
||||
self.c4 = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"FallingThreeMethods"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = FallingThreeMethods::new();
|
||||
assert_eq!(t.name(), "FallingThreeMethods");
|
||||
assert_eq!(t.warmup_period(), 5);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falling_three_methods_is_minus_one() {
|
||||
let mut t = FallingThreeMethods::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 12.1, 10.9, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.5, 12.6, 11.4, 12.5, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.0, 13.1, 11.9, 13.0, 3)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 4)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_bar_breaks_range_yields_zero() {
|
||||
let mut t = FallingThreeMethods::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(11.0, 12.1, 10.9, 12.0, 1));
|
||||
// bar3 pokes below bar1's low.
|
||||
t.update(c(11.5, 12.6, 9.0, 12.5, 2));
|
||||
t.update(c(12.0, 13.1, 11.9, 13.0, 3));
|
||||
assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar5_not_new_low_yields_zero() {
|
||||
let mut t = FallingThreeMethods::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(11.0, 12.1, 10.9, 12.0, 1));
|
||||
t.update(c(11.5, 12.6, 11.4, 12.5, 2));
|
||||
t.update(c(12.0, 13.1, 11.9, 13.0, 3));
|
||||
// bar5 black but closes above bar1's close.
|
||||
assert_eq!(t.update(c(12.5, 12.6, 10.4, 10.5, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_four_bars_return_zero() {
|
||||
let mut t = FallingThreeMethods::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.0, 12.1, 10.9, 12.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(11.5, 12.6, 11.4, 12.5, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.0, 13.1, 11.9, 13.0, 3)), 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 + 5.0, base + 5.1, base - 0.1, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = FallingThreeMethods::new();
|
||||
let mut b = FallingThreeMethods::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = FallingThreeMethods::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(11.0, 12.1, 10.9, 12.0, 1));
|
||||
t.update(c(11.5, 12.6, 11.4, 12.5, 2));
|
||||
t.update(c(12.0, 13.1, 11.9, 13.0, 3));
|
||||
t.update(c(12.5, 12.6, 8.9, 9.0, 4));
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
//! Long Line candlestick pattern.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Long Line — a single candle whose range is *longer* than the recent average and
|
||||
/// whose body dominates that range (a solid directional bar). Because "long" only
|
||||
/// has meaning relative to recent activity, the detector compares each candle's
|
||||
/// range against a rolling average of the previous `period` ranges.
|
||||
///
|
||||
/// ```text
|
||||
/// avg = mean range of the previous `period` candles
|
||||
/// long line = range > avg AND |close − open| >= 0.5 * range
|
||||
/// white -> +1.0, black -> −1.0
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (long white line), `−1.0` (long black line), or `0.0`
|
||||
/// otherwise. The first `period` candles return `0.0` while the rolling average
|
||||
/// fills. `period` defaults to `5` and must be at least `1`. This rolling baseline
|
||||
/// is the one place the family departs from a purely intra-candle rule, since a
|
||||
/// short/long classification is inherently scale-relative. 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 as a single dimension.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_core::{Candle, Indicator, LongLine};
|
||||
///
|
||||
/// let mut indicator = LongLine::new();
|
||||
/// // Five quiet bars fill the rolling average.
|
||||
/// for ts in 0..5 {
|
||||
/// indicator.update(Candle::new(10.0, 10.5, 9.5, 10.2, 1.0, ts).unwrap());
|
||||
/// }
|
||||
/// // A wide solid white bar is a long white line.
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(10.0, 13.0, 9.9, 12.9, 1.0, 5).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LongLine {
|
||||
period: usize,
|
||||
ranges: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl Default for LongLine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl LongLine {
|
||||
/// Construct a Long Line detector with the default 5-candle rolling average.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
period: 5,
|
||||
ranges: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a Long Line detector with a custom averaging period.
|
||||
///
|
||||
/// `period` must be at least `1`.
|
||||
pub fn with_period(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
ranges: VecDeque::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured averaging period.
|
||||
pub fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for LongLine {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let range = candle.high - candle.low;
|
||||
let body = candle.close - candle.open;
|
||||
if self.ranges.len() < self.period {
|
||||
self.ranges.push_back(range);
|
||||
return Some(0.0);
|
||||
}
|
||||
let avg = self.ranges.iter().sum::<f64>() / self.period as f64;
|
||||
self.ranges.push_back(range);
|
||||
self.ranges.pop_front();
|
||||
if range > avg && body.abs() >= 0.5 * range {
|
||||
return Some(if body > 0.0 { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.ranges.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.ranges.len() >= self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"LongLine"
|
||||
}
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
|
||||
fn warm(t: &mut LongLine) {
|
||||
for ts in 0..5 {
|
||||
assert_eq!(t.update(c(10.0, 10.5, 9.5, 10.2, ts)), Some(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(LongLine::with_period(0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_period() {
|
||||
let t = LongLine::with_period(10).unwrap();
|
||||
assert_eq!(t.period(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = LongLine::new();
|
||||
assert_eq!(t.name(), "LongLine");
|
||||
assert_eq!(t.warmup_period(), 5);
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.period(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_white_line_is_plus_one() {
|
||||
let mut t = LongLine::new();
|
||||
warm(&mut t);
|
||||
assert!(t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, 5)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_black_line_is_minus_one() {
|
||||
let mut t = LongLine::new();
|
||||
warm(&mut t);
|
||||
assert_eq!(t.update(c(13.0, 13.1, 9.9, 10.0, 5)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_range_yields_zero() {
|
||||
let mut t = LongLine::new();
|
||||
warm(&mut t);
|
||||
// Range no bigger than the average -> not a long line.
|
||||
assert_eq!(t.update(c(10.0, 10.5, 9.5, 10.2, 5)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_range_small_body_yields_zero() {
|
||||
let mut t = LongLine::new();
|
||||
warm(&mut t);
|
||||
// Wide range but a tiny body -> a spinning top, not a long line.
|
||||
assert_eq!(t.update(c(10.5, 13.0, 9.9, 10.6, 5)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_returns_zero() {
|
||||
let mut t = LongLine::new();
|
||||
for ts in 0..5 {
|
||||
assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, ts)), Some(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
if i % 7 == 0 {
|
||||
c(base, base + 4.0, base - 0.1, base + 3.9, i)
|
||||
} else {
|
||||
c(base, base + 0.5, base - 0.5, base + 0.2, i)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut a = LongLine::new();
|
||||
let mut b = LongLine::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = LongLine::new();
|
||||
warm(&mut t);
|
||||
t.update(c(10.0, 13.0, 9.9, 12.9, 5));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 13.0, 9.9, 12.9, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
//! Matching Low candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Matching Low — a 2-bar bullish reversal. Two black candles in a decline close
|
||||
/// at the *same* level: the second sell-off cannot push price any lower, so the
|
||||
/// matching closes mark a support floor.
|
||||
///
|
||||
/// ```text
|
||||
/// bar1, bar2 both black
|
||||
/// equal closes = |close2 − close1| <= 0.05 · mean(range1, range2)
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Matching Low
|
||||
/// is a single-direction (bullish-only) reversal, so it never emits `−1.0`. The
|
||||
/// first bar always returns `0.0` because the two-bar window is not yet filled.
|
||||
/// The close-equality tolerance follows 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, MatchingLow};
|
||||
///
|
||||
/// let mut indicator = MatchingLow::new();
|
||||
/// indicator.update(Candle::new(15.0, 15.1, 9.9, 10.0, 1.0, 0).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(13.0, 13.1, 9.9, 10.0, 1.0, 1).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MatchingLow {
|
||||
prev: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl MatchingLow {
|
||||
/// Construct a new Matching Low detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
prev: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for MatchingLow {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let prev = self.prev;
|
||||
self.prev = Some(candle);
|
||||
let Some(bar1) = prev else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let mean_range = 0.5 * ((bar1.high - bar1.low) + (candle.high - candle.low));
|
||||
let tol = 0.05 * mean_range;
|
||||
if bar1.close < bar1.open
|
||||
&& candle.close < candle.open
|
||||
&& (candle.close - bar1.close).abs() <= tol
|
||||
{
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"MatchingLow"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = MatchingLow::new();
|
||||
assert_eq!(t.name(), "MatchingLow");
|
||||
assert_eq!(t.warmup_period(), 2);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matching_low_is_plus_one() {
|
||||
let mut t = MatchingLow::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 10.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 13.1, 9.9, 10.0, 1)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_close_yields_zero() {
|
||||
let mut t = MatchingLow::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
// Second close well away from the first.
|
||||
assert_eq!(t.update(c(13.0, 13.1, 11.4, 11.5, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_bar_white_yields_zero() {
|
||||
let mut t = MatchingLow::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
assert_eq!(t.update(c(9.0, 10.1, 8.9, 10.0, 1)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_bar_returns_zero() {
|
||||
let mut t = MatchingLow::new();
|
||||
assert_eq!(t.update(c(15.0, 15.1, 9.9, 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 + 2.0, base + 2.1, base - 0.1, base, i)
|
||||
})
|
||||
.collect();
|
||||
let mut a = MatchingLow::new();
|
||||
let mut b = MatchingLow::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = MatchingLow::new();
|
||||
t.update(c(15.0, 15.1, 9.9, 10.0, 0));
|
||||
t.update(c(13.0, 13.1, 9.9, 10.0, 1));
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,7 @@ mod empirical_mode_decomposition;
|
||||
mod engulfing;
|
||||
mod evening_doji_star;
|
||||
mod evwma;
|
||||
mod falling_three_methods;
|
||||
mod fama;
|
||||
mod fibonacci_pivots;
|
||||
mod fisher_transform;
|
||||
@@ -136,6 +137,7 @@ mod linreg_channel;
|
||||
mod linreg_slope;
|
||||
mod liquidation_features;
|
||||
mod long_legged_doji;
|
||||
mod long_line;
|
||||
mod long_short_ratio;
|
||||
mod ma_envelope;
|
||||
mod macd;
|
||||
@@ -144,6 +146,7 @@ mod market_facilitation_index;
|
||||
mod marubozu;
|
||||
mod mass_index;
|
||||
mod mat_hold;
|
||||
mod matching_low;
|
||||
mod max_drawdown;
|
||||
mod mcginley_dynamic;
|
||||
mod median_absolute_deviation;
|
||||
@@ -186,6 +189,7 @@ mod recovery_factor;
|
||||
mod relative_strength_ab;
|
||||
mod renko_trailing_stop;
|
||||
mod rickshaw_man;
|
||||
mod rising_three_methods;
|
||||
mod roc;
|
||||
mod rogers_satchell;
|
||||
mod roofing_filter;
|
||||
@@ -196,6 +200,7 @@ mod rwi;
|
||||
mod separating_lines;
|
||||
mod sharpe_ratio;
|
||||
mod shooting_star;
|
||||
mod short_line;
|
||||
mod signed_volume;
|
||||
mod sine_wave;
|
||||
mod skewness;
|
||||
@@ -353,6 +358,7 @@ pub use empirical_mode_decomposition::EmpiricalModeDecomposition;
|
||||
pub use engulfing::Engulfing;
|
||||
pub use evening_doji_star::EveningDojiStar;
|
||||
pub use evwma::Evwma;
|
||||
pub use falling_three_methods::FallingThreeMethods;
|
||||
pub use fama::Fama;
|
||||
pub use fibonacci_pivots::{FibonacciPivots, FibonacciPivotsOutput};
|
||||
pub use fisher_transform::FisherTransform;
|
||||
@@ -410,6 +416,7 @@ pub use linreg_channel::{LinRegChannel, LinRegChannelOutput};
|
||||
pub use linreg_slope::LinRegSlope;
|
||||
pub use liquidation_features::{LiquidationFeatures, LiquidationFeaturesOutput};
|
||||
pub use long_legged_doji::LongLeggedDoji;
|
||||
pub use long_line::LongLine;
|
||||
pub use long_short_ratio::LongShortRatio;
|
||||
pub use ma_envelope::{MaEnvelope, MaEnvelopeOutput};
|
||||
pub use macd::{MacdIndicator, MacdOutput};
|
||||
@@ -418,6 +425,7 @@ pub use market_facilitation_index::MarketFacilitationIndex;
|
||||
pub use marubozu::Marubozu;
|
||||
pub use mass_index::MassIndex;
|
||||
pub use mat_hold::MatHold;
|
||||
pub use matching_low::MatchingLow;
|
||||
pub use max_drawdown::MaxDrawdown;
|
||||
pub use mcginley_dynamic::McGinleyDynamic;
|
||||
pub use median_absolute_deviation::MedianAbsoluteDeviation;
|
||||
@@ -460,6 +468,7 @@ pub use recovery_factor::RecoveryFactor;
|
||||
pub use relative_strength_ab::{RelativeStrengthAB, RelativeStrengthOutput};
|
||||
pub use renko_trailing_stop::RenkoTrailingStop;
|
||||
pub use rickshaw_man::RickshawMan;
|
||||
pub use rising_three_methods::RisingThreeMethods;
|
||||
pub use roc::Roc;
|
||||
pub use rogers_satchell::RogersSatchellVolatility;
|
||||
pub use roofing_filter::RoofingFilter;
|
||||
@@ -470,6 +479,7 @@ pub use rwi::{Rwi, RwiOutput};
|
||||
pub use separating_lines::SeparatingLines;
|
||||
pub use sharpe_ratio::SharpeRatio;
|
||||
pub use shooting_star::ShootingStar;
|
||||
pub use short_line::ShortLine;
|
||||
pub use signed_volume::SignedVolume;
|
||||
pub use sine_wave::SineWave;
|
||||
pub use skewness::Skewness;
|
||||
@@ -845,6 +855,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"KickingByLength",
|
||||
"LadderBottom",
|
||||
"MatHold",
|
||||
"MatchingLow",
|
||||
"LongLine",
|
||||
"ShortLine",
|
||||
"RisingThreeMethods",
|
||||
"FallingThreeMethods",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -936,6 +951,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, 269, "FAMILIES total drifted from indicator count");
|
||||
assert_eq!(total, 274, "FAMILIES total drifted from indicator count");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
//! Rising Three Methods candlestick pattern.
|
||||
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
|
||||
/// Rising Three Methods — a 5-bar bullish continuation. A long white candle is
|
||||
/// followed by three small bars that drift back but stay inside its range (a brief
|
||||
/// rest), then a second long white candle closes above the first, resuming the
|
||||
/// advance.
|
||||
///
|
||||
/// ```text
|
||||
/// long body = |close − open| >= 0.5 * (high − low)
|
||||
/// bar1 white & long
|
||||
/// bar2, bar3, bar4 small bodies, each contained within bar1's high/low range
|
||||
/// bar5 white, closing above bar1's close
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Rising Three
|
||||
/// Methods is a single-direction (bullish-only) continuation, so it never emits
|
||||
/// `−1.0`. The first four bars always return `0.0` because the five-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` 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, RisingThreeMethods};
|
||||
///
|
||||
/// let mut indicator = RisingThreeMethods::new();
|
||||
/// indicator.update(Candle::new(10.0, 15.1, 9.9, 15.0, 1.0, 0).unwrap());
|
||||
/// indicator.update(Candle::new(14.0, 14.1, 12.9, 13.0, 1.0, 1).unwrap());
|
||||
/// indicator.update(Candle::new(13.5, 13.6, 12.4, 12.5, 1.0, 2).unwrap());
|
||||
/// indicator.update(Candle::new(13.0, 13.1, 11.9, 12.0, 1.0, 3).unwrap());
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(12.5, 16.1, 12.4, 16.0, 1.0, 4).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RisingThreeMethods {
|
||||
c1: Option<Candle>,
|
||||
c2: Option<Candle>,
|
||||
c3: Option<Candle>,
|
||||
c4: Option<Candle>,
|
||||
has_emitted: bool,
|
||||
}
|
||||
|
||||
impl RisingThreeMethods {
|
||||
/// Construct a new Rising Three Methods detector.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
c1: None,
|
||||
c2: None,
|
||||
c3: None,
|
||||
c4: None,
|
||||
has_emitted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for RisingThreeMethods {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
self.has_emitted = true;
|
||||
let bar1 = self.c1;
|
||||
let bar2 = self.c2;
|
||||
let bar3 = self.c3;
|
||||
let bar4 = self.c4;
|
||||
self.c1 = self.c2;
|
||||
self.c2 = self.c3;
|
||||
self.c3 = self.c4;
|
||||
self.c4 = Some(candle);
|
||||
let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
|
||||
return Some(0.0);
|
||||
};
|
||||
let range1 = bar1.high - bar1.low;
|
||||
if range1 <= 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
|
||||
}
|
||||
// The three middle bars stay within bar1's range with smaller bodies.
|
||||
for mid in [bar2, bar3, bar4] {
|
||||
if (mid.close - mid.open).abs() >= body1 || mid.high > bar1.high || mid.low < bar1.low {
|
||||
return Some(0.0);
|
||||
}
|
||||
}
|
||||
// bar5 is a white candle closing above bar1's close.
|
||||
if candle.close > candle.open && candle.close > bar1.close {
|
||||
return Some(1.0);
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.c1 = None;
|
||||
self.c2 = None;
|
||||
self.c3 = None;
|
||||
self.c4 = None;
|
||||
self.has_emitted = false;
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.has_emitted
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"RisingThreeMethods"
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = RisingThreeMethods::new();
|
||||
assert_eq!(t.name(), "RisingThreeMethods");
|
||||
assert_eq!(t.warmup_period(), 5);
|
||||
assert!(!t.is_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rising_three_methods_is_plus_one() {
|
||||
let mut t = RisingThreeMethods::new();
|
||||
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 12.9, 13.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.5, 13.6, 12.4, 12.5, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 3)), Some(0.0));
|
||||
assert_eq!(t.update(c(12.5, 16.1, 12.4, 16.0, 4)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_bar_breaks_range_yields_zero() {
|
||||
let mut t = RisingThreeMethods::new();
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
t.update(c(14.0, 14.1, 12.9, 13.0, 1));
|
||||
// bar3 pokes above bar1's high.
|
||||
t.update(c(13.5, 16.0, 12.4, 12.5, 2));
|
||||
t.update(c(13.0, 13.1, 11.9, 12.0, 3));
|
||||
assert_eq!(t.update(c(12.5, 16.1, 12.4, 16.0, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar5_not_new_high_yields_zero() {
|
||||
let mut t = RisingThreeMethods::new();
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
t.update(c(14.0, 14.1, 12.9, 13.0, 1));
|
||||
t.update(c(13.5, 13.6, 12.4, 12.5, 2));
|
||||
t.update(c(13.0, 13.1, 11.9, 12.0, 3));
|
||||
// bar5 white but closes below bar1's close.
|
||||
assert_eq!(t.update(c(12.5, 14.6, 12.4, 14.5, 4)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_four_bars_return_zero() {
|
||||
let mut t = RisingThreeMethods::new();
|
||||
assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), Some(0.0));
|
||||
assert_eq!(t.update(c(14.0, 14.1, 12.9, 13.0, 1)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.5, 13.6, 12.4, 12.5, 2)), Some(0.0));
|
||||
assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 3)), 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 = RisingThreeMethods::new();
|
||||
let mut b = RisingThreeMethods::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = RisingThreeMethods::new();
|
||||
t.update(c(10.0, 15.1, 9.9, 15.0, 0));
|
||||
t.update(c(14.0, 14.1, 12.9, 13.0, 1));
|
||||
t.update(c(13.5, 13.6, 12.4, 12.5, 2));
|
||||
t.update(c(13.0, 13.1, 11.9, 12.0, 3));
|
||||
t.update(c(12.5, 16.1, 12.4, 16.0, 4));
|
||||
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,228 @@
|
||||
//! Short Line candlestick pattern.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::ohlcv::Candle;
|
||||
use crate::traits::Indicator;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Short Line — a single candle whose range is *shorter* than the recent average
|
||||
/// while its body still dominates that (small) range: a compact directional bar.
|
||||
/// As with [`LongLine`](crate::LongLine), "short" only has meaning relative to
|
||||
/// recent activity, so the detector compares each candle's range against a rolling
|
||||
/// average of the previous `period` ranges.
|
||||
///
|
||||
/// ```text
|
||||
/// avg = mean range of the previous `period` candles
|
||||
/// short line = range < avg AND |close − open| >= 0.5 * range
|
||||
/// white -> +1.0, black -> −1.0
|
||||
/// ```
|
||||
///
|
||||
/// Output is `+1.0` (short white line), `−1.0` (short black line), or `0.0`
|
||||
/// otherwise. The first `period` candles return `0.0` while the rolling average
|
||||
/// fills. `period` defaults to `5` and must be at least `1`. Pattern-shape check
|
||||
/// only — no trend filter is applied; combine with a trend indicator for
|
||||
/// actionable signals.
|
||||
///
|
||||
/// # Signed ±1 encoding
|
||||
///
|
||||
/// This detector emits the uniform candlestick sign convention shared across the
|
||||
/// pattern family — `+1.0` bullish, `−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, ShortLine};
|
||||
///
|
||||
/// let mut indicator = ShortLine::new();
|
||||
/// // Five wide bars fill the rolling average.
|
||||
/// for ts in 0..5 {
|
||||
/// indicator.update(Candle::new(10.0, 13.0, 9.5, 12.9, 1.0, ts).unwrap());
|
||||
/// }
|
||||
/// // A compact solid white bar is a short white line.
|
||||
/// let out = indicator
|
||||
/// .update(Candle::new(10.0, 11.0, 9.9, 10.9, 1.0, 5).unwrap());
|
||||
/// assert_eq!(out, Some(1.0));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShortLine {
|
||||
period: usize,
|
||||
ranges: VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl Default for ShortLine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ShortLine {
|
||||
/// Construct a Short Line detector with the default 5-candle rolling average.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
period: 5,
|
||||
ranges: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a Short Line detector with a custom averaging period.
|
||||
///
|
||||
/// `period` must be at least `1`.
|
||||
pub fn with_period(period: usize) -> Result<Self> {
|
||||
if period == 0 {
|
||||
return Err(Error::PeriodZero);
|
||||
}
|
||||
Ok(Self {
|
||||
period,
|
||||
ranges: VecDeque::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Configured averaging period.
|
||||
pub fn period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for ShortLine {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||||
let range = candle.high - candle.low;
|
||||
let body = candle.close - candle.open;
|
||||
if self.ranges.len() < self.period {
|
||||
self.ranges.push_back(range);
|
||||
return Some(0.0);
|
||||
}
|
||||
let avg = self.ranges.iter().sum::<f64>() / self.period as f64;
|
||||
self.ranges.push_back(range);
|
||||
self.ranges.pop_front();
|
||||
if range < avg && body.abs() >= 0.5 * range {
|
||||
return Some(if body > 0.0 { 1.0 } else { -1.0 });
|
||||
}
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.ranges.clear();
|
||||
}
|
||||
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.period
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.ranges.len() >= self.period
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ShortLine"
|
||||
}
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
|
||||
fn warm(t: &mut ShortLine) {
|
||||
for ts in 0..5 {
|
||||
assert_eq!(t.update(c(10.0, 13.0, 9.5, 12.9, ts)), Some(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_period() {
|
||||
assert!(ShortLine::with_period(0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_period() {
|
||||
let t = ShortLine::with_period(10).unwrap();
|
||||
assert_eq!(t.period(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let t = ShortLine::new();
|
||||
assert_eq!(t.name(), "ShortLine");
|
||||
assert_eq!(t.warmup_period(), 5);
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.period(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_white_line_is_plus_one() {
|
||||
let mut t = ShortLine::new();
|
||||
warm(&mut t);
|
||||
assert!(t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 11.0, 9.9, 10.9, 5)), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_black_line_is_minus_one() {
|
||||
let mut t = ShortLine::new();
|
||||
warm(&mut t);
|
||||
assert_eq!(t.update(c(10.9, 11.0, 9.9, 10.0, 5)), Some(-1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_range_yields_zero() {
|
||||
let mut t = ShortLine::new();
|
||||
warm(&mut t);
|
||||
// Range as wide as the average -> not a short line.
|
||||
assert_eq!(t.update(c(10.0, 13.0, 9.5, 12.9, 5)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_range_small_body_yields_zero() {
|
||||
let mut t = ShortLine::new();
|
||||
warm(&mut t);
|
||||
// Compact range but a tiny body -> not a solid short line.
|
||||
assert_eq!(t.update(c(10.4, 11.0, 9.9, 10.5, 5)), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warmup_returns_zero() {
|
||||
let mut t = ShortLine::new();
|
||||
for ts in 0..5 {
|
||||
assert_eq!(t.update(c(10.0, 11.0, 9.9, 10.9, ts)), Some(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_equals_streaming() {
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64;
|
||||
if i % 7 == 0 {
|
||||
c(base, base + 0.6, base - 0.1, base + 0.5, i)
|
||||
} else {
|
||||
c(base, base + 3.0, base - 1.0, base + 2.8, i)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut a = ShortLine::new();
|
||||
let mut b = ShortLine::new();
|
||||
assert_eq!(
|
||||
a.batch(&candles),
|
||||
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_state() {
|
||||
let mut t = ShortLine::new();
|
||||
warm(&mut t);
|
||||
t.update(c(10.0, 11.0, 9.9, 10.9, 5));
|
||||
assert!(t.is_ready());
|
||||
t.reset();
|
||||
assert!(!t.is_ready());
|
||||
assert_eq!(t.update(c(10.0, 11.0, 9.9, 10.9, 0)), Some(0.0));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user