From 8431b1400ccf6580185b0f5643b05fada31d0099 Mon Sep 17 00:00:00 2001 From: kingchenc Date: Mon, 8 Jun 2026 01:12:46 +0200 Subject: [PATCH] feat: add DeMark deepening (B12, 7 indicators) (#204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B12 of the family-deepening roadmap — seven Tom DeMark indicators (467 -> 474). **Candle -> +1/0 qualifier patterns (candlestick macro bindings):** - **TD Camouflage** — hidden intrabar strength/weakness against the prior close. - **TD Clop** — two-bar open/close engulfing reversal. - **TD Clopwin** — the inside-body cousin of TD Clop (compression bar). - **TD Propulsion** — continuation thrust closing beyond the prior extreme. - **TD Trap** — inside ("trap") bar followed by a range breakout. **Hand-bound:** - **TD D-Wave** — streaming Elliott-style 1-5 / A-C swing-wave counter (candle -> f64, `strength` param). - **TD Moving Averages** — ST1/ST2 median-price trend ribbon (candle -> struct {st1, st2}). All seven join the existing **DeMark** family. Patterns follow the house-style +1/0 candle-pattern convention (neutral 0.0 during warmup). Public binding names use the family-consistent `TD...` casing. Wiring complete across core, Python, Node, WASM, fuzz, tests, README + docs counter (474) and CHANGELOG. Verified: core 3874 + doc 427, clippy clean, node 549, python 903. --- CHANGELOG.md | 7 + README.md | 16 +- bindings/node/__tests__/indicators.test.js | 7 + bindings/node/index.d.ts | 67 ++++ bindings/node/index.js | 9 +- bindings/node/src/lib.rs | 123 ++++++++ bindings/python/python/wickra/__init__.py | 14 + bindings/python/src/lib.rs | 137 ++++++++ bindings/python/tests/test_new_indicators.py | 61 ++++ bindings/wasm/src/lib.rs | 108 +++++++ crates/wickra-core/src/indicators/mod.rs | 23 +- .../src/indicators/td_camouflage.rs | 156 +++++++++ crates/wickra-core/src/indicators/td_clop.rs | 159 ++++++++++ .../wickra-core/src/indicators/td_clopwin.rs | 162 ++++++++++ crates/wickra-core/src/indicators/td_dwave.rs | 295 ++++++++++++++++++ .../src/indicators/td_moving_average.rs | 229 ++++++++++++++ .../src/indicators/td_propulsion.rs | 152 +++++++++ crates/wickra-core/src/indicators/td_trap.rs | 171 ++++++++++ crates/wickra-core/src/lib.rs | 7 +- docs/README.md | 2 +- fuzz/fuzz_targets/indicator_update_candle.rs | 9 +- 21 files changed, 1899 insertions(+), 15 deletions(-) create mode 100644 crates/wickra-core/src/indicators/td_camouflage.rs create mode 100644 crates/wickra-core/src/indicators/td_clop.rs create mode 100644 crates/wickra-core/src/indicators/td_clopwin.rs create mode 100644 crates/wickra-core/src/indicators/td_dwave.rs create mode 100644 crates/wickra-core/src/indicators/td_moving_average.rs create mode 100644 crates/wickra-core/src/indicators/td_propulsion.rs create mode 100644 crates/wickra-core/src/indicators/td_trap.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b8f37e4..0f2e2dae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- **TD Camouflage** — a DeMark qualifier flagging hidden intrabar strength or weakness against the prior close (`TDCamouflage`). +- **TD Clop** — a DeMark two-bar open/close engulfing reversal where the bar opens beyond and closes back across the prior body (`TDClop`). +- **TD Clopwin** — the inside-body cousin of TD Clop, marking a compression bar whose direction hints at the next move (`TDClopwin`). +- **TD Propulsion** — a DeMark continuation thrust that opens on the trend side and closes beyond the prior bar's extreme (`TDPropulsion`). +- **TD Trap** — an inside ("trap") bar followed by a close beyond its range, triggering a directional breakout signal (`TDTrap`). +- **TD D-Wave** — a streaming Elliott-style swing-wave counter labelling the market's 1–5 impulse / A–C correction sequence (`TDDWave`). +- **TD Moving Averages** — the DeMark ST1 (fast) and ST2 (slow) median-price trend ribbon whose crossover frames the trend (`TDMovingAverage`). ## [0.6.6] - 2026-06-08 - **Pivot Reversal** — a breakout signal when price closes through the most recently confirmed swing pivot (`PIVOT_REVERSAL`). diff --git a/README.md b/README.md index 22676060..47a0fe58 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Wickra — streaming-first technical indicators + Wickra — streaming-first technical indicators

[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml) @@ -48,7 +48,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**: [Node](https://docs.wickra.org/Quickstart-Node), [WASM](https://docs.wickra.org/Quickstart-WASM). - **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for - every one of the 467 indicators; start at the + every one of the 474 indicators; start at the [indicators overview](https://docs.wickra.org/Indicators-Overview). - **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods), [streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch), @@ -66,7 +66,7 @@ an afterthought — **live, tick-by-tick data** — without giving up the breadt a full batch library, and without making you reimplement your indicators four times to get there. -- **The biggest streaming-native catalogue, period.** 467 indicators across 24 +- **The biggest streaming-native catalogue, period.** 474 indicators across 24 families — candlesticks, harmonic & chart patterns, market profile, market breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and @@ -77,7 +77,7 @@ times to get there. - **Correct by construction, not by hope.** Every `update` validates its input, runs a real warmup, and returns an `Option` so a single bad tick can't silently poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered - for all 467 indicators**. + for all 474 indicators**. - **Orders of magnitude faster where it counts.** In streaming Wickra is **9–58×** faster than the only other incremental peer and **thousands of times** faster than recompute-on-every-tick libraries. On batch it wins several rows outright @@ -95,7 +95,7 @@ Every other library forces one of those compromises. Wickra doesn't: | Library | Install | Streaming | Languages | Indicators | Active | |------------------|-------------|-------------|-----------------------------|-----------:|--------| -| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **467** | **yes** | +| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **474** | **yes** | | kand | clean | yes | Python · WASM · Rust | ~60 | yes | | ta-rs | clean | yes | Rust only | ~30 | stale | | yata | clean | partial | Rust only | ~35 | yes | @@ -128,7 +128,7 @@ Full tables (Rust + Python, streaming + batch) and how to reproduce them live in ## Indicators -467 streaming-first indicators across twenty-four families. Every one passes the +474 streaming-first indicators across twenty-four families. Every one passes the `batch == streaming` equivalence test, reference-value tests, and reset semantics tests. Each has a per-indicator deep dive (formula, parameters, warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview). @@ -146,7 +146,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview). | Price Statistics | Typical Price, Median Price, Weighted Close, Linear Regression, Linear Regression Slope, Z-Score, Linear Regression Angle, Variance, Coefficient of Variation, Skewness, Kurtosis, Standard Error, Detrended StdDev, R², Median Absolute Deviation, Autocorrelation, Hurst Exponent, Pearson Correlation, Beta, Pairwise Beta, Pair Spread Z-Score, Lead-Lag Cross-Correlation, Cointegration, Relative Strength A-vs-B, Spearman Correlation, Mid Price, Mid Point, Average Price, Linear Regression Intercept, Time Series Forecast, Rolling Correlation, Rolling Covariance, OU Half-Life, Spread Hurst, Distance SSD, Beta-Neutral Spread, Variance Ratio, Granger Causality, Kalman Hedge Ratio, Spread Bollinger Bands, Spread AR(1) Coefficient, Jarque-Bera, Rolling Min-Max Scaler, Shannon Entropy, Sample Entropy, Kendall Tau | | Ehlers / Cycle (DSP) | MAMA, FAMA, Fisher Transform, Inverse Fisher Transform, SuperSmoother, Hilbert Dominant Cycle, Hilbert Phasor, Hilbert DC Phase, Hilbert Trend Mode, Sine Wave, Decycler, Decycler Oscillator, Roofing Filter, Center of Gravity, Cybernetic Cycle, Adaptive Cycle, Empirical Mode Decomposition, Ehlers Stochastic, Instantaneous Trendline, Highpass Filter, Reflex, Trendflex, Correlation Trend Indicator, Adaptive RSI, Universal Oscillator, Adaptive CCI, Bandpass Filter, Even Better Sinewave, Autocorrelation Periodogram | | Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag, Central Pivot Range, Murrey Math Lines, Andrews Pitchfork, Volume-Weighted Support/Resistance, Pivot Reversal | -| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level | +| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level, TD Camouflage, TD Clop, TD Clopwin, TD Propulsion, TD Trap, TD D-Wave, TD Moving Averages | | Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi | | Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns) | | Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri, Closing Marubozu, Opening Marubozu, Tasuki Gap, Unique Three River, Concealing Baby Swallow | @@ -237,7 +237,7 @@ A Python live-trading example using the public `websockets` package lives at ``` wickra/ ├── crates/ -│ ├── wickra-core/ core engine + all 467 indicators +│ ├── wickra-core/ core engine + all 474 indicators │ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/ │ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds │ └── wickra-bench/ internal cross-library benchmark harness (not published) diff --git a/bindings/node/__tests__/indicators.test.js b/bindings/node/__tests__/indicators.test.js index 265f89ec..72307a2b 100644 --- a/bindings/node/__tests__/indicators.test.js +++ b/bindings/node/__tests__/indicators.test.js @@ -378,6 +378,12 @@ const candleScalar = { BetterVolume: { make: () => new wickra.BetterVolume(14), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, ADAPTIVECCI: { make: () => new wickra.ADAPTIVECCI(20), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, PivotReversal: { make: () => new wickra.PivotReversal(1, 1), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, + TDCamouflage: { make: () => new wickra.TDCamouflage(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + TDClop: { make: () => new wickra.TDClop(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + TDClopwin: { make: () => new wickra.TDClopwin(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + TDPropulsion: { make: () => new wickra.TDPropulsion(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + TDTrap: { make: () => new wickra.TDTrap(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, + TDDWave: { make: () => new wickra.TDDWave(2), 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)) { @@ -479,6 +485,7 @@ const multi = { MurreyMathLines: { make: () => new wickra.MurreyMathLines(4), fields: ['mm8_8', 'mm7_8', 'mm6_8', 'mm5_8', 'mm4_8', 'mm3_8', 'mm2_8', 'mm1_8', 'mm0_8'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, AndrewsPitchfork: { make: () => new wickra.AndrewsPitchfork(2), fields: ['median', 'upper', 'lower'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, VolumeWeightedSr: { make: () => new wickra.VolumeWeightedSr(3), fields: ['support', 'resistance'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) }, + TDMovingAverage: { make: () => new wickra.TDMovingAverage(5, 13), fields: ['st1', 'st2'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, }; for (const [name, d] of Object.entries(multi)) { diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index a83e35f6..3fa231f0 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -342,6 +342,10 @@ export interface TdSequentialValue { countdown: number direction: number } +export interface TdMovingAverageValue { + st1: number + st2: number +} /** TD Lines output pair: latest TDST resistance / support (NaN if unset). */ export interface TdLinesValue { resistance: number @@ -3159,6 +3163,24 @@ export declare class TDCombo { isReady(): boolean warmupPeriod(): number } +export type TdDWaveNode = TDDWave +export declare class TDDWave { + constructor(strength: number) + update(high: number, low: number, close: number): number | null + batch(high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type TdMovingAverageNode = TDMovingAverage +export declare class TDMovingAverage { + constructor(periodSt1: number, periodSt2: number) + update(high: number, low: number): TdMovingAverageValue | null + batch(high: Array, low: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} export type TdCountdownNode = TDCountdown export declare class TDCountdown { constructor(setupLookback: number, setupTarget: number, countdownLookback: number, countdownTarget: number) @@ -4072,6 +4094,51 @@ export declare class ThreeDrives { isReady(): boolean warmupPeriod(): number } +export type TdCamouflageNode = TDCamouflage +export declare class TDCamouflage { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type TdClopNode = TDClop +export declare class TDClop { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type TdClopwinNode = TDClopwin +export declare class TDClopwin { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type TdPropulsionNode = TDPropulsion +export declare class TDPropulsion { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type TdTrapNode = TDTrap +export declare class TDTrap { + constructor() + update(open: number, high: number, low: number, close: number): number | null + batch(open: Array, high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1 export declare class OrderBookImbalanceTop1 { constructor() diff --git a/bindings/node/index.js b/bindings/node/index.js index d9e9f74b..7b952910 100644 --- a/bindings/node/index.js +++ b/bindings/node/index.js @@ -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, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, LogReturn, RealizedVolatility, RollingIqr, RollingPercentileRank, TrendLabel, WinRate, Expectancy, SWMA, GMA, EHMA, MedianMA, AdaptiveLaguerre, DisparityIndex, FisherRSI, RSX, DynamicMomentumIndex, TREND_STRENGTH_INDEX, TsfOscillator, BipowerVariation, JARQUEBERA, ROLLINGMINMAX, HIGHPASS, REFLEX, TRENDFLEX, CTI, ADAPTIVERSI, UNIVERSALOSC, BANDPASS, EVENBETTERSINE, AUTOCORRPGRAM, SHANNONENT, SAMPLEENT, EwmaVolatility, Garch11, VolatilityOfVolatility, VolatilityCone, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, KendallTau, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, VolatilityRatio, ProjectionOscillator, TimeBasedStop, ADAPTIVECCI, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, GD, HoltWinters, RMI, DerivativeOscillator, MacdHistogram, PpoHistogram, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, KaseDevStop, ElderSafeZone, AtrRatchet, Nrtr, ModifiedMaStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, QuartileBands, BomarBands, MedianChannel, ProjectionBands, CentralPivotRange, MurreyMathLines, AndrewsPitchfork, VolumeWeightedSr, PivotReversal, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, DoubleTopBottom, TripleTopBottom, HeadAndShoulders, Triangle, Wedge, FlagPennant, RectangleRange, CupAndHandle, Abcd, Gartley, Butterfly, Bat, Crab, Shark, Cypher, ThreeDrives, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn, FibRetracement, FibExtension, FibProjection, AutoFib, GoldenPocket, FibConfluence, FibFan, FibArcs, FibChannel, FibTimeZones, VolumeRsi, Wad, TwiggsMoneyFlow, TradeVolumeIndex, IntradayIntensity, BetterVolume, VolumeWeightedMacd } = nativeBinding +const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, LogReturn, RealizedVolatility, RollingIqr, RollingPercentileRank, TrendLabel, WinRate, Expectancy, SWMA, GMA, EHMA, MedianMA, AdaptiveLaguerre, DisparityIndex, FisherRSI, RSX, DynamicMomentumIndex, TREND_STRENGTH_INDEX, TsfOscillator, BipowerVariation, JARQUEBERA, ROLLINGMINMAX, HIGHPASS, REFLEX, TRENDFLEX, CTI, ADAPTIVERSI, UNIVERSALOSC, BANDPASS, EVENBETTERSINE, AUTOCORRPGRAM, SHANNONENT, SAMPLEENT, EwmaVolatility, Garch11, VolatilityOfVolatility, VolatilityCone, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, KendallTau, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, VolatilityRatio, ProjectionOscillator, TimeBasedStop, ADAPTIVECCI, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, GD, HoltWinters, RMI, DerivativeOscillator, MacdHistogram, PpoHistogram, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, KaseDevStop, ElderSafeZone, AtrRatchet, Nrtr, ModifiedMaStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, QuartileBands, BomarBands, MedianChannel, ProjectionBands, CentralPivotRange, MurreyMathLines, AndrewsPitchfork, VolumeWeightedSr, PivotReversal, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDDWave, TDMovingAverage, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, DoubleTopBottom, TripleTopBottom, HeadAndShoulders, Triangle, Wedge, FlagPennant, RectangleRange, CupAndHandle, Abcd, Gartley, Butterfly, Bat, Crab, Shark, Cypher, ThreeDrives, TDCamouflage, TDClop, TDClopwin, TDPropulsion, TDTrap, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn, FibRetracement, FibExtension, FibProjection, AutoFib, GoldenPocket, FibConfluence, FibFan, FibArcs, FibChannel, FibTimeZones, VolumeRsi, Wad, TwiggsMoneyFlow, TradeVolumeIndex, IntradayIntensity, BetterVolume, VolumeWeightedMacd } = nativeBinding module.exports.version = version module.exports.SMA = SMA @@ -587,6 +587,8 @@ module.exports.TDDeMarker = TDDeMarker module.exports.TDREI = TDREI module.exports.TDPressure = TDPressure module.exports.TDCombo = TDCombo +module.exports.TDDWave = TDDWave +module.exports.TDMovingAverage = TDMovingAverage module.exports.TDCountdown = TDCountdown module.exports.TDLines = TDLines module.exports.TDRangeProjection = TDRangeProjection @@ -687,6 +689,11 @@ module.exports.Crab = Crab module.exports.Shark = Shark module.exports.Cypher = Cypher module.exports.ThreeDrives = ThreeDrives +module.exports.TDCamouflage = TDCamouflage +module.exports.TDClop = TDClop +module.exports.TDClopwin = TDClopwin +module.exports.TDPropulsion = TDPropulsion +module.exports.TDTrap = TDTrap module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1 module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull module.exports.Microprice = Microprice diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index 984cc8ca..25d5f853 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -11100,6 +11100,124 @@ impl TdComboNode { } } +// ============================== TD D-Wave ============================== + +#[napi(js_name = "TDDWave")] +pub struct TdDWaveNode { + inner: wc::TdDWave, +} + +#[napi] +impl TdDWaveNode { + #[napi(constructor)] + pub fn new(strength: u32) -> napi::Result { + Ok(Self { + inner: wc::TdDWave::new(strength as usize).map_err(map_err)?, + }) + } + #[napi] + pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result> { + Ok(self.inner.update(cnd(high, low, close, 0.0)?)) + } + #[napi] + pub fn batch( + &mut self, + high: Vec, + low: Vec, + close: Vec, + ) -> napi::Result> { + 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 + } +} + +// ============================== TD Moving Averages ============================== + +#[napi(object)] +pub struct TdMovingAverageValue { + pub st1: f64, + pub st2: f64, +} + +#[napi(js_name = "TDMovingAverage")] +pub struct TdMovingAverageNode { + inner: wc::TdMovingAverage, +} + +#[napi] +impl TdMovingAverageNode { + #[napi(constructor)] + pub fn new(period_st1: u32, period_st2: u32) -> napi::Result { + Ok(Self { + inner: wc::TdMovingAverage::new(period_st1 as usize, period_st2 as usize) + .map_err(map_err)?, + }) + } + #[napi] + pub fn update(&mut self, high: f64, low: f64) -> napi::Result> { + Ok(self + .inner + .update(cnd(high, low, low, 0.0)?) + .map(|o| TdMovingAverageValue { + st1: o.st1, + st2: o.st2, + })) + } + #[napi] + pub fn batch(&mut self, high: Vec, low: Vec) -> napi::Result> { + if high.len() != low.len() { + return Err(NapiError::from_reason( + "high, low must be equal length".to_string(), + )); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 2]; + for i in 0..n { + if let Some(o) = self.inner.update(cnd(high[i], low[i], low[i], 0.0)?) { + out[i * 2] = o.st1; + out[i * 2 + 1] = o.st2; + } + } + 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 + } +} + // ============================== TD Countdown ============================== #[napi(js_name = "TDCountdown")] @@ -12807,6 +12925,11 @@ node_candle_pattern!(CrabNode, wc::Crab, "Crab"); node_candle_pattern!(SharkNode, wc::Shark, "Shark"); node_candle_pattern!(CypherNode, wc::Cypher, "Cypher"); node_candle_pattern!(ThreeDrivesNode, wc::ThreeDrives, "ThreeDrives"); +node_candle_pattern!(TdCamouflageNode, wc::TdCamouflage, "TDCamouflage"); +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"); // ============================== Microstructure: Order Book ============================== // diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py index e5727f1d..c78335c9 100644 --- a/bindings/python/python/wickra/__init__.py +++ b/bindings/python/python/wickra/__init__.py @@ -322,6 +322,13 @@ from ._wickra import ( WilliamsFractals, ZigZag, # DeMark + TDMovingAverage, + TDDWave, + TDTrap, + TDPropulsion, + TDClopwin, + TDClop, + TDCamouflage, TDSetup, TDSequential, TDDeMarker, @@ -819,6 +826,13 @@ __all__ = [ "WilliamsFractals", "ZigZag", # DeMark + "TDMovingAverage", + "TDDWave", + "TDTrap", + "TDPropulsion", + "TDClopwin", + "TDClop", + "TDCamouflage", "TDSetup", "TDSequential", "TDDeMarker", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 6f96bd79..a291a1ca 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -14086,6 +14086,131 @@ impl PyTdCombo { } } +// ============================== TD D-Wave ============================== + +#[pyclass(name = "TDDWave", module = "wickra._wickra", skip_from_py_object)] +#[derive(Clone)] +struct PyTdDWave { + inner: wc::TdDWave, +} + +#[pymethods] +impl PyTdDWave { + #[new] + #[pyo3(signature = (strength=2))] + fn new(strength: usize) -> PyResult { + Ok(Self { + inner: wc::TdDWave::new(strength).map_err(map_err)?, + }) + } + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c)) + } + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + close: PyReadonlyArray1<'py, f64>, + ) -> PyResult>> { + 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 h.len() != l.len() || l.len() != c.len() { + return Err(PyValueError::new_err( + "high, low, close must be equal length", + )); + } + let mut out = Vec::with_capacity(h.len()); + for i in 0..h.len() { + let candle = wc::Candle::new(c[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() + } +} + +// ============================== TD Moving Averages ============================== + +#[pyclass( + name = "TDMovingAverage", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyTdMovingAverage { + inner: wc::TdMovingAverage, +} + +#[pymethods] +impl PyTdMovingAverage { + #[new] + #[pyo3(signature = (period_st1=5, period_st2=13))] + fn new(period_st1: usize, period_st2: usize) -> PyResult { + Ok(Self { + inner: wc::TdMovingAverage::new(period_st1, period_st2).map_err(map_err)?, + }) + } + /// Returns `(st1, st2)`. + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c).map(|o| (o.st1, o.st2))) + } + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + ) -> PyResult>> { + let h = high + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let l = low + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() { + return Err(PyValueError::new_err("high, low must be equal length")); + } + let n = h.len(); + let mut out = vec![f64::NAN; n * 2]; + for i in 0..n { + let candle = wc::Candle::new(l[i], h[i], l[i], l[i], 0.0, 0).map_err(map_err)?; + if let Some(o) = self.inner.update(candle) { + out[i * 2] = o.st1; + out[i * 2 + 1] = o.st2; + } + } + Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out) + .expect("shape consistent") + .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() + } +} + // ============================== TD Countdown ============================== #[pyclass(name = "TDCountdown", module = "wickra._wickra", skip_from_py_object)] @@ -17703,6 +17828,11 @@ candle_pattern_no_param!(PyCrab, wc::Crab, "Crab"); candle_pattern_no_param!(PyShark, wc::Shark, "Shark"); candle_pattern_no_param!(PyCypher, wc::Cypher, "Cypher"); candle_pattern_no_param!(PyThreeDrives, wc::ThreeDrives, "ThreeDrives"); +candle_pattern_no_param!(PyTdCamouflage, wc::TdCamouflage, "TDCamouflage"); +candle_pattern_no_param!(PyTdClop, wc::TdClop, "TDClop"); +candle_pattern_no_param!(PyTdClopwin, wc::TdClopwin, "TDClopwin"); +candle_pattern_no_param!(PyTdPropulsion, wc::TdPropulsion, "TDPropulsion"); +candle_pattern_no_param!(PyTdTrap, wc::TdTrap, "TDTrap"); // ============================== Microstructure: Order Book ============================== // // Order-book indicators consume a depth snapshot rather than OHLCV. Streaming @@ -24028,6 +24158,8 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; @@ -24336,5 +24468,10 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/bindings/python/tests/test_new_indicators.py b/bindings/python/tests/test_new_indicators.py index 4bfbef8b..ed32ba7c 100644 --- a/bindings/python/tests/test_new_indicators.py +++ b/bindings/python/tests/test_new_indicators.py @@ -382,6 +382,30 @@ def test_relative_strength_streaming_matches_batch(): # 6-tuple candle; the batch helper takes only the columns it needs. CANDLE_SCALAR = { + "TDDWave": ( + lambda: ta.TDDWave(2), + lambda ind, h, l, c, v: ind.batch(h, l, c), + ), + "TDTrap": ( + lambda: ta.TDTrap(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), + "TDPropulsion": ( + lambda: ta.TDPropulsion(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), + "TDClopwin": ( + lambda: ta.TDClopwin(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), + "TDClop": ( + lambda: ta.TDClop(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), + "TDCamouflage": ( + lambda: ta.TDCamouflage(), + lambda ind, h, l, c, v: ind.batch(c, h, l, c), + ), "PivotReversal": ( lambda: ta.PivotReversal(1, 1), lambda ind, h, l, c, v: ind.batch(h, l, c), @@ -952,6 +976,11 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv): # --- Candle-input, multi-output indicators -------------------------------- MULTI = { + "TDMovingAverage": ( + lambda: ta.TDMovingAverage(5, 13), + lambda ind, h, l, c, v: ind.batch(h, l), + 2, + ), "VolumeWeightedSr": ( lambda: ta.VolumeWeightedSr(3), lambda ind, h, l, c, v: ind.batch(h, l, v), @@ -3174,6 +3203,38 @@ def test_pivot_reversal_reference(): assert t.update((13.0, 14.0, 12.5, 13.0, 1.0, 4)) == pytest.approx(1.0) + +def test_td_camouflage_reference(): + t = ta.TDCamouflage() + assert t.update((10.0, 11.0, 8.0, 10.0, 1.0, 0)) == pytest.approx(0.0) + assert t.update((9.0, 10.0, 7.0, 9.5, 1.0, 1)) == pytest.approx(1.0) + + +def test_td_clop_reference(): + t = ta.TDClop() + assert t.update((10.0, 12.0, 9.0, 11.0, 1.0, 0)) == pytest.approx(0.0) + assert t.update((9.0, 13.0, 8.0, 12.0, 1.0, 1)) == pytest.approx(1.0) + + +def test_td_clopwin_reference(): + t = ta.TDClopwin() + assert t.update((10.0, 15.0, 9.0, 14.0, 1.0, 0)) == pytest.approx(0.0) + assert t.update((11.0, 14.0, 10.0, 13.0, 1.0, 1)) == pytest.approx(1.0) + + +def test_td_propulsion_reference(): + t = ta.TDPropulsion() + assert t.update((9.5, 11.0, 9.0, 10.0, 1.0, 0)) == pytest.approx(0.0) + assert t.update((10.5, 12.0, 10.0, 11.5, 1.0, 1)) == pytest.approx(1.0) + + +def test_td_trap_reference(): + t = ta.TDTrap() + assert t.update((100.0, 110.0, 90.0, 100.0, 1.0, 0)) == pytest.approx(0.0) + assert t.update((101.5, 108.0, 95.0, 102.0, 1.0, 1)) == pytest.approx(0.0) + assert t.update((106.0, 112.0, 100.0, 109.0, 1.0, 2)) == pytest.approx(1.0) + + # --- Lifecycle ------------------------------------------------------------ diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 15f260bf..fff3db38 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -7817,6 +7817,109 @@ impl WasmTdCombo { } } +// ---------- TD D-Wave ---------- + +#[wasm_bindgen(js_name = TDDWave)] +pub struct WasmTdDWave { + inner: wc::TdDWave, +} + +#[wasm_bindgen(js_class = TDDWave)] +impl WasmTdDWave { + #[wasm_bindgen(constructor)] + pub fn new(strength: usize) -> Result { + Ok(Self { + inner: wc::TdDWave::new(strength).map_err(map_err)?, + }) + } + pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result, JsError> { + let c = make_candle(high, low, close, 0.0)?; + Ok(self.inner.update(c)) + } + pub fn batch( + &mut self, + high: &[f64], + low: &[f64], + close: &[f64], + ) -> Result { + if high.len() != low.len() || low.len() != close.len() { + return Err(JsError::new("high, low, close must be equal length")); + } + let mut out = Vec::with_capacity(high.len()); + for i in 0..high.len() { + let c = make_candle(high[i], low[i], close[i], 0.0)?; + 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() + } +} + +// ---------- TD Moving Averages ---------- + +#[wasm_bindgen(js_name = TDMovingAverage)] +pub struct WasmTdMovingAverage { + inner: wc::TdMovingAverage, +} + +#[wasm_bindgen(js_class = TDMovingAverage)] +impl WasmTdMovingAverage { + #[wasm_bindgen(constructor)] + pub fn new(period_st1: usize, period_st2: usize) -> Result { + Ok(Self { + inner: wc::TdMovingAverage::new(period_st1, period_st2).map_err(map_err)?, + }) + } + pub fn update(&mut self, high: f64, low: f64) -> Result { + let candle = make_candle(high, low, low, 0.0)?; + match self.inner.update(candle) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"st1".into(), &o.st1.into()).ok(); + Reflect::set(&obj, &"st2".into(), &o.st2.into()).ok(); + Ok(obj.into()) + } + None => Ok(JsValue::NULL), + } + } + pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result { + if high.len() != low.len() { + return Err(JsError::new("high, low must be equal length")); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 2]; + for i in 0..n { + let candle = make_candle(high[i], low[i], low[i], 0.0)?; + if let Some(o) = self.inner.update(candle) { + out[i * 2] = o.st1; + out[i * 2 + 1] = o.st2; + } + } + 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() + } +} + // ---------- TD Countdown ---------- #[wasm_bindgen(js_name = TDCountdown)] @@ -8942,6 +9045,11 @@ wasm_candle_pattern!(WasmCrab, wc::Crab, Crab); wasm_candle_pattern!(WasmShark, wc::Shark, Shark); wasm_candle_pattern!(WasmCypher, wc::Cypher, Cypher); wasm_candle_pattern!(WasmThreeDrives, wc::ThreeDrives, ThreeDrives); +wasm_candle_pattern!(WasmTdCamouflage, wc::TdCamouflage, TDCamouflage); +wasm_candle_pattern!(WasmTdClop, wc::TdClop, TDClop); +wasm_candle_pattern!(WasmTdClopwin, wc::TdClopwin, TDClopwin); +wasm_candle_pattern!(WasmTdPropulsion, wc::TdPropulsion, TDPropulsion); +wasm_candle_pattern!(WasmTdTrap, wc::TdTrap, TDTrap); // ============================== Microstructure: Order Book ============================== // diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs index e729b365..4fce4bec 100644 --- a/crates/wickra-core/src/indicators/mod.rs +++ b/crates/wickra-core/src/indicators/mod.rs @@ -379,18 +379,25 @@ mod t3; mod taker_buy_sell_ratio; mod takuri; mod tasuki_gap; +mod td_camouflage; +mod td_clop; +mod td_clopwin; mod td_combo; mod td_countdown; mod td_demarker; mod td_differential; +mod td_dwave; mod td_lines; +mod td_moving_average; mod td_open; mod td_pressure; +mod td_propulsion; mod td_range_projection; mod td_rei; mod td_risk_level; mod td_sequential; mod td_setup; +mod td_trap; mod tema; mod term_structure_basis; mod three_drives; @@ -846,18 +853,25 @@ pub use t3::T3; pub use taker_buy_sell_ratio::TakerBuySellRatio; pub use takuri::Takuri; pub use tasuki_gap::TasukiGap; +pub use td_camouflage::TdCamouflage; +pub use td_clop::TdClop; +pub use td_clopwin::TdClopwin; pub use td_combo::TdCombo; pub use td_countdown::TdCountdown; pub use td_demarker::TdDeMarker; pub use td_differential::TdDifferential; +pub use td_dwave::TdDWave; pub use td_lines::{TdLines, TdLinesOutput}; +pub use td_moving_average::{TdMovingAverage, TdMovingAverageOutput}; pub use td_open::TdOpen; pub use td_pressure::TdPressure; +pub use td_propulsion::TdPropulsion; pub use td_range_projection::{TdRangeProjection, TdRangeProjectionOutput}; pub use td_rei::TdRei; pub use td_risk_level::{TdRiskLevel, TdRiskLevelOutput}; pub use td_sequential::{TdSequential, TdSequentialOutput}; pub use td_setup::TdSetup; +pub use td_trap::TdTrap; pub use tema::Tema; pub use term_structure_basis::TermStructureBasis; pub use three_drives::ThreeDrives; @@ -1304,6 +1318,13 @@ pub const FAMILIES: &[(&str, &[&str])] = &[ "TdDifferential", "TdOpen", "TdRiskLevel", + "TdCamouflage", + "TdClop", + "TdClopwin", + "TdPropulsion", + "TdTrap", + "TdDWave", + "TdMovingAverage", ], ), ("Ichimoku & Charts", &["Ichimoku", "HeikinAshi"]), @@ -1555,6 +1576,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, 467, "FAMILIES total drifted from indicator count"); + assert_eq!(total, 474, "FAMILIES total drifted from indicator count"); } } diff --git a/crates/wickra-core/src/indicators/td_camouflage.rs b/crates/wickra-core/src/indicators/td_camouflage.rs new file mode 100644 index 00000000..d4f11e2c --- /dev/null +++ b/crates/wickra-core/src/indicators/td_camouflage.rs @@ -0,0 +1,156 @@ +#![allow(clippy::doc_markdown)] + +//! Tom DeMark TD Camouflage — a hidden-strength/weakness 1-bar reversal pattern. +//! +//! TD Camouflage spots a bar that *looks* weak (or strong) on its close-to-close +//! comparison but reveals the opposite intrabar, "camouflaging" a reversal. +//! +//! - **Buy signal** (`+1.0`): `close < close[-1]` (a lower close, looks bearish), +//! yet `close > open` (it actually closed up on the bar) and `low < low[-1]` +//! (it dipped to a new low and was bought back) — hidden accumulation. +//! - **Sell signal** (`-1.0`): `close > close[-1]`, `close < open`, and +//! `high > high[-1]` — hidden distribution. +//! - Otherwise the output is `0.0`. +//! +//! The one-bar lookback means the first value lands on the second candle. + +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// TD Camouflage — 1-bar hidden-strength/weakness reversal detector. +#[derive(Debug, Clone, Default)] +pub struct TdCamouflage { + prev: Option, + last_value: Option, +} + +impl TdCamouflage { + /// Construct a new `TdCamouflage`. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Latest emitted signal if available. + pub const fn value(&self) -> Option { + self.last_value + } +} + +impl Indicator for TdCamouflage { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + let Some(prev) = self.prev else { + self.prev = Some(candle); + self.last_value = Some(0.0); + return Some(0.0); + }; + let v = if candle.close < prev.close && candle.close > candle.open && candle.low < prev.low + { + 1.0 + } else if candle.close > prev.close && candle.close < candle.open && candle.high > prev.high + { + -1.0 + } else { + 0.0 + }; + self.prev = Some(candle); + self.last_value = Some(v); + Some(v) + } + + fn reset(&mut self) { + self.prev = None; + self.last_value = None; + } + + fn warmup_period(&self) -> usize { + 2 + } + + fn is_ready(&self) -> bool { + self.last_value.is_some() + } + + fn name(&self) -> &'static str { + "TDCamouflage" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(open: f64, high: f64, low: f64, close: f64) -> Candle { + Candle::new_unchecked(open, high, low, close, 0.0, 0) + } + + #[test] + fn accessors_and_metadata() { + let td = TdCamouflage::new(); + assert_eq!(td.warmup_period(), 2); + assert_eq!(td.name(), "TDCamouflage"); + assert!(!td.is_ready()); + assert_eq!(td.value(), None); + } + + #[test] + fn first_bar_seeds_without_signal() { + let mut td = TdCamouflage::new(); + assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), Some(0.0)); + assert!(td.update(c(10.0, 11.0, 8.0, 9.5)).is_some()); + } + + #[test] + fn bullish_camouflage_buy() { + // prev close 10. Current: close 9.5 < 10 (lower close), close 9.5 > open 9.0, + // low 7.0 < prev low 8.0 -> buy. + let mut td = TdCamouflage::new(); + td.update(c(10.0, 11.0, 8.0, 10.0)); + assert_eq!(td.update(c(9.0, 10.0, 7.0, 9.5)), Some(1.0)); + } + + #[test] + fn bearish_camouflage_sell() { + // prev close 10. Current: close 10.5 > 10, close 10.5 < open 11.0, + // high 12.0 > prev high 11.0 -> sell. + let mut td = TdCamouflage::new(); + td.update(c(10.0, 11.0, 8.0, 10.0)); + assert_eq!(td.update(c(11.0, 12.0, 10.0, 10.5)), Some(-1.0)); + } + + #[test] + fn no_pattern_is_zero() { + let mut td = TdCamouflage::new(); + td.update(c(10.0, 11.0, 9.0, 10.0)); + assert_eq!(td.update(c(10.0, 11.5, 9.5, 11.0)), Some(0.0)); + } + + #[test] + fn reset_clears_state() { + let mut td = TdCamouflage::new(); + td.update(c(10.0, 11.0, 9.0, 10.0)); + td.update(c(9.0, 10.0, 7.0, 9.5)); + assert!(td.is_ready()); + td.reset(); + assert!(!td.is_ready()); + assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..40) + .map(|i| { + let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0; + c(b, b + 1.0, b - 1.0, b + 0.2) + }) + .collect(); + let batch = TdCamouflage::new().batch(&candles); + let mut b = TdCamouflage::new(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/td_clop.rs b/crates/wickra-core/src/indicators/td_clop.rs new file mode 100644 index 00000000..7e4eb648 --- /dev/null +++ b/crates/wickra-core/src/indicators/td_clop.rs @@ -0,0 +1,159 @@ +#![allow(clippy::doc_markdown)] + +//! Tom DeMark TD Clop — a 2-bar open/close engulfing reversal. +//! +//! TD Clop ("CLose/OPen") fires when the current bar's open opens beyond **both** +//! the prior bar's open and close, and its close finishes back beyond both — an +//! open-gap that fully reverses, signalling a turn. +//! +//! - **Buy signal** (`+1.0`): `open < open[-1]` AND `open < close[-1]` +//! (opens below the whole prior body) AND `close > open[-1]` AND +//! `close > close[-1]` (closes above it). +//! - **Sell signal** (`-1.0`): `open > open[-1]` AND `open > close[-1]` AND +//! `close < open[-1]` AND `close < close[-1]`. +//! - Otherwise the output is `0.0`. +//! +//! The one-bar lookback means the first value lands on the second candle. + +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// TD Clop — 2-bar open/close engulfing reversal detector. +#[derive(Debug, Clone, Default)] +pub struct TdClop { + prev: Option, + last_value: Option, +} + +impl TdClop { + /// Construct a new `TdClop`. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Latest emitted signal if available. + pub const fn value(&self) -> Option { + self.last_value + } +} + +impl Indicator for TdClop { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + let Some(prev) = self.prev else { + self.prev = Some(candle); + self.last_value = Some(0.0); + return Some(0.0); + }; + let below_body = candle.open < prev.open && candle.open < prev.close; + let above_body = candle.close > prev.open && candle.close > prev.close; + let over_body = candle.open > prev.open && candle.open > prev.close; + let under_body = candle.close < prev.open && candle.close < prev.close; + let v = if below_body && above_body { + 1.0 + } else if over_body && under_body { + -1.0 + } else { + 0.0 + }; + self.prev = Some(candle); + self.last_value = Some(v); + Some(v) + } + + fn reset(&mut self) { + self.prev = None; + self.last_value = None; + } + + fn warmup_period(&self) -> usize { + 2 + } + + fn is_ready(&self) -> bool { + self.last_value.is_some() + } + + fn name(&self) -> &'static str { + "TDClop" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(open: f64, close: f64) -> Candle { + let high = open.max(close) + 1.0; + let low = open.min(close) - 1.0; + Candle::new_unchecked(open, high, low, close, 0.0, 0) + } + + #[test] + fn accessors_and_metadata() { + let td = TdClop::new(); + assert_eq!(td.warmup_period(), 2); + assert_eq!(td.name(), "TDClop"); + assert!(!td.is_ready()); + assert_eq!(td.value(), None); + } + + #[test] + fn first_bar_seeds_without_signal() { + let mut td = TdClop::new(); + assert_eq!(td.update(c(10.0, 11.0)), Some(0.0)); + assert!(td.update(c(9.0, 12.0)).is_some()); + } + + #[test] + fn bullish_clop_buy() { + // prev body [10, 11]. Current open 9 < both, close 12 > both -> buy. + let mut td = TdClop::new(); + td.update(c(10.0, 11.0)); + assert_eq!(td.update(c(9.0, 12.0)), Some(1.0)); + } + + #[test] + fn bearish_clop_sell() { + // prev body [10, 11]. Current open 12 > both, close 9 < both -> sell. + let mut td = TdClop::new(); + td.update(c(10.0, 11.0)); + assert_eq!(td.update(c(12.0, 9.0)), Some(-1.0)); + } + + #[test] + fn no_pattern_is_zero() { + let mut td = TdClop::new(); + td.update(c(10.0, 11.0)); + assert_eq!(td.update(c(10.5, 11.5)), Some(0.0)); + } + + #[test] + fn reset_clears_state() { + let mut td = TdClop::new(); + td.update(c(10.0, 11.0)); + td.update(c(9.0, 12.0)); + assert!(td.is_ready()); + td.reset(); + assert!(!td.is_ready()); + assert_eq!(td.update(c(10.0, 11.0)), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..40) + .map(|i| { + let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0; + c(b, b + 0.5) + }) + .collect(); + let batch = TdClop::new().batch(&candles); + let mut b = TdClop::new(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/td_clopwin.rs b/crates/wickra-core/src/indicators/td_clopwin.rs new file mode 100644 index 00000000..d7def7bc --- /dev/null +++ b/crates/wickra-core/src/indicators/td_clopwin.rs @@ -0,0 +1,162 @@ +#![allow(clippy::doc_markdown)] + +//! Tom DeMark TD Clopwin — a 2-bar "close/open within" inside-body pattern. +//! +//! TD Clopwin ("CLose/OPen WInthIN") is the inside-body cousin of TD Clop: the +//! current bar's open **and** close both sit within the prior bar's real body, +//! marking a compression bar whose direction hints at the next move. +//! +//! - **Buy signal** (`+1.0`): current `open` and `close` are both inside the prior +//! bar's body `[min(open,close)[-1], max(open,close)[-1]]` AND `close >= open` +//! (a bullish inside bar). +//! - **Sell signal** (`-1.0`): both inside the prior body AND `close < open` +//! (a bearish inside bar). +//! - Otherwise the output is `0.0`. +//! +//! The one-bar lookback means the first value lands on the second candle. + +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// TD Clopwin — 2-bar inside-body compression pattern detector. +#[derive(Debug, Clone, Default)] +pub struct TdClopwin { + prev: Option, + last_value: Option, +} + +impl TdClopwin { + /// Construct a new `TdClopwin`. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Latest emitted signal if available. + pub const fn value(&self) -> Option { + self.last_value + } +} + +impl Indicator for TdClopwin { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + let Some(prev) = self.prev else { + self.prev = Some(candle); + self.last_value = Some(0.0); + return Some(0.0); + }; + let body_low = prev.open.min(prev.close); + let body_high = prev.open.max(prev.close); + let open_in = candle.open >= body_low && candle.open <= body_high; + let close_in = candle.close >= body_low && candle.close <= body_high; + let v = if open_in && close_in { + if candle.close >= candle.open { + 1.0 + } else { + -1.0 + } + } else { + 0.0 + }; + self.prev = Some(candle); + self.last_value = Some(v); + Some(v) + } + + fn reset(&mut self) { + self.prev = None; + self.last_value = None; + } + + fn warmup_period(&self) -> usize { + 2 + } + + fn is_ready(&self) -> bool { + self.last_value.is_some() + } + + fn name(&self) -> &'static str { + "TDClopwin" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(open: f64, close: f64) -> Candle { + let high = open.max(close) + 1.0; + let low = open.min(close) - 1.0; + Candle::new_unchecked(open, high, low, close, 0.0, 0) + } + + #[test] + fn accessors_and_metadata() { + let td = TdClopwin::new(); + assert_eq!(td.warmup_period(), 2); + assert_eq!(td.name(), "TDClopwin"); + assert!(!td.is_ready()); + assert_eq!(td.value(), None); + } + + #[test] + fn first_bar_seeds_without_signal() { + let mut td = TdClopwin::new(); + assert_eq!(td.update(c(10.0, 14.0)), Some(0.0)); + assert!(td.update(c(11.0, 13.0)).is_some()); + } + + #[test] + fn bullish_inside_body_buy() { + // prev body [10, 14]. Current open 11, close 13 both inside, close>open -> +1. + let mut td = TdClopwin::new(); + td.update(c(10.0, 14.0)); + assert_eq!(td.update(c(11.0, 13.0)), Some(1.0)); + } + + #[test] + fn bearish_inside_body_sell() { + // prev body [10, 14]. Current open 13, close 11 inside, close -1. + let mut td = TdClopwin::new(); + td.update(c(10.0, 14.0)); + assert_eq!(td.update(c(13.0, 11.0)), Some(-1.0)); + } + + #[test] + fn outside_body_is_zero() { + let mut td = TdClopwin::new(); + td.update(c(10.0, 14.0)); + // close 16 outside the prior body -> 0. + assert_eq!(td.update(c(11.0, 16.0)), Some(0.0)); + } + + #[test] + fn reset_clears_state() { + let mut td = TdClopwin::new(); + td.update(c(10.0, 14.0)); + td.update(c(11.0, 13.0)); + assert!(td.is_ready()); + td.reset(); + assert!(!td.is_ready()); + assert_eq!(td.update(c(10.0, 14.0)), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..40) + .map(|i| { + let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0; + c(b, b + 0.3) + }) + .collect(); + let batch = TdClopwin::new().batch(&candles); + let mut b = TdClopwin::new(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/td_dwave.rs b/crates/wickra-core/src/indicators/td_dwave.rs new file mode 100644 index 00000000..eee50efe --- /dev/null +++ b/crates/wickra-core/src/indicators/td_dwave.rs @@ -0,0 +1,295 @@ +#![allow(clippy::doc_markdown)] + +//! Tom DeMark TD D-Wave — a simplified Elliott-style swing-wave counter. + +use std::collections::VecDeque; + +use crate::error::{Error, Result}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Tom DeMark **TD D-Wave** — a streaming wave counter that labels the market's +/// swing sequence with an Elliott-style `1–5` impulse / `A–C` correction count. +/// +/// TD D-Wave is DeMark's objective alternative to discretionary Elliott Wave +/// counting. This streaming implementation detects alternating swing pivots with a +/// symmetric fractal of half-width `strength`, and advances a counter through the +/// eight-leg cycle each time a new swing leg is confirmed: +/// +/// ```text +/// legs: 1 → 2 → 3 → 4 → 5 → A(6) → B(7) → C(8) → 1 … +/// output = current wave number, 1.0..8.0 (6/7/8 = corrective A/B/C) +/// ``` +/// +/// The number tells you which wave of the cycle price is currently working on — a +/// running map of impulse versus correction that updates as each swing confirms. +/// This is a **simplified** swing-leg count (it does not enforce Elliott's price +/// ratio and overlap rules); treat it as a structural guide, not a strict wave +/// label. +/// +/// Readiness is data-dependent: the first value appears once the first swing pivot +/// confirms (`strength` bars after it forms). `warmup_period` returns the minimum +/// bars to confirm one pivot. Each `update` is O(`strength`). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{Candle, Indicator, TdDWave}; +/// +/// let mut indicator = TdDWave::new(2).unwrap(); +/// let mut last = None; +/// for i in 0..120 { +/// let base = 100.0 + (f64::from(i) * 0.5).sin() * 10.0; +/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap(); +/// last = indicator.update(c); +/// } +/// let _ = last; +/// ``` +#[derive(Debug, Clone)] +pub struct TdDWave { + strength: usize, + window: VecDeque, + last_is_high: Option, + last_extreme: f64, + wave: usize, + last_value: Option, +} + +impl TdDWave { + /// Construct a TD D-Wave with the given fractal `strength`. + /// + /// # Errors + /// + /// Returns [`Error::PeriodZero`] if `strength == 0`. + pub fn new(strength: usize) -> Result { + if strength == 0 { + return Err(Error::PeriodZero); + } + Ok(Self { + strength, + window: VecDeque::with_capacity(2 * strength + 1), + last_is_high: None, + last_extreme: 0.0, + wave: 0, + last_value: None, + }) + } + + /// Configured fractal strength. + pub const fn strength(&self) -> usize { + self.strength + } + + /// Current wave number if available. + pub const fn value(&self) -> Option { + self.last_value + } + + fn advance(&mut self, is_high: bool, price: f64) { + match self.last_is_high { + Some(prev) if prev == is_high => { + // Same-direction extreme: extend the current leg if more extreme. + let extends = if is_high { + price > self.last_extreme + } else { + price < self.last_extreme + }; + if extends { + self.last_extreme = price; + } + } + _ => { + // A new alternating leg: advance the wave counter (1..8 cycle). + self.wave = self.wave % 8 + 1; + self.last_is_high = Some(is_high); + self.last_extreme = price; + self.last_value = Some(self.wave as f64); + } + } + } +} + +impl Indicator for TdDWave { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + let span = 2 * self.strength + 1; + if self.window.len() == span { + self.window.pop_front(); + } + self.window.push_back(candle); + if self.window.len() == span { + let center = self.window[self.strength]; + let is_high = self + .window + .iter() + .enumerate() + .all(|(i, c)| i == self.strength || c.high < center.high); + let is_low = self + .window + .iter() + .enumerate() + .all(|(i, c)| i == self.strength || c.low > center.low); + if is_high && !is_low { + self.advance(true, center.high); + } else if is_low && !is_high { + self.advance(false, center.low); + } + } + self.last_value + } + + fn reset(&mut self) { + self.window.clear(); + self.last_is_high = None; + self.last_extreme = 0.0; + self.wave = 0; + self.last_value = None; + } + + fn warmup_period(&self) -> usize { + 2 * self.strength + 1 + } + + fn is_ready(&self) -> bool { + self.last_value.is_some() + } + + fn name(&self) -> &'static str { + "TDDWave" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(high: f64, low: f64) -> Candle { + Candle::new_unchecked( + f64::midpoint(high, low), + high, + low, + f64::midpoint(high, low), + 1_000.0, + 0, + ) + } + + fn zigzag() -> Vec { + (0..200) + .map(|i| { + let base = 100.0 + (f64::from(i) * 0.5).sin() * 10.0; + c(base + 1.0, base - 1.0) + }) + .collect() + } + + #[test] + fn rejects_zero_strength() { + assert!(matches!(TdDWave::new(0), Err(Error::PeriodZero))); + } + + #[test] + fn accessors_and_metadata() { + let td = TdDWave::new(2).unwrap(); + assert_eq!(td.strength(), 2); + assert_eq!(td.warmup_period(), 5); + assert_eq!(td.name(), "TDDWave"); + assert!(!td.is_ready()); + assert_eq!(td.value(), None); + } + + #[test] + fn counts_waves_on_swings() { + let mut td = TdDWave::new(2).unwrap(); + let out = td.batch(&zigzag()); + assert!(out.iter().any(Option::is_some)); + assert!(td.is_ready()); + } + + #[test] + fn same_direction_pivots_extend_one_leg() { + // Strictly decreasing lows mean no bar is ever a low pivot, so the + // confirmed pivots are all highs. Consecutive same-direction highs + // exercise the `extends` branch (true at 30 > 20, false at 25 < 30) + // without ever advancing the wave past leg 1. + let mut td = TdDWave::new(1).unwrap(); + let bars = [ + (10.0, 100.0), + (20.0, 99.0), + (12.0, 98.0), + (30.0, 97.0), + (15.0, 96.0), + (25.0, 95.0), + (14.0, 94.0), + (14.0, 93.0), + ]; + let vals: Vec = bars + .iter() + .filter_map(|&(high, low)| td.update(c(high, low))) + .collect(); + assert!(!vals.is_empty()); + assert!(vals.iter().all(|&v| v == 1.0)); + } + + #[test] + fn same_direction_low_pivots_extend_one_leg() { + // Mirror of the high-pivot case: strictly increasing highs mean no bar + // is ever a high pivot, so the confirmed pivots are all lows. The + // `extends` else-branch fires (true at 2 < 5, false at 4 > 2). + let mut td = TdDWave::new(1).unwrap(); + let bars = [ + (100.0, 10.0), + (101.0, 5.0), + (102.0, 8.0), + (103.0, 2.0), + (104.0, 6.0), + (105.0, 4.0), + (106.0, 7.0), + (107.0, 7.0), + ]; + let vals: Vec = bars + .iter() + .filter_map(|&(high, low)| td.update(c(high, low))) + .collect(); + assert!(!vals.is_empty()); + assert!(vals.iter().all(|&v| v == 1.0)); + } + + #[test] + fn wave_stays_in_one_to_eight() { + let mut td = TdDWave::new(2).unwrap(); + for v in td.batch(&zigzag()).into_iter().flatten() { + assert!((1.0..=8.0).contains(&v), "wave out of range: {v}"); + } + } + + #[test] + fn flat_input_never_counts() { + // A perfectly flat series has no distinct swing highs/lows. + let mut td = TdDWave::new(2).unwrap(); + let candles: Vec = (0..40).map(|_| c(100.0, 100.0)).collect(); + assert!(td.batch(&candles).iter().all(Option::is_none)); + } + + #[test] + fn reset_clears_state() { + let mut td = TdDWave::new(2).unwrap(); + td.batch(&zigzag()); + assert!(td.is_ready()); + td.reset(); + assert!(!td.is_ready()); + assert_eq!(td.value(), None); + } + + #[test] + fn batch_equals_streaming() { + let candles = zigzag(); + let batch = TdDWave::new(2).unwrap().batch(&candles); + let mut b = TdDWave::new(2).unwrap(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/td_moving_average.rs b/crates/wickra-core/src/indicators/td_moving_average.rs new file mode 100644 index 00000000..81e969f5 --- /dev/null +++ b/crates/wickra-core/src/indicators/td_moving_average.rs @@ -0,0 +1,229 @@ +#![allow(clippy::doc_markdown)] + +//! Tom DeMark TD Moving Averages — the ST1 (fast) and ST2 (slow) trend ribbon. + +use crate::error::{Error, Result}; +use crate::indicators::sma::Sma; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Output of [`TdMovingAverage`]: the fast (`st1`) and slow (`st2`) moving-average +/// lines. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TdMovingAverageOutput { + /// ST1 — the fast (short) moving average. + pub st1: f64, + /// ST2 — the slow (long) moving average. + pub st2: f64, +} + +/// Tom DeMark **TD Moving Averages** — a two-line trend ribbon (ST1 fast, ST2 +/// slow) computed on the median price, whose relationship defines the trend. +/// +/// ```text +/// price = (high + low) / 2 (median price) +/// st1 = SMA(price, period_st1) (fast / "Sequential Trend 1") +/// st2 = SMA(price, period_st2) (slow / "Sequential Trend 2") +/// ``` +/// +/// DeMark's moving-average pair frames the trend objectively: when `st1` is above +/// `st2` the trend is up, below it down, and the cross marks the change. Using the +/// **median price** rather than the close de-emphasises closing noise. This is a +/// streaming dual-SMA implementation of the ST1/ST2 ribbon; read the lines and +/// their crossover exactly as a fast/slow moving-average system. +/// +/// `period_st1` must be strictly smaller than `period_st2`. The first value lands +/// once the slow average is seeded (`period_st2` inputs). Each `update` is O(1). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{Candle, Indicator, TdMovingAverage}; +/// +/// let mut indicator = TdMovingAverage::new(5, 13).unwrap(); +/// let mut last = None; +/// for i in 0..40 { +/// let base = 100.0 + f64::from(i); +/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap(); +/// last = indicator.update(c); +/// } +/// assert!(last.is_some()); +/// ``` +#[derive(Debug, Clone)] +pub struct TdMovingAverage { + st1: Sma, + st2: Sma, + period_st1: usize, + period_st2: usize, + last: Option, +} + +impl TdMovingAverage { + /// Construct TD Moving Averages with the given fast and slow periods. + /// + /// # Errors + /// + /// Returns [`Error::PeriodZero`] if either period is `0`, and + /// [`Error::InvalidPeriod`] if `period_st1 >= period_st2`. + pub fn new(period_st1: usize, period_st2: usize) -> Result { + if period_st1 == 0 || period_st2 == 0 { + return Err(Error::PeriodZero); + } + if period_st1 >= period_st2 { + return Err(Error::InvalidPeriod { + message: "TD moving average ST1 period must be strictly less than ST2", + }); + } + Ok(Self { + st1: Sma::new(period_st1)?, + st2: Sma::new(period_st2)?, + period_st1, + period_st2, + last: None, + }) + } + + /// Configured `(period_st1, period_st2)`. + pub const fn periods(&self) -> (usize, usize) { + (self.period_st1, self.period_st2) + } + + /// Current value if available. + pub const fn value(&self) -> Option { + self.last + } +} + +impl Indicator for TdMovingAverage { + type Input = Candle; + type Output = TdMovingAverageOutput; + + fn update(&mut self, candle: Candle) -> Option { + let price = candle.median_price(); + let fast = self.st1.update(price); + let slow = self.st2.update(price); + if let (Some(st1), Some(st2)) = (fast, slow) { + let out = TdMovingAverageOutput { st1, st2 }; + self.last = Some(out); + return Some(out); + } + None + } + + fn reset(&mut self) { + self.st1.reset(); + self.st2.reset(); + self.last = None; + } + + fn warmup_period(&self) -> usize { + self.period_st2 + } + + fn is_ready(&self) -> bool { + self.last.is_some() + } + + fn name(&self) -> &'static str { + "TDMovingAverage" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + fn c(median: f64) -> Candle { + Candle::new_unchecked(median, median + 1.0, median - 1.0, median, 1_000.0, 0) + } + + #[test] + fn rejects_invalid_periods() { + assert!(matches!( + TdMovingAverage::new(0, 13), + Err(Error::PeriodZero) + )); + assert!(matches!( + TdMovingAverage::new(13, 5), + Err(Error::InvalidPeriod { .. }) + )); + assert!(matches!( + TdMovingAverage::new(5, 5), + Err(Error::InvalidPeriod { .. }) + )); + } + + #[test] + fn accessors_and_metadata() { + let td = TdMovingAverage::new(5, 13).unwrap(); + assert_eq!(td.periods(), (5, 13)); + assert_eq!(td.warmup_period(), 13); + assert_eq!(td.name(), "TDMovingAverage"); + assert!(!td.is_ready()); + assert_eq!(td.value(), None); + } + + #[test] + fn first_emission_at_warmup_period() { + let mut td = TdMovingAverage::new(2, 4).unwrap(); + let candles: Vec = (0..8).map(|i| c(100.0 + f64::from(i))).collect(); + let out = td.batch(&candles); + for v in out.iter().take(3) { + assert!(v.is_none()); + } + assert!(out[3].is_some()); + } + + #[test] + fn fast_leads_slow_in_uptrend() { + let mut td = TdMovingAverage::new(3, 7).unwrap(); + let candles: Vec = (0..40).map(|i| c(100.0 + f64::from(i))).collect(); + let out = td.batch(&candles).into_iter().flatten().last().unwrap(); + assert!(out.st1 > out.st2, "fast MA should lead in an uptrend"); + } + + #[test] + fn fast_below_slow_in_downtrend() { + let mut td = TdMovingAverage::new(3, 7).unwrap(); + let candles: Vec = (0..40).map(|i| c(200.0 - f64::from(i))).collect(); + let out = td.batch(&candles).into_iter().flatten().last().unwrap(); + assert!(out.st1 < out.st2, "fast MA should trail in a downtrend"); + } + + #[test] + fn flat_series_equal_lines() { + let mut td = TdMovingAverage::new(2, 4).unwrap(); + let out = td + .batch(&[c(50.0); 10]) + .into_iter() + .flatten() + .last() + .unwrap(); + assert_relative_eq!(out.st1, 50.0, epsilon = 1e-9); + assert_relative_eq!(out.st2, 50.0, epsilon = 1e-9); + } + + #[test] + fn reset_clears_state() { + let mut td = TdMovingAverage::new(2, 4).unwrap(); + td.batch(&(0..10).map(|i| c(100.0 + f64::from(i))).collect::>()); + assert!(td.is_ready()); + td.reset(); + assert!(!td.is_ready()); + assert_eq!(td.value(), None); + assert_eq!(td.update(c(100.0)), None); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..80) + .map(|i| c(100.0 + (f64::from(i) * 0.25).sin() * 9.0)) + .collect(); + let batch = TdMovingAverage::new(5, 13).unwrap().batch(&candles); + let mut b = TdMovingAverage::new(5, 13).unwrap(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/td_propulsion.rs b/crates/wickra-core/src/indicators/td_propulsion.rs new file mode 100644 index 00000000..189917e1 --- /dev/null +++ b/crates/wickra-core/src/indicators/td_propulsion.rs @@ -0,0 +1,152 @@ +#![allow(clippy::doc_markdown)] + +//! Tom DeMark TD Propulsion — a 2-bar trend-continuation thrust signal. +//! +//! TD Propulsion qualifies a continuation thrust: the bar opens on the trend side +//! of the prior close and then closes beyond the prior bar's extreme, "propelling" +//! the move forward. +//! +//! - **Propulsion up** (`+1.0`): `open >= close[-1]` (opens at or above the prior +//! close) AND `close > high[-1]` (closes above the prior high). +//! - **Propulsion down** (`-1.0`): `open <= close[-1]` AND `close < low[-1]`. +//! - Otherwise the output is `0.0`. +//! +//! The one-bar lookback means the first value lands on the second candle. + +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// TD Propulsion — 2-bar trend-continuation thrust detector. +#[derive(Debug, Clone, Default)] +pub struct TdPropulsion { + prev: Option, + last_value: Option, +} + +impl TdPropulsion { + /// Construct a new `TdPropulsion`. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Latest emitted signal if available. + pub const fn value(&self) -> Option { + self.last_value + } +} + +impl Indicator for TdPropulsion { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + let Some(prev) = self.prev else { + self.prev = Some(candle); + self.last_value = Some(0.0); + return Some(0.0); + }; + let v = if candle.open >= prev.close && candle.close > prev.high { + 1.0 + } else if candle.open <= prev.close && candle.close < prev.low { + -1.0 + } else { + 0.0 + }; + self.prev = Some(candle); + self.last_value = Some(v); + Some(v) + } + + fn reset(&mut self) { + self.prev = None; + self.last_value = None; + } + + fn warmup_period(&self) -> usize { + 2 + } + + fn is_ready(&self) -> bool { + self.last_value.is_some() + } + + fn name(&self) -> &'static str { + "TDPropulsion" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(open: f64, high: f64, low: f64, close: f64) -> Candle { + Candle::new_unchecked(open, high, low, close, 0.0, 0) + } + + #[test] + fn accessors_and_metadata() { + let td = TdPropulsion::new(); + assert_eq!(td.warmup_period(), 2); + assert_eq!(td.name(), "TDPropulsion"); + assert!(!td.is_ready()); + assert_eq!(td.value(), None); + } + + #[test] + fn first_bar_seeds_without_signal() { + let mut td = TdPropulsion::new(); + assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), Some(0.0)); + assert!(td.update(c(10.5, 12.0, 10.0, 11.5)).is_some()); + } + + #[test] + fn propulsion_up() { + // prev close 10, high 11. Current open 10.5 >= 10, close 11.5 > 11 -> +1. + let mut td = TdPropulsion::new(); + td.update(c(9.5, 11.0, 9.0, 10.0)); + assert_eq!(td.update(c(10.5, 12.0, 10.0, 11.5)), Some(1.0)); + } + + #[test] + fn propulsion_down() { + // prev close 10, low 9. Current open 9.5 <= 10, close 8.5 < 9 -> -1. + let mut td = TdPropulsion::new(); + td.update(c(10.5, 11.0, 9.0, 10.0)); + assert_eq!(td.update(c(9.5, 10.0, 8.0, 8.5)), Some(-1.0)); + } + + #[test] + fn no_thrust_is_zero() { + let mut td = TdPropulsion::new(); + td.update(c(9.5, 11.0, 9.0, 10.0)); + // close 10.5 not above prior high 11 -> 0. + assert_eq!(td.update(c(10.5, 10.8, 10.0, 10.5)), Some(0.0)); + } + + #[test] + fn reset_clears_state() { + let mut td = TdPropulsion::new(); + td.update(c(9.5, 11.0, 9.0, 10.0)); + td.update(c(10.5, 12.0, 10.0, 11.5)); + assert!(td.is_ready()); + td.reset(); + assert!(!td.is_ready()); + assert_eq!(td.update(c(9.5, 11.0, 9.0, 10.0)), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..40) + .map(|i| { + let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0; + c(b, b + 1.0, b - 1.0, b + 0.3) + }) + .collect(); + let batch = TdPropulsion::new().batch(&candles); + let mut b = TdPropulsion::new(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/td_trap.rs b/crates/wickra-core/src/indicators/td_trap.rs new file mode 100644 index 00000000..9a8188ec --- /dev/null +++ b/crates/wickra-core/src/indicators/td_trap.rs @@ -0,0 +1,171 @@ +#![allow(clippy::doc_markdown)] + +//! Tom DeMark TD Trap — an inside-bar ("trap") followed by a range breakout. +//! +//! A TD Trap forms when one bar is an **inside bar** (its high below and low above +//! the prior bar's), coiling the market; the next bar that closes beyond the trap +//! bar's high or low triggers the directional signal. +//! +//! - **Buy signal** (`+1.0`): the prior bar was an inside bar and the current +//! `close` is above that inside bar's `high`. +//! - **Sell signal** (`-1.0`): the prior bar was an inside bar and the current +//! `close` is below that inside bar's `low`. +//! - Otherwise the output is `0.0`. +//! +//! The two-bar lookback (one to set the inside bar, one before it) means the first +//! value lands on the third candle. + +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// TD Trap — inside-bar breakout signal detector. +#[derive(Debug, Clone, Default)] +pub struct TdTrap { + prev1: Option, + prev2: Option, + last_value: Option, +} + +impl TdTrap { + /// Construct a new `TdTrap`. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Latest emitted signal if available. + pub const fn value(&self) -> Option { + self.last_value + } +} + +impl Indicator for TdTrap { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + let (Some(trap), Some(before)) = (self.prev1, self.prev2) else { + // Not enough history yet: emit a neutral 0.0 while seeding. + self.prev2 = self.prev1; + self.prev1 = Some(candle); + self.last_value = Some(0.0); + return Some(0.0); + }; + let is_inside = trap.high < before.high && trap.low > before.low; + let v = if is_inside && candle.close > trap.high { + 1.0 + } else if is_inside && candle.close < trap.low { + -1.0 + } else { + 0.0 + }; + self.prev2 = self.prev1; + self.prev1 = Some(candle); + self.last_value = Some(v); + Some(v) + } + + fn reset(&mut self) { + self.prev1 = None; + self.prev2 = None; + self.last_value = None; + } + + fn warmup_period(&self) -> usize { + 3 + } + + fn is_ready(&self) -> bool { + self.last_value.is_some() + } + + fn name(&self) -> &'static str { + "TDTrap" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(high: f64, low: f64, close: f64) -> Candle { + Candle::new_unchecked(f64::midpoint(high, low), high, low, close, 0.0, 0) + } + + #[test] + fn accessors_and_metadata() { + let td = TdTrap::new(); + assert_eq!(td.warmup_period(), 3); + assert_eq!(td.name(), "TDTrap"); + assert!(!td.is_ready()); + assert_eq!(td.value(), None); + } + + #[test] + fn first_two_bars_seed_without_signal() { + let mut td = TdTrap::new(); + assert_eq!(td.update(c(110.0, 90.0, 100.0)), Some(0.0)); + assert_eq!(td.update(c(108.0, 95.0, 102.0)), Some(0.0)); + assert!(td.update(c(112.0, 100.0, 110.0)).is_some()); + } + + #[test] + fn inside_then_breakout_up_buys() { + // bar0 wide [90,110]; bar1 inside [95,108]; bar2 close 109 > 108 -> +1. + let mut td = TdTrap::new(); + td.update(c(110.0, 90.0, 100.0)); + td.update(c(108.0, 95.0, 102.0)); // inside bar (high<110, low>90) + assert_eq!(td.update(c(112.0, 100.0, 109.0)), Some(1.0)); + } + + #[test] + fn inside_then_breakdown_sells() { + let mut td = TdTrap::new(); + td.update(c(110.0, 90.0, 100.0)); + td.update(c(108.0, 95.0, 102.0)); // inside bar + assert_eq!(td.update(c(100.0, 92.0, 94.0)), Some(-1.0)); // close 94 < 95 + } + + #[test] + fn no_inside_bar_is_zero() { + let mut td = TdTrap::new(); + td.update(c(110.0, 90.0, 100.0)); + td.update(c(115.0, 85.0, 100.0)); // outside bar, not inside + assert_eq!(td.update(c(120.0, 110.0, 118.0)), Some(0.0)); + } + + #[test] + fn inside_but_no_breakout_is_zero() { + let mut td = TdTrap::new(); + td.update(c(110.0, 90.0, 100.0)); + td.update(c(108.0, 95.0, 102.0)); // inside bar + assert_eq!(td.update(c(107.0, 96.0, 103.0)), Some(0.0)); // close 103 within [95,108] + } + + #[test] + fn reset_clears_state() { + let mut td = TdTrap::new(); + td.update(c(110.0, 90.0, 100.0)); + td.update(c(108.0, 95.0, 102.0)); + td.update(c(112.0, 100.0, 109.0)); + assert!(td.is_ready()); + td.reset(); + assert!(!td.is_ready()); + assert_eq!(td.update(c(110.0, 90.0, 100.0)), Some(0.0)); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..40) + .map(|i| { + let b = 100.0 + (f64::from(i) * 0.4).sin() * 6.0; + c(b + 2.0, b - 2.0, b) + }) + .collect(); + let batch = TdTrap::new().batch(&candles); + let mut b = TdTrap::new(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/lib.rs b/crates/wickra-core/src/lib.rs index f1d1f6a3..f0e7975d 100644 --- a/crates/wickra-core/src/lib.rs +++ b/crates/wickra-core/src/lib.rs @@ -130,10 +130,11 @@ pub use indicators::{ SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi, Stochastic, StochasticCci, StochasticOutput, SuperSmoother, - SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown, - TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection, + SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCamouflage, TdClop, + TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker, TdDifferential, TdLines, TdLinesOutput, + TdMovingAverage, TdMovingAverageOutput, TdOpen, TdPressure, TdPropulsion, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential, - TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeDrives, ThreeInside, + TdSequentialOutput, TdSetup, TdTrap, Tema, TermStructureBasis, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TpoProfile, TpoProfileOutput, TradeImbalance, TradeVolumeIndex, TrendLabel, TrendStrengthIndex, Trendflex, diff --git a/docs/README.md b/docs/README.md index f60c2056..9174597d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ That includes: [Python](https://docs.wickra.org/Quickstart-Python), [Node](https://docs.wickra.org/Quickstart-Node), and [WASM](https://docs.wickra.org/Quickstart-WASM). -- A per-indicator deep dive for every one of the **467 indicators** across +- A per-indicator deep dive for every one of the **474 indicators** across the sixteen families (Moving Averages, Momentum Oscillators, Trend & Directional, Price Oscillators, Volatility & Bands, Bands & Channels, Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots & diff --git a/fuzz/fuzz_targets/indicator_update_candle.rs b/fuzz/fuzz_targets/indicator_update_candle.rs index ab45cc97..7922485b 100644 --- a/fuzz/fuzz_targets/indicator_update_candle.rs +++ b/fuzz/fuzz_targets/indicator_update_candle.rs @@ -22,7 +22,7 @@ //! WeightedClose. use libfuzzer_sys::fuzz_target; -use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, AdaptiveCci, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, AndrewsPitchfork, Aroon, AroonOscillator, Atr, AtrBands, AtrRatchet, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BetterVolume, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, Cci, CentralPivotRange, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, ElderRay, ElderSafeZone, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KaseDevStop, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, ModifiedMaStop, MorningDojiStar, MorningEveningStar, MurreyMathLines, Natr, Nrtr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PivotReversal, PlusDi, PlusDm, ProjectionBands, ProjectionOscillator, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeBasedStop, TimeOfDayReturnProfile, TpoProfile, TradeVolumeIndex, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VolatilityCone, VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeRsi, VolumeWeightedMacd, VolumeWeightedSr, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, Wad, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag}; +use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, AdaptiveCci, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, AndrewsPitchfork, Aroon, AroonOscillator, Atr, AtrBands, AtrRatchet, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BetterVolume, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, Cci, CentralPivotRange, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, ElderRay, ElderSafeZone, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KaseDevStop, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, ModifiedMaStop, MorningDojiStar, MorningEveningStar, MurreyMathLines, Natr, Nrtr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PivotReversal, PlusDi, PlusDm, ProjectionBands, ProjectionOscillator, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker, TdDifferential, TdLines, TdMovingAverage, TdOpen, TdPressure, TdPropulsion, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, TdTrap, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeBasedStop, TimeOfDayReturnProfile, TpoProfile, TradeVolumeIndex, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VolatilityCone, VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeRsi, VolumeWeightedMacd, VolumeWeightedSr, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, Wad, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag}; /// Convert a flat `f64` stream into a `Vec` by chunking it into /// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV @@ -315,6 +315,13 @@ fuzz_target!(|data: Vec| { } // --- Candlestick Patterns (family 14) --- + drive(TdTrap::new, &candles); + drive(TdPropulsion::new, &candles); + drive(TdClopwin::new, &candles); + drive(TdClop::new, &candles); + drive(TdCamouflage::new, &candles); + drive(|| TdDWave::new(2).unwrap(), &candles); + drive(|| TdMovingAverage::new(5, 13).unwrap(), &candles); drive(ConcealingBabySwallow::new, &candles); drive(UniqueThreeRiver::new, &candles); drive(TasukiGap::new, &candles);