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:
@@ -180,6 +180,22 @@ const candleScalar = {
|
||||
TDCountdown: { make: () => new wickra.TDCountdown(4, 9, 2, 13), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
TDDifferential: { make: () => new wickra.TDDifferential(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
|
||||
TDOpen: { make: () => new wickra.TDOpen(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
// Family 14 — Candlestick patterns
|
||||
Doji: { make: () => new wickra.Doji(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Hammer: { make: () => new wickra.Hammer(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
InvertedHammer: { make: () => new wickra.InvertedHammer(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
HangingMan: { make: () => new wickra.HangingMan(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ShootingStar: { make: () => new wickra.ShootingStar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Engulfing: { make: () => new wickra.Engulfing(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Harami: { make: () => new wickra.Harami(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
MorningEveningStar: { make: () => new wickra.MorningEveningStar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ThreeSoldiersOrCrows: { make: () => new wickra.ThreeSoldiersOrCrows(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
PiercingDarkCloud: { make: () => new wickra.PiercingDarkCloud(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Marubozu: { make: () => new wickra.Marubozu(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
Tweezer: { make: () => new wickra.Tweezer(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
SpinningTop: { make: () => new wickra.SpinningTop(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ThreeInside: { make: () => new wickra.ThreeInside(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
ThreeOutside: { make: () => new wickra.ThreeOutside(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
|
||||
};
|
||||
|
||||
for (const [name, d] of Object.entries(candleScalar)) {
|
||||
|
||||
+19
-1
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, KAMA, RVI, PGO, KST, SMI, LaguerreRSI, ConnorsRSI, Inertia, ALMA, McGinleyDynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, APO, AwesomeOscillatorHistogram, CFO, ZeroLagMACD, ElderImpulse, STC, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, EaseOfMovement, KVO, VolumeOscillator, NVI, PVI, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, Vortex, RWI, WaveTrend, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, RVIVolatility, ParkinsonVolatility, GarmanKlassVolatility, RogersSatchellVolatility, YangZhangVolatility, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, SuperSmoother, FisherTransform, InverseFisherTransform, Decycler, DecyclerOscillator, RoofingFilter, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, SpearmanCorrelation } = nativeBinding
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, KAMA, RVI, PGO, KST, SMI, LaguerreRSI, ConnorsRSI, Inertia, ALMA, McGinleyDynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, APO, AwesomeOscillatorHistogram, CFO, ZeroLagMACD, ElderImpulse, STC, T3, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, EaseOfMovement, KVO, VolumeOscillator, NVI, PVI, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, Vortex, RWI, WaveTrend, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, RVIVolatility, ParkinsonVolatility, GarmanKlassVolatility, RogersSatchellVolatility, YangZhangVolatility, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, SuperSmoother, FisherTransform, InverseFisherTransform, Decycler, DecyclerOscillator, RoofingFilter, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, SpearmanCorrelation, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside } = nativeBinding
|
||||
|
||||
module.exports.version = version
|
||||
module.exports.SMA = SMA
|
||||
@@ -492,3 +492,21 @@ module.exports.HurstExponent = HurstExponent
|
||||
module.exports.PearsonCorrelation = PearsonCorrelation
|
||||
module.exports.Beta = Beta
|
||||
module.exports.SpearmanCorrelation = SpearmanCorrelation
|
||||
module.exports.ValueArea = ValueArea
|
||||
module.exports.InitialBalance = InitialBalance
|
||||
module.exports.OpeningRange = OpeningRange
|
||||
module.exports.Doji = Doji
|
||||
module.exports.Hammer = Hammer
|
||||
module.exports.InvertedHammer = InvertedHammer
|
||||
module.exports.HangingMan = HangingMan
|
||||
module.exports.ShootingStar = ShootingStar
|
||||
module.exports.Engulfing = Engulfing
|
||||
module.exports.Harami = Harami
|
||||
module.exports.MorningEveningStar = MorningEveningStar
|
||||
module.exports.ThreeSoldiersOrCrows = ThreeSoldiersOrCrows
|
||||
module.exports.PiercingDarkCloud = PiercingDarkCloud
|
||||
module.exports.Marubozu = Marubozu
|
||||
module.exports.Tweezer = Tweezer
|
||||
module.exports.SpinningTop = SpinningTop
|
||||
module.exports.ThreeInside = ThreeInside
|
||||
module.exports.ThreeOutside = ThreeOutside
|
||||
|
||||
@@ -8317,3 +8317,105 @@ impl OpeningRangeNode {
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
// ============================== 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! node_candle_pattern {
|
||||
($node:ident, $inner:ty, $js:literal) => {
|
||||
#[napi(js_name = $js)]
|
||||
pub struct $node {
|
||||
inner: $inner,
|
||||
}
|
||||
|
||||
impl Default for $node {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl $node {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: <$inner>::new(),
|
||||
}
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
open: f64,
|
||||
high: f64,
|
||||
low: f64,
|
||||
close: f64,
|
||||
) -> napi::Result<Option<f64>> {
|
||||
let candle = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
|
||||
Ok(self.inner.update(candle))
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
open: Vec<f64>,
|
||||
high: Vec<f64>,
|
||||
low: Vec<f64>,
|
||||
close: Vec<f64>,
|
||||
) -> napi::Result<Vec<f64>> {
|
||||
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(NapiError::from_reason(
|
||||
"open, high, low, close must be equal length".to_string(),
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(open.len());
|
||||
for i in 0..open.len() {
|
||||
let candle = wc::Candle::new(open[i], high[i], low[i], close[i], 0.0, 0)
|
||||
.map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
node_candle_pattern!(DojiNode, wc::Doji, "Doji");
|
||||
node_candle_pattern!(HammerNode, wc::Hammer, "Hammer");
|
||||
node_candle_pattern!(InvertedHammerNode, wc::InvertedHammer, "InvertedHammer");
|
||||
node_candle_pattern!(HangingManNode, wc::HangingMan, "HangingMan");
|
||||
node_candle_pattern!(ShootingStarNode, wc::ShootingStar, "ShootingStar");
|
||||
node_candle_pattern!(EngulfingNode, wc::Engulfing, "Engulfing");
|
||||
node_candle_pattern!(HaramiNode, wc::Harami, "Harami");
|
||||
node_candle_pattern!(
|
||||
MorningEveningStarNode,
|
||||
wc::MorningEveningStar,
|
||||
"MorningEveningStar"
|
||||
);
|
||||
node_candle_pattern!(
|
||||
ThreeSoldiersOrCrowsNode,
|
||||
wc::ThreeSoldiersOrCrows,
|
||||
"ThreeSoldiersOrCrows"
|
||||
);
|
||||
node_candle_pattern!(
|
||||
PiercingDarkCloudNode,
|
||||
wc::PiercingDarkCloud,
|
||||
"PiercingDarkCloud"
|
||||
);
|
||||
node_candle_pattern!(MarubozuNode, wc::Marubozu, "Marubozu");
|
||||
node_candle_pattern!(TweezerNode, wc::Tweezer, "Tweezer");
|
||||
node_candle_pattern!(SpinningTopNode, wc::SpinningTop, "SpinningTop");
|
||||
node_candle_pattern!(ThreeInsideNode, wc::ThreeInside, "ThreeInside");
|
||||
node_candle_pattern!(ThreeOutsideNode, wc::ThreeOutside, "ThreeOutside");
|
||||
|
||||
@@ -219,6 +219,22 @@ from ._wickra import (
|
||||
ValueArea,
|
||||
InitialBalance,
|
||||
OpeningRange,
|
||||
# Candlestick patterns
|
||||
Doji,
|
||||
Hammer,
|
||||
InvertedHammer,
|
||||
HangingMan,
|
||||
ShootingStar,
|
||||
Engulfing,
|
||||
Harami,
|
||||
MorningEveningStar,
|
||||
ThreeSoldiersOrCrows,
|
||||
PiercingDarkCloud,
|
||||
Marubozu,
|
||||
Tweezer,
|
||||
SpinningTop,
|
||||
ThreeInside,
|
||||
ThreeOutside,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -417,4 +433,20 @@ __all__ = [
|
||||
"ValueArea",
|
||||
"InitialBalance",
|
||||
"OpeningRange",
|
||||
# Candlestick patterns
|
||||
"Doji",
|
||||
"Hammer",
|
||||
"InvertedHammer",
|
||||
"HangingMan",
|
||||
"ShootingStar",
|
||||
"Engulfing",
|
||||
"Harami",
|
||||
"MorningEveningStar",
|
||||
"ThreeSoldiersOrCrows",
|
||||
"PiercingDarkCloud",
|
||||
"Marubozu",
|
||||
"Tweezer",
|
||||
"SpinningTop",
|
||||
"ThreeInside",
|
||||
"ThreeOutside",
|
||||
]
|
||||
|
||||
@@ -11054,6 +11054,109 @@ impl PyOpeningRange {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Candlestick Patterns ==============================
|
||||
//
|
||||
// All 15 patterns take Candles and emit a signed f64 signal per bar:
|
||||
// +1.0 bullish, -1.0 bearish, 0.0 no pattern. Doji is direction-less, so it
|
||||
// uses +1.0 / 0.0 only.
|
||||
|
||||
macro_rules! candle_pattern_no_param {
|
||||
($name:ident, $inner:ty, $repr:expr) => {
|
||||
#[pyclass(name = $repr, module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct $name {
|
||||
inner: $inner,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl $name {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: <$inner>::new(),
|
||||
}
|
||||
}
|
||||
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
|
||||
let c = extract_candle(candle)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
open: PyReadonlyArray1<'py, f64>,
|
||||
high: PyReadonlyArray1<'py, f64>,
|
||||
low: PyReadonlyArray1<'py, f64>,
|
||||
close: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let o = open
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let h = high
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let l = low
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
let c = close
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
if o.len() != h.len() || h.len() != l.len() || l.len() != c.len() {
|
||||
return Err(PyValueError::new_err(
|
||||
"open, high, low, close must be equal length",
|
||||
));
|
||||
}
|
||||
let mut out = Vec::with_capacity(o.len());
|
||||
for i in 0..o.len() {
|
||||
let candle =
|
||||
wc::Candle::new(o[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
|
||||
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("{}()", $repr)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
candle_pattern_no_param!(PyDoji, wc::Doji, "Doji");
|
||||
candle_pattern_no_param!(PyHammer, wc::Hammer, "Hammer");
|
||||
candle_pattern_no_param!(PyInvertedHammer, wc::InvertedHammer, "InvertedHammer");
|
||||
candle_pattern_no_param!(PyHangingMan, wc::HangingMan, "HangingMan");
|
||||
candle_pattern_no_param!(PyShootingStar, wc::ShootingStar, "ShootingStar");
|
||||
candle_pattern_no_param!(PyEngulfing, wc::Engulfing, "Engulfing");
|
||||
candle_pattern_no_param!(PyHarami, wc::Harami, "Harami");
|
||||
candle_pattern_no_param!(
|
||||
PyMorningEveningStar,
|
||||
wc::MorningEveningStar,
|
||||
"MorningEveningStar"
|
||||
);
|
||||
candle_pattern_no_param!(
|
||||
PyThreeSoldiersOrCrows,
|
||||
wc::ThreeSoldiersOrCrows,
|
||||
"ThreeSoldiersOrCrows"
|
||||
);
|
||||
candle_pattern_no_param!(
|
||||
PyPiercingDarkCloud,
|
||||
wc::PiercingDarkCloud,
|
||||
"PiercingDarkCloud"
|
||||
);
|
||||
candle_pattern_no_param!(PyMarubozu, wc::Marubozu, "Marubozu");
|
||||
candle_pattern_no_param!(PyTweezer, wc::Tweezer, "Tweezer");
|
||||
candle_pattern_no_param!(PySpinningTop, wc::SpinningTop, "SpinningTop");
|
||||
candle_pattern_no_param!(PyThreeInside, wc::ThreeInside, "ThreeInside");
|
||||
candle_pattern_no_param!(PyThreeOutside, wc::ThreeOutside, "ThreeOutside");
|
||||
|
||||
// ============================== Module ==============================
|
||||
|
||||
#[pymodule]
|
||||
@@ -11245,5 +11348,21 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyValueArea>()?;
|
||||
m.add_class::<PyInitialBalance>()?;
|
||||
m.add_class::<PyOpeningRange>()?;
|
||||
// Candlestick patterns.
|
||||
m.add_class::<PyDoji>()?;
|
||||
m.add_class::<PyHammer>()?;
|
||||
m.add_class::<PyInvertedHammer>()?;
|
||||
m.add_class::<PyHangingMan>()?;
|
||||
m.add_class::<PyShootingStar>()?;
|
||||
m.add_class::<PyEngulfing>()?;
|
||||
m.add_class::<PyHarami>()?;
|
||||
m.add_class::<PyMorningEveningStar>()?;
|
||||
m.add_class::<PyThreeSoldiersOrCrows>()?;
|
||||
m.add_class::<PyPiercingDarkCloud>()?;
|
||||
m.add_class::<PyMarubozu>()?;
|
||||
m.add_class::<PyTweezer>()?;
|
||||
m.add_class::<PySpinningTop>()?;
|
||||
m.add_class::<PyThreeInside>()?;
|
||||
m.add_class::<PyThreeOutside>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -325,6 +325,68 @@ CANDLE_SCALAR = {
|
||||
lambda: ta.TDDifferential(),
|
||||
lambda ind, h, l, c, v: ind.batch(h, l, c),
|
||||
),
|
||||
# Candlestick patterns -- batch takes (open, high, low, close) and the
|
||||
# streaming side feeds close as open (same convention as BalanceOfPower).
|
||||
"Doji": (
|
||||
lambda: ta.Doji(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Hammer": (
|
||||
lambda: ta.Hammer(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"InvertedHammer": (
|
||||
lambda: ta.InvertedHammer(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"HangingMan": (
|
||||
lambda: ta.HangingMan(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"ShootingStar": (
|
||||
lambda: ta.ShootingStar(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Engulfing": (
|
||||
lambda: ta.Engulfing(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Harami": (
|
||||
lambda: ta.Harami(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"MorningEveningStar": (
|
||||
lambda: ta.MorningEveningStar(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"ThreeSoldiersOrCrows": (
|
||||
lambda: ta.ThreeSoldiersOrCrows(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"PiercingDarkCloud": (
|
||||
lambda: ta.PiercingDarkCloud(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Marubozu": (
|
||||
lambda: ta.Marubozu(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"Tweezer": (
|
||||
lambda: ta.Tweezer(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"SpinningTop": (
|
||||
lambda: ta.SpinningTop(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"ThreeInside": (
|
||||
lambda: ta.ThreeInside(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
"ThreeOutside": (
|
||||
lambda: ta.ThreeOutside(),
|
||||
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1551,3 +1613,117 @@ def test_heikin_ashi_lifecycle_and_reset():
|
||||
assert ha.is_ready() is True
|
||||
ha.reset()
|
||||
assert ha.is_ready() is False
|
||||
# --- Candlestick pattern reference values --------------------------------
|
||||
|
||||
|
||||
def test_doji_reference():
|
||||
# body 0, range 2 -> doji.
|
||||
assert ta.Doji().update((10.0, 11.0, 9.0, 10.0, 1.0, 0)) == pytest.approx(1.0)
|
||||
# Marubozu shape -> not a doji.
|
||||
assert ta.Doji().update((10.0, 12.0, 10.0, 12.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_hammer_reference():
|
||||
# body 0.5, lower shadow 5.0, upper 0.1.
|
||||
assert ta.Hammer().update((10.0, 10.6, 5.0, 10.5, 1.0, 0)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_inverted_hammer_reference():
|
||||
assert ta.InvertedHammer().update(
|
||||
(10.0, 15.0, 9.9, 10.5, 1.0, 0)
|
||||
) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_hanging_man_reference():
|
||||
assert ta.HangingMan().update((10.0, 10.6, 5.0, 10.5, 1.0, 0)) == pytest.approx(
|
||||
-1.0
|
||||
)
|
||||
|
||||
|
||||
def test_shooting_star_reference():
|
||||
assert ta.ShootingStar().update(
|
||||
(10.0, 15.0, 9.9, 10.5, 1.0, 0)
|
||||
) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_engulfing_reference():
|
||||
e = ta.Engulfing()
|
||||
assert e.update((11.0, 11.2, 9.8, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert e.update((9.5, 12.0, 9.5, 11.5, 1.0, 1)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_harami_reference():
|
||||
h = ta.Harami()
|
||||
assert h.update((12.0, 12.5, 9.5, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert h.update((10.5, 11.5, 10.4, 11.0, 1.0, 1)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_morning_evening_star_reference():
|
||||
m = ta.MorningEveningStar()
|
||||
assert m.update((12.0, 12.2, 9.5, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert m.update((9.9, 10.1, 9.7, 9.95, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert m.update((10.1, 12.0, 10.0, 11.8, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_three_soldiers_reference():
|
||||
t = ta.ThreeSoldiersOrCrows()
|
||||
assert t.update((10.0, 11.5, 9.9, 11.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((10.5, 12.5, 10.4, 12.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((11.5, 13.5, 11.4, 13.0, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_piercing_dark_cloud_reference():
|
||||
p = ta.PiercingDarkCloud()
|
||||
assert p.update((12.0, 12.5, 10.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert p.update((9.8, 11.8, 9.5, 11.5, 1.0, 1)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_marubozu_reference():
|
||||
# Bullish marubozu: open == low, close == high.
|
||||
assert ta.Marubozu().update(
|
||||
(10.0, 12.0, 10.0, 12.0, 1.0, 0)
|
||||
) == pytest.approx(1.0)
|
||||
assert ta.Marubozu().update(
|
||||
(12.0, 12.0, 10.0, 10.0, 1.0, 0)
|
||||
) == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_tweezer_reference():
|
||||
t = ta.Tweezer()
|
||||
assert t.update((11.0, 12.0, 9.5, 9.6, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((9.7, 10.5, 9.5, 10.2, 1.0, 1)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_spinning_top_reference():
|
||||
# body 0.5, both shadows 3.0, range 6.5 -> body/range ~= 0.077.
|
||||
assert ta.SpinningTop().update(
|
||||
(10.0, 13.5, 7.0, 10.5, 1.0, 0)
|
||||
) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_three_inside_reference():
|
||||
t = ta.ThreeInside()
|
||||
assert t.update((12.0, 12.5, 9.5, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((10.5, 11.5, 10.4, 11.0, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((11.0, 13.0, 10.9, 12.5, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_three_outside_reference():
|
||||
t = ta.ThreeOutside()
|
||||
assert t.update((11.0, 11.2, 9.8, 10.0, 1.0, 0)) == pytest.approx(0.0)
|
||||
assert t.update((9.5, 12.0, 9.5, 11.5, 1.0, 1)) == pytest.approx(0.0)
|
||||
assert t.update((11.5, 13.0, 11.4, 12.5, 1.0, 2)) == pytest.approx(1.0)
|
||||
|
||||
|
||||
# --- Lifecycle ------------------------------------------------------------
|
||||
|
||||
|
||||
def test_new_indicators_expose_lifecycle():
|
||||
instances = [make() for make, _ in CANDLE_SCALAR.values()]
|
||||
instances += [make() for make, *_ in MULTI.values()]
|
||||
instances += [cls(*args) for cls, args in SCALAR]
|
||||
for ind in instances:
|
||||
assert ind.is_ready() is False
|
||||
assert ind.warmup_period() >= 1
|
||||
ind.reset()
|
||||
assert ind.is_ready() is False
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user