feat: add Candlestick Patterns deepening (B14, 6 indicators) (#209)

B14 of the family-deepening roadmap — six candlestick patterns (479 -> 485), all in the **Candlestick Patterns** family.

**Fixed-lookback (candle-pattern macro bindings, neutral 0.0 during warmup):**
- **Tristar** — three-doji star reversal.
- **Harami Cross** — Harami whose second candle is a contained doji.
- **Tower Top/Bottom** — tall bar, small pause, tall opposite bar.

**Windowed / parameterized (hand-bound, `candle -> f64`):**
- **Frying Pan Bottom** — rounded U-shaped accumulation base, recovery-confirmed.
- **Dumpling Top** — rounded dome-shaped distribution top, breakdown-confirmed.
- **New Price Lines** — run of N consecutive new closing highs (+1) / lows (-1).

Window/Gap (Rising-Falling) dropped (SKIP — existing gap coverage). Wiring complete across core, Python, Node, WASM, fuzz, tests, README + docs counter (485) and CHANGELOG. Verified: core 3966 + doc 435, clippy clean, node 560, python 922.
This commit is contained in:
kingchenc
2026-06-08 02:29:24 +02:00
committed by GitHub
parent d16df1e224
commit 943825d6a0
20 changed files with 1979 additions and 48 deletions
@@ -386,6 +386,12 @@ const candleScalar = {
TDDWave: { make: () => new wickra.TDDWave(2), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
HeikinAshiOscillator: { make: () => new wickra.HeikinAshiOscillator(5), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
ThreeLineBreak: { make: () => new wickra.ThreeLineBreak(3), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
Tristar: { make: () => new wickra.Tristar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
HaramiCross: { make: () => new wickra.HaramiCross(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
TowerTopBottom: { make: () => new wickra.TowerTopBottom(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
DumplingTop: { make: () => new wickra.DumplingTop(9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
NewPriceLines: { make: () => new wickra.NewPriceLines(5), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
FryPanBottom: { make: () => new wickra.FryPanBottom(9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
};
for (const [name, d] of Object.entries(candleScalar)) {
+54
View File
@@ -3421,6 +3421,33 @@ export declare class CandleVolume {
isReady(): boolean
warmupPeriod(): number
}
export type FryPanBottomNode = FryPanBottom
export declare class FryPanBottom {
constructor(period: number)
update(high: number, low: number, close: number): number | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type DumplingTopNode = DumplingTop
export declare class DumplingTop {
constructor(period: number)
update(high: number, low: number, close: number): number | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type NewPriceLinesNode = NewPriceLines
export declare class NewPriceLines {
constructor(count: number)
update(high: number, low: number, close: number): number | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type ValueAreaNode = ValueArea
export declare class ValueArea {
constructor(period: number, binCount: number, valueAreaPct: number)
@@ -4198,6 +4225,33 @@ export declare class TDTrap {
isReady(): boolean
warmupPeriod(): number
}
export type TristarNode = Tristar
export declare class Tristar {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type HaramiCrossNode = HaramiCross
export declare class HaramiCross {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TowerTopBottomNode = TowerTopBottom
export declare class TowerTopBottom {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1
export declare class OrderBookImbalanceTop1 {
constructor()
File diff suppressed because one or more lines are too long
+168
View File
@@ -12618,6 +12618,171 @@ impl CandleVolumeNode {
}
}
// ============================== Frying Pan Bottom ==============================
#[napi(js_name = "FryPanBottom")]
pub struct FryPanBottomNode {
inner: wc::FryPanBottom,
}
#[napi]
impl FryPanBottomNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::FryPanBottom::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
out.push(
self.inner
.update(cnd(high[i], low[i], close[i], 0.0)?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== Dumpling Top ==============================
#[napi(js_name = "DumplingTop")]
pub struct DumplingTopNode {
inner: wc::DumplingTop,
}
#[napi]
impl DumplingTopNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::DumplingTop::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
out.push(
self.inner
.update(cnd(high[i], low[i], close[i], 0.0)?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== New Price Lines ==============================
#[napi(js_name = "NewPriceLines")]
pub struct NewPriceLinesNode {
inner: wc::NewPriceLines,
}
#[napi]
impl NewPriceLinesNode {
#[napi(constructor)]
pub fn new(count: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::NewPriceLines::new(count as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
out.push(
self.inner
.update(cnd(high[i], low[i], close[i], 0.0)?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== ValueArea ==============================
#[napi(object)]
@@ -13271,6 +13436,9 @@ node_candle_pattern!(TdClopNode, wc::TdClop, "TDClop");
node_candle_pattern!(TdClopwinNode, wc::TdClopwin, "TDClopwin");
node_candle_pattern!(TdPropulsionNode, wc::TdPropulsion, "TDPropulsion");
node_candle_pattern!(TdTrapNode, wc::TdTrap, "TDTrap");
node_candle_pattern!(TristarNode, wc::Tristar, "Tristar");
node_candle_pattern!(HaramiCrossNode, wc::HaramiCross, "HaramiCross");
node_candle_pattern!(TowerTopBottomNode, wc::TowerTopBottom, "TowerTopBottom");
// ============================== Microstructure: Order Book ==============================
//