feat(family-14): add 15 candlestick patterns (#53)

* feat(family-14): add 15 candlestick patterns

Introduces the Candlestick Patterns family (block A of the family-14 spec)
as scalar f64 indicators on Candle inputs. Each detector emits +1.0 for a
bullish reading, -1.0 for a bearish reading, and 0.0 when no pattern is
present. Doji is direction-less and emits +1.0 / 0.0 only.

New indicators (15):

- Doji
- Hammer
- InvertedHammer
- HangingMan
- ShootingStar
- Engulfing
- Harami
- MorningEveningStar (signed: +1.0 morning star, -1.0 evening star)
- ThreeSoldiersOrCrows (signed: +1.0 soldiers, -1.0 crows)
- PiercingDarkCloud (signed: +1.0 piercing, -1.0 dark cloud)
- Marubozu (signed: +1.0 bullish, -1.0 bearish, 5 percent shadow tolerance default)
- Tweezer (signed: +1.0 bottom, -1.0 top, 10 bps relative tolerance default)
- SpinningTop (direction-signed indecision)
- ThreeInside (confirmed Harami)
- ThreeOutside (confirmed Engulfing)

MVP scope notes:

- Pattern-shape check only, no trend filter applied. Caller combines with a
  trend indicator for actionable signals. Documented in every doc comment.
- Block B (Harmonic patterns) and block C (Chart patterns) remain
  out-of-scope and will follow when the pattern-detection framework (pivot
  detector, multi-bar state machines) lands.

Touched across all bindings: Python, Node, WASM. Fuzz target, Python tests
(streaming-vs-batch + reference values), Node tests (streaming-vs-batch +
reference values), and a representative bench subset (1-, 2- and 3-bar
patterns) added. README family table + indicator counter (71 -> 86, eight
-> nine families) and CHANGELOG [Unreleased] updated.

* fix(family-14): unpack MULTI values with *_ to handle 3-element tuples

* cov(family-14): cover Default impl cold paths and MorningEveningStar guard branches
This commit is contained in:
kingchenc
2026-05-26 00:54:11 +02:00
committed by GitHub
parent 9b8e1346ed
commit 55284a3042
28 changed files with 3490 additions and 50 deletions
+99
View File
@@ -5946,6 +5946,105 @@ impl WasmOpeningRange {
}
}
// ============================== Candlestick Patterns ==============================
//
// All 15 patterns take Candles (open, high, low, close) and emit a signed f64
// signal per bar: +1.0 bullish, -1.0 bearish, 0.0 no pattern. Doji is
// direction-less and emits 0/+1 only.
macro_rules! wasm_candle_pattern {
($wasm:ident, $inner:ty, $js:ident) => {
#[wasm_bindgen(js_name = $js)]
pub struct $wasm {
inner: $inner,
}
impl Default for $wasm {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen(js_class = $js)]
impl $wasm {
#[wasm_bindgen(constructor)]
pub fn new() -> $wasm {
Self {
inner: <$inner>::new(),
}
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> Result<Option<f64>, JsError> {
let c = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
Ok(self.inner.update(c))
}
pub fn batch(
&mut self,
open: &[f64],
high: &[f64],
low: &[f64],
close: &[f64],
) -> Result<Float64Array, JsError> {
let n = open.len();
if high.len() != n || low.len() != n || close.len() != n {
return Err(JsError::new("open, high, low, close must be equal length"));
}
let mut out = Vec::with_capacity(n);
for i in 0..n {
let c = wc::Candle::new(open[i], high[i], low[i], close[i], 0.0, 0)
.map_err(map_err)?;
out.push(self.inner.update(c).unwrap_or(f64::NAN));
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
};
}
wasm_candle_pattern!(WasmDoji, wc::Doji, Doji);
wasm_candle_pattern!(WasmHammer, wc::Hammer, Hammer);
wasm_candle_pattern!(WasmInvertedHammer, wc::InvertedHammer, InvertedHammer);
wasm_candle_pattern!(WasmHangingMan, wc::HangingMan, HangingMan);
wasm_candle_pattern!(WasmShootingStar, wc::ShootingStar, ShootingStar);
wasm_candle_pattern!(WasmEngulfing, wc::Engulfing, Engulfing);
wasm_candle_pattern!(WasmHarami, wc::Harami, Harami);
wasm_candle_pattern!(
WasmMorningEveningStar,
wc::MorningEveningStar,
MorningEveningStar
);
wasm_candle_pattern!(
WasmThreeSoldiersOrCrows,
wc::ThreeSoldiersOrCrows,
ThreeSoldiersOrCrows
);
wasm_candle_pattern!(
WasmPiercingDarkCloud,
wc::PiercingDarkCloud,
PiercingDarkCloud
);
wasm_candle_pattern!(WasmMarubozu, wc::Marubozu, Marubozu);
wasm_candle_pattern!(WasmTweezer, wc::Tweezer, Tweezer);
wasm_candle_pattern!(WasmSpinningTop, wc::SpinningTop, SpinningTop);
wasm_candle_pattern!(WasmThreeInside, wc::ThreeInside, ThreeInside);
wasm_candle_pattern!(WasmThreeOutside, wc::ThreeOutside, ThreeOutside);
#[cfg(test)]
mod tests {
use super::*;