diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9aa2520a..4c34a1dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,12 @@ 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]
+- **Tristar** — a three-doji star reversal: three consecutive dojis with the middle gapped above (bearish) or below (bullish) its neighbours (`Tristar`).
+- **Harami Cross** — a Harami whose second candle is a contained doji, a stronger reversal than a plain Harami (`HaramiCross`).
+- **Tower Top/Bottom** — a tall bar, a small pause bar, then a tall opposite bar marking a reversal (`TowerTopBottom`).
+- **Frying Pan Bottom** — a rounded (U-shaped) accumulation base over the lookback window, confirmed when price recovers above the rim (`FryPanBottom`).
+- **Dumpling Top** — a rounded (dome-shaped) distribution top over the lookback window, confirmed when price breaks below the start (`DumplingTop`).
+- **New Price Lines** — flags a run of N consecutive new closing highs (+1) or lows (-1), the eight/ten-new-price-lines exhaustion gauge (`NewPriceLines`).
## [0.6.8] - 2026-06-08
- **Smoothed Heikin-Ashi** — a Heikin-Ashi candle computed from EMA-smoothed OHLC, damping noise into a cleaner trend candle (`SmoothedHeikinAshi`).
diff --git a/README.md b/README.md
index 44c4405e..63bf61b2 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
[](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 479 indicators; start at the
+ every one of the 485 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.** 479 indicators across 24
+- **The biggest streaming-native catalogue, period.** 485 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 479 indicators**.
+ for all 485 indicators**.
- **Orders of magnitude faster where it counts.** In streaming Wickra is **11–56×**
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** | **479** | **yes** |
+| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **485** | **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
-479 streaming-first indicators across twenty-four families. Every one passes the
+485 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).
@@ -149,7 +149,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
| 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, Heikin-Ashi Oscillator, Three Line Break, Smoothed Heikin-Ashi, Equivolume, CandleVolume |
| 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 |
+| 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, Tristar, Harami Cross, Tower Top/Bottom, Dumpling Top, New Price Lines, Frying Pan Bottom |
| Chart Patterns | Double Top / Bottom, Triple Top / Bottom, Head and Shoulders, Triangle (asc/desc/sym), Wedge (rising/falling), Flag / Pennant, Rectangle / Range, Cup and Handle |
| Harmonic Patterns | AB=CD, Gartley, Butterfly, Bat, Crab, Shark, Cypher, Three Drives |
| Fibonacci | Fibonacci Retracement, Fibonacci Extension, Fibonacci Projection, Auto-Fibonacci, Golden Pocket, Fibonacci Confluence, Fibonacci Fan, Fibonacci Arcs, Fibonacci Channel, Fibonacci Time Zones |
@@ -237,7 +237,7 @@ A Python live-trading example using the public `websockets` package lives at
```
wickra/
├── crates/
-│ ├── wickra-core/ core engine + all 479 indicators
+│ ├── wickra-core/ core engine + all 485 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 1d849200..2a0e8013 100644
--- a/bindings/node/__tests__/indicators.test.js
+++ b/bindings/node/__tests__/indicators.test.js
@@ -386,6 +386,12 @@ const candleScalar = {
TDDWave: { make: () => new wickra.TDDWave(2), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
HeikinAshiOscillator: { make: () => new wickra.HeikinAshiOscillator(5), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
ThreeLineBreak: { make: () => new wickra.ThreeLineBreak(3), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
+ Tristar: { make: () => new wickra.Tristar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ HaramiCross: { make: () => new wickra.HaramiCross(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ TowerTopBottom: { make: () => new wickra.TowerTopBottom(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
+ DumplingTop: { make: () => new wickra.DumplingTop(9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
+ NewPriceLines: { make: () => new wickra.NewPriceLines(5), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
+ FryPanBottom: { make: () => new wickra.FryPanBottom(9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
};
for (const [name, d] of Object.entries(candleScalar)) {
diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts
index 067710dd..8b8c76d5 100644
--- a/bindings/node/index.d.ts
+++ b/bindings/node/index.d.ts
@@ -3421,6 +3421,33 @@ export declare class CandleVolume {
isReady(): boolean
warmupPeriod(): number
}
+export type FryPanBottomNode = FryPanBottom
+export declare class FryPanBottom {
+ constructor(period: number)
+ update(high: number, low: number, close: number): number | null
+ batch(high: Array, low: Array, close: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
+export type DumplingTopNode = DumplingTop
+export declare class DumplingTop {
+ constructor(period: number)
+ update(high: number, low: number, close: number): number | null
+ batch(high: Array, low: Array, close: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
+export type NewPriceLinesNode = NewPriceLines
+export declare class NewPriceLines {
+ constructor(count: number)
+ update(high: number, low: number, close: number): number | null
+ batch(high: Array, low: Array, close: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
export type ValueAreaNode = ValueArea
export declare class ValueArea {
constructor(period: number, binCount: number, valueAreaPct: number)
@@ -4198,6 +4225,33 @@ export declare class TDTrap {
isReady(): boolean
warmupPeriod(): number
}
+export type TristarNode = Tristar
+export declare class Tristar {
+ constructor()
+ update(open: number, high: number, low: number, close: number): number | null
+ batch(open: Array, high: Array, low: Array, close: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
+export type HaramiCrossNode = HaramiCross
+export declare class HaramiCross {
+ constructor()
+ update(open: number, high: number, low: number, close: number): number | null
+ batch(open: Array, high: Array, low: Array, close: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
+export type TowerTopBottomNode = TowerTopBottom
+export declare class TowerTopBottom {
+ constructor()
+ update(open: number, high: number, low: number, close: number): number | null
+ batch(open: Array, 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 be31164a..843fab63 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, TDDWave, TDMovingAverage, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, HeikinAshiOscillator, ThreeLineBreak, SmoothedHeikinAshi, Equivolume, CandleVolume, 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
+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, HeikinAshiOscillator, ThreeLineBreak, SmoothedHeikinAshi, Equivolume, CandleVolume, FryPanBottom, DumplingTop, NewPriceLines, 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, Tristar, HaramiCross, TowerTopBottom, 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
@@ -613,6 +613,9 @@ module.exports.ThreeLineBreak = ThreeLineBreak
module.exports.SmoothedHeikinAshi = SmoothedHeikinAshi
module.exports.Equivolume = Equivolume
module.exports.CandleVolume = CandleVolume
+module.exports.FryPanBottom = FryPanBottom
+module.exports.DumplingTop = DumplingTop
+module.exports.NewPriceLines = NewPriceLines
module.exports.ValueArea = ValueArea
module.exports.VolumeProfile = VolumeProfile
module.exports.TpoProfile = TpoProfile
@@ -699,6 +702,9 @@ module.exports.TDClop = TDClop
module.exports.TDClopwin = TDClopwin
module.exports.TDPropulsion = TDPropulsion
module.exports.TDTrap = TDTrap
+module.exports.Tristar = Tristar
+module.exports.HaramiCross = HaramiCross
+module.exports.TowerTopBottom = TowerTopBottom
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 26e01cc7..e2807200 100644
--- a/bindings/node/src/lib.rs
+++ b/bindings/node/src/lib.rs
@@ -12618,6 +12618,171 @@ impl CandleVolumeNode {
}
}
+// ============================== Frying Pan Bottom ==============================
+
+#[napi(js_name = "FryPanBottom")]
+pub struct FryPanBottomNode {
+ inner: wc::FryPanBottom,
+}
+
+#[napi]
+impl FryPanBottomNode {
+ #[napi(constructor)]
+ pub fn new(period: u32) -> napi::Result {
+ Ok(Self {
+ inner: wc::FryPanBottom::new(period as usize).map_err(map_err)?,
+ })
+ }
+ #[napi]
+ pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result> {
+ 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
+ }
+}
+
+// ============================== Dumpling Top ==============================
+
+#[napi(js_name = "DumplingTop")]
+pub struct DumplingTopNode {
+ inner: wc::DumplingTop,
+}
+
+#[napi]
+impl DumplingTopNode {
+ #[napi(constructor)]
+ pub fn new(period: u32) -> napi::Result {
+ Ok(Self {
+ inner: wc::DumplingTop::new(period as usize).map_err(map_err)?,
+ })
+ }
+ #[napi]
+ pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result> {
+ 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
+ }
+}
+
+// ============================== New Price Lines ==============================
+
+#[napi(js_name = "NewPriceLines")]
+pub struct NewPriceLinesNode {
+ inner: wc::NewPriceLines,
+}
+
+#[napi]
+impl NewPriceLinesNode {
+ #[napi(constructor)]
+ pub fn new(count: u32) -> napi::Result {
+ Ok(Self {
+ inner: wc::NewPriceLines::new(count as usize).map_err(map_err)?,
+ })
+ }
+ #[napi]
+ pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result> {
+ 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
+ }
+}
+
// ============================== ValueArea ==============================
#[napi(object)]
@@ -13271,6 +13436,9 @@ node_candle_pattern!(TdClopNode, wc::TdClop, "TDClop");
node_candle_pattern!(TdClopwinNode, wc::TdClopwin, "TDClopwin");
node_candle_pattern!(TdPropulsionNode, wc::TdPropulsion, "TDPropulsion");
node_candle_pattern!(TdTrapNode, wc::TdTrap, "TDTrap");
+node_candle_pattern!(TristarNode, wc::Tristar, "Tristar");
+node_candle_pattern!(HaramiCrossNode, wc::HaramiCross, "HaramiCross");
+node_candle_pattern!(TowerTopBottomNode, wc::TowerTopBottom, "TowerTopBottom");
// ============================== Microstructure: Order Book ==============================
//
diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py
index 2b0fc32b..8d293412 100644
--- a/bindings/python/python/wickra/__init__.py
+++ b/bindings/python/python/wickra/__init__.py
@@ -360,6 +360,12 @@ from ._wickra import (
KagiBars,
PointAndFigureBars,
# Candlestick patterns
+ TowerTopBottom,
+ HaramiCross,
+ Tristar,
+ FryPanBottom,
+ DumplingTop,
+ NewPriceLines,
Doji,
Hammer,
InvertedHammer,
@@ -869,6 +875,12 @@ __all__ = [
"KagiBars",
"PointAndFigureBars",
# Candlestick patterns
+ "TowerTopBottom",
+ "HaramiCross",
+ "Tristar",
+ "FryPanBottom",
+ "DumplingTop",
+ "NewPriceLines",
"Doji",
"Hammer",
"InvertedHammer",
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index 0fcae48e..e13b7e31 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -15705,6 +15705,186 @@ impl PyCandleVolume {
}
}
+// ============================== Frying Pan Bottom ==============================
+
+#[pyclass(name = "FryPanBottom", module = "wickra._wickra", skip_from_py_object)]
+#[derive(Clone)]
+struct PyFryPanBottom {
+ inner: wc::FryPanBottom,
+}
+
+#[pymethods]
+impl PyFryPanBottom {
+ #[new]
+ #[pyo3(signature = (period=9))]
+ fn new(period: usize) -> PyResult {
+ Ok(Self {
+ inner: wc::FryPanBottom::new(period).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()
+ }
+}
+
+// ============================== Dumpling Top ==============================
+
+#[pyclass(name = "DumplingTop", module = "wickra._wickra", skip_from_py_object)]
+#[derive(Clone)]
+struct PyDumplingTop {
+ inner: wc::DumplingTop,
+}
+
+#[pymethods]
+impl PyDumplingTop {
+ #[new]
+ #[pyo3(signature = (period=9))]
+ fn new(period: usize) -> PyResult {
+ Ok(Self {
+ inner: wc::DumplingTop::new(period).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()
+ }
+}
+
+// ============================== New Price Lines ==============================
+
+#[pyclass(name = "NewPriceLines", module = "wickra._wickra", skip_from_py_object)]
+#[derive(Clone)]
+struct PyNewPriceLines {
+ inner: wc::NewPriceLines,
+}
+
+#[pymethods]
+impl PyNewPriceLines {
+ #[new]
+ #[pyo3(signature = (count=5))]
+ fn new(count: usize) -> PyResult {
+ Ok(Self {
+ inner: wc::NewPriceLines::new(count).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()
+ }
+}
+
// ============================== CoefficientOfVariation ==============================
#[pyclass(
@@ -18181,6 +18361,9 @@ 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");
+candle_pattern_no_param!(PyTristar, wc::Tristar, "Tristar");
+candle_pattern_no_param!(PyHaramiCross, wc::HaramiCross, "HaramiCross");
+candle_pattern_no_param!(PyTowerTopBottom, wc::TowerTopBottom, "TowerTopBottom");
// ============================== Microstructure: Order Book ==============================
//
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
@@ -24826,5 +25009,11 @@ 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::()?;
+ m.add_class::()?;
Ok(())
}
diff --git a/bindings/python/tests/test_new_indicators.py b/bindings/python/tests/test_new_indicators.py
index 737d1f4c..eb33f0e7 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 = {
+ "FryPanBottom": (
+ lambda: ta.FryPanBottom(9),
+ lambda ind, h, l, c, v: ind.batch(h, l, c),
+ ),
+ "NewPriceLines": (
+ lambda: ta.NewPriceLines(5),
+ lambda ind, h, l, c, v: ind.batch(h, l, c),
+ ),
+ "DumplingTop": (
+ lambda: ta.DumplingTop(9),
+ lambda ind, h, l, c, v: ind.batch(h, l, c),
+ ),
+ "TowerTopBottom": (
+ lambda: ta.TowerTopBottom(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "HaramiCross": (
+ lambda: ta.HaramiCross(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
+ "Tristar": (
+ lambda: ta.Tristar(),
+ lambda ind, h, l, c, v: ind.batch(c, h, l, c),
+ ),
"ThreeLineBreak": (
lambda: ta.ThreeLineBreak(3),
lambda ind, h, l, c, v: ind.batch(h, l, c),
@@ -3278,6 +3302,27 @@ def test_equivolume_reference():
def test_candle_volume_reference():
t = ta.CandleVolume(20)
+
+def test_tristar_reference():
+ t = ta.Tristar()
+ assert t.update((100.0, 101.0, 99.0, 100.02, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((105.0, 106.0, 104.0, 105.02, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((100.0, 101.0, 99.0, 100.02, 1.0, 2)) == pytest.approx(-1.0)
+
+
+def test_harami_cross_reference():
+ t = ta.HaramiCross()
+ assert t.update((110.0, 110.2, 99.8, 100.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((105.0, 106.0, 104.0, 105.02, 1.0, 1)) == pytest.approx(1.0)
+
+
+def test_tower_top_bottom_reference():
+ t = ta.TowerTopBottom()
+ assert t.update((100.0, 110.1, 99.9, 110.0, 1.0, 0)) == pytest.approx(0.0)
+ assert t.update((105.0, 107.0, 103.0, 105.1, 1.0, 1)) == pytest.approx(0.0)
+ assert t.update((110.0, 110.1, 99.9, 100.0, 1.0, 2)) == pytest.approx(-1.0)
+
+
# --- Lifecycle ------------------------------------------------------------
diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs
index 0aa48e61..af001d8a 100644
--- a/bindings/wasm/src/lib.rs
+++ b/bindings/wasm/src/lib.rs
@@ -9050,6 +9050,9 @@ 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);
+wasm_candle_pattern!(WasmTristar, wc::Tristar, Tristar);
+wasm_candle_pattern!(WasmHaramiCross, wc::HaramiCross, HaramiCross);
+wasm_candle_pattern!(WasmTowerTopBottom, wc::TowerTopBottom, TowerTopBottom);
// ============================== Microstructure: Order Book ==============================
//
@@ -10488,6 +10491,150 @@ impl WasmCandleVolume {
}
}
+// ---------- Frying Pan Bottom ----------
+
+#[wasm_bindgen(js_name = FryPanBottom)]
+pub struct WasmFryPanBottom {
+ inner: wc::FryPanBottom,
+}
+
+#[wasm_bindgen(js_class = FryPanBottom)]
+impl WasmFryPanBottom {
+ #[wasm_bindgen(constructor)]
+ pub fn new(period: usize) -> Result {
+ Ok(Self {
+ inner: wc::FryPanBottom::new(period).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()
+ }
+}
+
+// ---------- Dumpling Top ----------
+
+#[wasm_bindgen(js_name = DumplingTop)]
+pub struct WasmDumplingTop {
+ inner: wc::DumplingTop,
+}
+
+#[wasm_bindgen(js_class = DumplingTop)]
+impl WasmDumplingTop {
+ #[wasm_bindgen(constructor)]
+ pub fn new(period: usize) -> Result {
+ Ok(Self {
+ inner: wc::DumplingTop::new(period).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()
+ }
+}
+
+// ---------- New Price Lines ----------
+
+#[wasm_bindgen(js_name = NewPriceLines)]
+pub struct WasmNewPriceLines {
+ inner: wc::NewPriceLines,
+}
+
+#[wasm_bindgen(js_class = NewPriceLines)]
+impl WasmNewPriceLines {
+ #[wasm_bindgen(constructor)]
+ pub fn new(count: usize) -> Result {
+ Ok(Self {
+ inner: wc::NewPriceLines::new(count).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()
+ }
+}
+
// ---------- Market Breadth (CrossSection input) ----------
//
// A breadth tick is the per-symbol state of the whole universe, passed as four
diff --git a/crates/wickra-core/src/indicators/dumpling_top.rs b/crates/wickra-core/src/indicators/dumpling_top.rs
new file mode 100644
index 00000000..a3bd7153
--- /dev/null
+++ b/crates/wickra-core/src/indicators/dumpling_top.rs
@@ -0,0 +1,215 @@
+//! Dumpling Top — a rounded top (dome) confirmed by a breakdown.
+
+use std::collections::VecDeque;
+
+use crate::error::{Error, Result};
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Dumpling Top — the bearish mirror of the [`FryPanBottom`](crate::FryPanBottom):
+/// a gently rounded **top** (dome) across the window, confirmed by a close back
+/// below where it started.
+///
+/// ```text
+/// over the last `period` closes:
+/// the maximum close sits in the middle third of the window (the "dome")
+/// the latest close is below the first close (the breakdown)
+/// signal = −1 when both hold, else 0
+/// ```
+///
+/// The dumpling top is a distribution pattern: price rounds over at the top as
+/// buying fades, then rolls down through the level it rose from. Detection requires
+/// a *central* high (a symmetric dome, not a one-sided spike) and a close below the
+/// window's opening level. The output is `−1.0` (pattern) or `0.0`.
+///
+/// The first value lands after `period` inputs; each `update` scans the window in
+/// O(`period`).
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, Indicator, DumplingTop};
+///
+/// let mut indicator = DumplingTop::new(9).unwrap();
+/// let closes = [100.0, 102.0, 104.0, 105.0, 104.0, 102.0, 99.0, 97.0, 95.0];
+/// let mut last = None;
+/// for &cl in &closes {
+/// let c = Candle::new(cl, cl + 0.5, cl - 0.5, cl, 1_000.0, 0).unwrap();
+/// last = indicator.update(c);
+/// }
+/// assert_eq!(last, Some(-1.0));
+/// ```
+#[derive(Debug, Clone)]
+pub struct DumplingTop {
+ period: usize,
+ closes: VecDeque,
+ last: Option,
+}
+
+impl DumplingTop {
+ /// Construct a Dumpling Top over `period` bars.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`Error::InvalidPeriod`] if `period < 5`.
+ pub fn new(period: usize) -> Result {
+ if period < 5 {
+ return Err(Error::InvalidPeriod {
+ message: "dumpling top needs period >= 5",
+ });
+ }
+ Ok(Self {
+ period,
+ closes: VecDeque::with_capacity(period),
+ last: None,
+ })
+ }
+
+ /// Configured window period.
+ pub const fn period(&self) -> usize {
+ self.period
+ }
+
+ /// Current value if available.
+ pub const fn value(&self) -> Option {
+ self.last
+ }
+}
+
+impl Indicator for DumplingTop {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ if self.closes.len() == self.period {
+ self.closes.pop_front();
+ }
+ self.closes.push_back(candle.close);
+ if self.closes.len() < self.period {
+ return None;
+ }
+ let first = *self.closes.front().expect("non-empty");
+ let last = *self.closes.back().expect("non-empty");
+ let mut max_idx = 0;
+ let mut max_val = f64::NEG_INFINITY;
+ for (i, &v) in self.closes.iter().enumerate() {
+ if v > max_val {
+ max_val = v;
+ max_idx = i;
+ }
+ }
+ let lo = self.period / 4;
+ let hi = self.period - self.period / 4;
+ let dome = max_idx >= lo && max_idx < hi;
+ let broke_down = last < first && last < max_val;
+ let v = if dome && broke_down { -1.0 } else { 0.0 };
+ self.last = Some(v);
+ Some(v)
+ }
+
+ fn reset(&mut self) {
+ self.closes.clear();
+ self.last = None;
+ }
+
+ fn warmup_period(&self) -> usize {
+ self.period
+ }
+
+ fn is_ready(&self) -> bool {
+ self.last.is_some()
+ }
+
+ fn name(&self) -> &'static str {
+ "DumplingTop"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+
+ fn c(close: f64) -> Candle {
+ Candle::new_unchecked(close, close + 0.5, close - 0.5, close, 1_000.0, 0)
+ }
+
+ #[test]
+ fn rejects_small_period() {
+ assert!(matches!(
+ DumplingTop::new(4),
+ Err(Error::InvalidPeriod { .. })
+ ));
+ assert!(DumplingTop::new(5).is_ok());
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let d = DumplingTop::new(9).unwrap();
+ assert_eq!(d.period(), 9);
+ assert_eq!(d.warmup_period(), 9);
+ assert_eq!(d.name(), "DumplingTop");
+ assert!(!d.is_ready());
+ assert_eq!(d.value(), None);
+ }
+
+ #[test]
+ fn first_emission_at_warmup_period() {
+ let mut d = DumplingTop::new(5).unwrap();
+ let out = d.batch(&[c(100.0), c(101.0), c(102.0), c(101.0), c(99.0), c(98.0)]);
+ for v in out.iter().take(4) {
+ assert!(v.is_none());
+ }
+ assert!(out[4].is_some());
+ }
+
+ #[test]
+ fn rounded_top_then_breakdown_signals() {
+ let mut d = DumplingTop::new(9).unwrap();
+ let closes = [100.0, 102.0, 104.0, 105.0, 104.0, 102.0, 99.0, 97.0, 95.0];
+ let candles: Vec = closes.iter().map(|&x| c(x)).collect();
+ let last = d.batch(&candles).into_iter().flatten().last().unwrap();
+ assert_eq!(last, -1.0);
+ }
+
+ #[test]
+ fn one_sided_rise_is_zero() {
+ let mut d = DumplingTop::new(9).unwrap();
+ let candles: Vec = (0..9).map(|i| c(100.0 + f64::from(i))).collect();
+ let last = d.batch(&candles).into_iter().flatten().last().unwrap();
+ assert_eq!(last, 0.0);
+ }
+
+ #[test]
+ fn no_breakdown_is_zero() {
+ let mut d = DumplingTop::new(9).unwrap();
+ let closes = [
+ 100.0, 102.0, 104.0, 105.0, 104.0, 103.0, 102.0, 101.0, 100.5,
+ ];
+ let candles: Vec = closes.iter().map(|&x| c(x)).collect();
+ let last = d.batch(&candles).into_iter().flatten().last().unwrap();
+ assert_eq!(last, 0.0);
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut d = DumplingTop::new(5).unwrap();
+ d.batch(&[c(100.0), c(101.0), c(102.0), c(101.0), c(99.0)]);
+ assert!(d.is_ready());
+ d.reset();
+ assert!(!d.is_ready());
+ assert_eq!(d.value(), None);
+ assert_eq!(d.update(c(100.0)), None);
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..60)
+ .map(|i| c(100.0 + (f64::from(i) * 0.3).sin() * 5.0))
+ .collect();
+ let batch = DumplingTop::new(9).unwrap().batch(&candles);
+ let mut b = DumplingTop::new(9).unwrap();
+ let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
+ assert_eq!(batch, streamed);
+ }
+}
diff --git a/crates/wickra-core/src/indicators/fry_pan_bottom.rs b/crates/wickra-core/src/indicators/fry_pan_bottom.rs
new file mode 100644
index 00000000..17446d09
--- /dev/null
+++ b/crates/wickra-core/src/indicators/fry_pan_bottom.rs
@@ -0,0 +1,218 @@
+//! Frying Pan Bottom — a rounded bottom (U) confirmed by recovery.
+
+use std::collections::VecDeque;
+
+use crate::error::{Error, Result};
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Frying Pan Bottom — a gently rounded bottom across the lookback window: prices
+/// decline, flatten near the centre, then recover above where they started.
+///
+/// ```text
+/// over the last `period` closes:
+/// the minimum close sits in the middle third of the window (the "bowl")
+/// the latest close is above the first close (the rim is recovered)
+/// signal = +1 when both hold, else 0
+/// ```
+///
+/// The frying pan is a bullish accumulation pattern: a saucer-shaped base where
+/// selling dries up, the curve flattens, and price lifts off the rim. Detecting it
+/// requires the low point to be central (a symmetric bowl, not a one-sided drop)
+/// and the close to have climbed back above the window's opening level, confirming
+/// the breakout from the base. The output is `+1.0` (pattern) or `0.0`.
+///
+/// The first value lands after `period` inputs; each `update` scans the window in
+/// O(`period`).
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, Indicator, FryPanBottom};
+///
+/// let mut indicator = FryPanBottom::new(9).unwrap();
+/// // A U-shaped base then recovery.
+/// let closes = [100.0, 98.0, 96.0, 95.0, 96.0, 98.0, 101.0, 103.0, 105.0];
+/// let mut last = None;
+/// for &cl in &closes {
+/// let c = Candle::new(cl, cl + 0.5, cl - 0.5, cl, 1_000.0, 0).unwrap();
+/// last = indicator.update(c);
+/// }
+/// assert_eq!(last, Some(1.0));
+/// ```
+#[derive(Debug, Clone)]
+pub struct FryPanBottom {
+ period: usize,
+ closes: VecDeque,
+ last: Option,
+}
+
+impl FryPanBottom {
+ /// Construct a Frying Pan Bottom over `period` bars.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`Error::InvalidPeriod`] if `period < 5` (a bowl needs room for a
+ /// central low between recovering sides).
+ pub fn new(period: usize) -> Result {
+ if period < 5 {
+ return Err(Error::InvalidPeriod {
+ message: "frying pan bottom needs period >= 5",
+ });
+ }
+ Ok(Self {
+ period,
+ closes: VecDeque::with_capacity(period),
+ last: None,
+ })
+ }
+
+ /// Configured window period.
+ pub const fn period(&self) -> usize {
+ self.period
+ }
+
+ /// Current value if available.
+ pub const fn value(&self) -> Option {
+ self.last
+ }
+}
+
+impl Indicator for FryPanBottom {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ if self.closes.len() == self.period {
+ self.closes.pop_front();
+ }
+ self.closes.push_back(candle.close);
+ if self.closes.len() < self.period {
+ return None;
+ }
+ let first = *self.closes.front().expect("non-empty");
+ let last = *self.closes.back().expect("non-empty");
+ // Index of the minimum close.
+ let mut min_idx = 0;
+ let mut min_val = f64::INFINITY;
+ for (i, &v) in self.closes.iter().enumerate() {
+ if v < min_val {
+ min_val = v;
+ min_idx = i;
+ }
+ }
+ let lo = self.period / 4;
+ let hi = self.period - self.period / 4;
+ let bowl = min_idx >= lo && min_idx < hi;
+ let recovered = last > first && last > min_val;
+ let v = if bowl && recovered { 1.0 } else { 0.0 };
+ self.last = Some(v);
+ Some(v)
+ }
+
+ fn reset(&mut self) {
+ self.closes.clear();
+ self.last = None;
+ }
+
+ fn warmup_period(&self) -> usize {
+ self.period
+ }
+
+ fn is_ready(&self) -> bool {
+ self.last.is_some()
+ }
+
+ fn name(&self) -> &'static str {
+ "FryPanBottom"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+
+ fn c(close: f64) -> Candle {
+ Candle::new_unchecked(close, close + 0.5, close - 0.5, close, 1_000.0, 0)
+ }
+
+ #[test]
+ fn rejects_small_period() {
+ assert!(matches!(
+ FryPanBottom::new(4),
+ Err(Error::InvalidPeriod { .. })
+ ));
+ assert!(FryPanBottom::new(5).is_ok());
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let f = FryPanBottom::new(9).unwrap();
+ assert_eq!(f.period(), 9);
+ assert_eq!(f.warmup_period(), 9);
+ assert_eq!(f.name(), "FryPanBottom");
+ assert!(!f.is_ready());
+ assert_eq!(f.value(), None);
+ }
+
+ #[test]
+ fn first_emission_at_warmup_period() {
+ let mut f = FryPanBottom::new(5).unwrap();
+ let out = f.batch(&[c(100.0), c(99.0), c(98.0), c(99.0), c(101.0), c(102.0)]);
+ for v in out.iter().take(4) {
+ assert!(v.is_none());
+ }
+ assert!(out[4].is_some());
+ }
+
+ #[test]
+ fn rounded_bottom_then_recovery_signals() {
+ let mut f = FryPanBottom::new(9).unwrap();
+ let closes = [100.0, 98.0, 96.0, 95.0, 96.0, 98.0, 101.0, 103.0, 105.0];
+ let candles: Vec = closes.iter().map(|&x| c(x)).collect();
+ let last = f.batch(&candles).into_iter().flatten().last().unwrap();
+ assert_eq!(last, 1.0);
+ }
+
+ #[test]
+ fn one_sided_drop_is_zero() {
+ // A straight decline (min at the end) is not a bowl.
+ let mut f = FryPanBottom::new(9).unwrap();
+ let candles: Vec = (0..9).map(|i| c(100.0 - f64::from(i))).collect();
+ let last = f.batch(&candles).into_iter().flatten().last().unwrap();
+ assert_eq!(last, 0.0);
+ }
+
+ #[test]
+ fn no_recovery_is_zero() {
+ // Bowl shape but the last close never climbs above the first.
+ let mut f = FryPanBottom::new(9).unwrap();
+ let closes = [100.0, 98.0, 96.0, 95.0, 96.0, 97.0, 98.0, 99.0, 99.5];
+ let candles: Vec = closes.iter().map(|&x| c(x)).collect();
+ let last = f.batch(&candles).into_iter().flatten().last().unwrap();
+ assert_eq!(last, 0.0);
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut f = FryPanBottom::new(5).unwrap();
+ f.batch(&[c(100.0), c(99.0), c(98.0), c(99.0), c(101.0)]);
+ assert!(f.is_ready());
+ f.reset();
+ assert!(!f.is_ready());
+ assert_eq!(f.value(), None);
+ assert_eq!(f.update(c(100.0)), None);
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..60)
+ .map(|i| c(100.0 + (f64::from(i) * 0.3).sin() * 5.0))
+ .collect();
+ let batch = FryPanBottom::new(9).unwrap().batch(&candles);
+ let mut b = FryPanBottom::new(9).unwrap();
+ let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
+ assert_eq!(batch, streamed);
+ }
+}
diff --git a/crates/wickra-core/src/indicators/harami_cross.rs b/crates/wickra-core/src/indicators/harami_cross.rs
new file mode 100644
index 00000000..49148f53
--- /dev/null
+++ b/crates/wickra-core/src/indicators/harami_cross.rs
@@ -0,0 +1,193 @@
+#![allow(clippy::doc_markdown)]
+
+//! Harami Cross — a Harami whose second candle is a Doji.
+//!
+//! A Harami Cross is a stronger Harami: a large real body followed by a Doji whose
+//! body sits *within* the prior body. The Doji's total indecision after a strong
+//! move makes the reversal signal more potent than a plain Harami.
+//!
+//! - **Bullish** (`+1.0`): the prior candle is a large **bearish** body
+//! (`close < open`) and the current candle is a Doji whose open and close lie
+//! within the prior body.
+//! - **Bearish** (`-1.0`): the prior candle is a large **bullish** body and the
+//! current is a contained Doji.
+//! - Otherwise the output is `0.0`.
+//!
+//! A doji is a candle whose body is `<= 0.1 * range`. The two-bar lookback means
+//! the first value lands on the second candle.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+fn is_doji(candle: Candle) -> bool {
+ let body = (candle.close - candle.open).abs();
+ let range = candle.high - candle.low;
+ range > 0.0 && body <= 0.1 * range
+}
+
+/// Harami Cross — large-body-then-contained-doji reversal detector.
+#[derive(Debug, Clone, Default)]
+pub struct HaramiCross {
+ prev: Option,
+ last_value: Option,
+}
+
+impl HaramiCross {
+ /// Construct a new `HaramiCross`.
+ #[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 HaramiCross {
+ 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 prev_body_low = prev.open.min(prev.close);
+ let prev_body_high = prev.open.max(prev.close);
+ let prev_is_solid = !is_doji(prev);
+ let curr_is_doji = is_doji(candle);
+ let contained = candle.open >= prev_body_low
+ && candle.open <= prev_body_high
+ && candle.close >= prev_body_low
+ && candle.close <= prev_body_high;
+
+ let v = if prev_is_solid && curr_is_doji && contained {
+ if prev.close < prev.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 {
+ "HaramiCross"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+
+ fn solid(open: f64, close: f64) -> Candle {
+ Candle::new_unchecked(
+ open,
+ open.max(close) + 0.2,
+ open.min(close) - 0.2,
+ close,
+ 0.0,
+ 0,
+ )
+ }
+
+ fn doji(mid: f64) -> Candle {
+ Candle::new_unchecked(mid, mid + 1.0, mid - 1.0, mid + 0.02, 0.0, 0)
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let h = HaramiCross::new();
+ assert_eq!(h.warmup_period(), 2);
+ assert_eq!(h.name(), "HaramiCross");
+ assert!(!h.is_ready());
+ assert_eq!(h.value(), None);
+ }
+
+ #[test]
+ fn first_bar_seeds_without_signal() {
+ let mut h = HaramiCross::new();
+ assert_eq!(h.update(solid(110.0, 100.0)), Some(0.0));
+ assert!(h.update(doji(105.0)).is_some());
+ }
+
+ #[test]
+ fn bullish_harami_cross() {
+ // prior big bearish body [100, 110]; doji centred at 105 inside it -> +1.
+ let mut h = HaramiCross::new();
+ h.update(solid(110.0, 100.0));
+ assert_eq!(h.update(doji(105.0)), Some(1.0));
+ }
+
+ #[test]
+ fn bearish_harami_cross() {
+ // prior big bullish body [100, 110]; doji inside -> -1.
+ let mut h = HaramiCross::new();
+ h.update(solid(100.0, 110.0));
+ assert_eq!(h.update(doji(105.0)), Some(-1.0));
+ }
+
+ #[test]
+ fn doji_outside_body_is_zero() {
+ let mut h = HaramiCross::new();
+ h.update(solid(110.0, 100.0));
+ // doji centred at 120, outside the prior body -> 0.
+ assert_eq!(h.update(doji(120.0)), Some(0.0));
+ }
+
+ #[test]
+ fn non_doji_second_is_zero() {
+ let mut h = HaramiCross::new();
+ h.update(solid(110.0, 100.0));
+ assert_eq!(h.update(solid(104.0, 106.0)), Some(0.0));
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut h = HaramiCross::new();
+ h.update(solid(110.0, 100.0));
+ h.update(doji(105.0));
+ assert!(h.is_ready());
+ h.reset();
+ assert!(!h.is_ready());
+ assert_eq!(h.update(solid(110.0, 100.0)), Some(0.0));
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| {
+ if i % 2 == 0 {
+ solid(110.0, 100.0)
+ } else {
+ doji(105.0)
+ }
+ })
+ .collect();
+ let batch = HaramiCross::new().batch(&candles);
+ let mut b = HaramiCross::new();
+ let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
+ assert_eq!(batch, streamed);
+ }
+}
diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs
index 16d3ee8f..333780de 100644
--- a/crates/wickra-core/src/indicators/mod.rs
+++ b/crates/wickra-core/src/indicators/mod.rs
@@ -117,6 +117,7 @@ mod downside_gap_three_methods;
mod dpo;
mod dragonfly_doji;
mod drawdown_duration;
+mod dumpling_top;
mod dx;
mod dynamic_momentum_index;
mod ease_of_movement;
@@ -153,6 +154,7 @@ mod footprint;
mod force_index;
mod fractal_chaos_bands;
mod frama;
+mod fry_pan_bottom;
mod funding_basis;
mod funding_rate;
mod funding_rate_mean;
@@ -171,6 +173,7 @@ mod gravestone_doji;
mod hammer;
mod hanging_man;
mod harami;
+mod harami_cross;
mod head_and_shoulders;
mod heikin_ashi;
mod heikin_ashi_oscillator;
@@ -265,6 +268,7 @@ mod morning_evening_star;
mod murrey_math_lines;
mod natr;
mod new_highs_new_lows;
+mod new_price_lines;
mod nrtr;
mod nvi;
mod ob_imbalance_full;
@@ -416,6 +420,7 @@ mod tick_index;
mod tii;
mod time_based_stop;
mod time_of_day_return_profile;
+mod tower_top_bottom;
mod tpo_profile;
mod trade_imbalance;
mod trade_volume_index;
@@ -427,6 +432,7 @@ mod triangle;
mod trima;
mod trin;
mod triple_top_bottom;
+mod tristar;
mod trix;
mod true_range;
mod tsf;
@@ -596,6 +602,7 @@ pub use downside_gap_three_methods::DownsideGapThreeMethods;
pub use dpo::Dpo;
pub use dragonfly_doji::DragonflyDoji;
pub use drawdown_duration::DrawdownDuration;
+pub use dumpling_top::DumplingTop;
pub use dx::Dx;
pub use dynamic_momentum_index::DynamicMomentumIndex;
pub use ease_of_movement::EaseOfMovement;
@@ -632,6 +639,7 @@ pub use footprint::{Footprint, FootprintLevel, FootprintOutput};
pub use force_index::ForceIndex;
pub use fractal_chaos_bands::{FractalChaosBands, FractalChaosBandsOutput};
pub use frama::Frama;
+pub use fry_pan_bottom::FryPanBottom;
pub use funding_basis::FundingBasis;
pub use funding_rate::FundingRate;
pub use funding_rate_mean::FundingRateMean;
@@ -650,6 +658,7 @@ pub use gravestone_doji::GravestoneDoji;
pub use hammer::Hammer;
pub use hanging_man::HangingMan;
pub use harami::Harami;
+pub use harami_cross::HaramiCross;
pub use head_and_shoulders::HeadAndShoulders;
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
pub use heikin_ashi_oscillator::HeikinAshiOscillator;
@@ -744,6 +753,7 @@ pub use morning_evening_star::MorningEveningStar;
pub use murrey_math_lines::{MurreyMathLines, MurreyMathLinesOutput};
pub use natr::Natr;
pub use new_highs_new_lows::NewHighsNewLows;
+pub use new_price_lines::NewPriceLines;
pub use nrtr::{Nrtr, NrtrOutput};
pub use nvi::Nvi;
pub use ob_imbalance_full::OrderBookImbalanceFull;
@@ -895,6 +905,7 @@ pub use tick_index::TickIndex;
pub use tii::Tii;
pub use time_based_stop::TimeBasedStop;
pub use time_of_day_return_profile::{TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput};
+pub use tower_top_bottom::TowerTopBottom;
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
pub use trade_imbalance::TradeImbalance;
pub use trade_volume_index::TradeVolumeIndex;
@@ -906,6 +917,7 @@ pub use triangle::Triangle;
pub use trima::Trima;
pub use trin::Trin;
pub use triple_top_bottom::TripleTopBottom;
+pub use tristar::Tristar;
pub use trix::Trix;
pub use true_range::TrueRange;
pub use tsf::Tsf;
@@ -1412,6 +1424,12 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"TasukiGap",
"UniqueThreeRiver",
"ConcealingBabySwallow",
+ "Tristar",
+ "HaramiCross",
+ "TowerTopBottom",
+ "FryPanBottom",
+ "DumplingTop",
+ "NewPriceLines",
],
),
(
@@ -1597,6 +1615,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, 479, "FAMILIES total drifted from indicator count");
+ assert_eq!(total, 485, "FAMILIES total drifted from indicator count");
}
}
diff --git a/crates/wickra-core/src/indicators/new_price_lines.rs b/crates/wickra-core/src/indicators/new_price_lines.rs
new file mode 100644
index 00000000..b6a66afa
--- /dev/null
+++ b/crates/wickra-core/src/indicators/new_price_lines.rs
@@ -0,0 +1,234 @@
+//! New Price Lines — the "eight/ten new price lines" exhaustion count.
+
+use crate::error::{Error, Result};
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// New Price Lines — the Japanese "shinne" (new-price) exhaustion count: when the
+/// close has made `count` consecutive new highs (or lows), the trend is considered
+/// stretched and ripe for a pause or reversal.
+///
+/// ```text
+/// consecutive higher closes form "new price lines" up
+/// consecutive lower closes form "new price lines" down
+/// signal = −1 once `count` consecutive higher closes (overbought / sell warning)
+/// signal = +1 once `count` consecutive lower closes (oversold / buy warning)
+/// signal = 0 otherwise
+/// ```
+///
+/// Traditional Japanese practice flags **eight** new price lines (and a stronger
+/// **ten** or twelve) as the point where a directional run becomes exhausted —
+/// the market has gone up (or down) so many bars in a row that a corrective pause
+/// is statistically due. The signal stays active for every bar the streak remains
+/// at or above `count`, and clears the moment a close breaks the streak.
+///
+/// The first value lands on the second bar (one prior close is needed). The
+/// output is `+1` / `0` / `−1`. Each `update` is O(1).
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Candle, Indicator, NewPriceLines};
+///
+/// let mut indicator = NewPriceLines::new(8).unwrap();
+/// let mut last = None;
+/// for i in 0..12 {
+/// let close = 100.0 + f64::from(i); // 11 consecutive higher closes
+/// let c = Candle::new(close, close, close, close, 1_000.0, 0).unwrap();
+/// last = indicator.update(c);
+/// }
+/// assert_eq!(last, Some(-1.0));
+/// ```
+#[derive(Debug, Clone)]
+pub struct NewPriceLines {
+ count: usize,
+ prev_close: Option,
+ consec_up: usize,
+ consec_down: usize,
+ last: Option,
+}
+
+impl NewPriceLines {
+ /// Construct a New Price Lines counter that fires at `count` consecutive new
+ /// closes (classic `8`, stronger `10`/`12`).
+ ///
+ /// # Errors
+ ///
+ /// Returns [`Error::InvalidPeriod`] if `count < 2`.
+ pub fn new(count: usize) -> Result {
+ if count < 2 {
+ return Err(Error::InvalidPeriod {
+ message: "new price lines count must be >= 2",
+ });
+ }
+ Ok(Self {
+ count,
+ prev_close: None,
+ consec_up: 0,
+ consec_down: 0,
+ last: None,
+ })
+ }
+
+ /// Configured count threshold.
+ pub const fn count(&self) -> usize {
+ self.count
+ }
+
+ /// Current consecutive streak `(up, down)`.
+ pub const fn streak(&self) -> (usize, usize) {
+ (self.consec_up, self.consec_down)
+ }
+
+ /// Current value if available.
+ pub const fn value(&self) -> Option {
+ self.last
+ }
+}
+
+impl Indicator for NewPriceLines {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ let close = candle.close;
+ let Some(prev) = self.prev_close else {
+ self.prev_close = Some(close);
+ return None;
+ };
+ if close > prev {
+ self.consec_up += 1;
+ self.consec_down = 0;
+ } else if close < prev {
+ self.consec_down += 1;
+ self.consec_up = 0;
+ } else {
+ self.consec_up = 0;
+ self.consec_down = 0;
+ }
+ self.prev_close = Some(close);
+
+ let v = if self.consec_up >= self.count {
+ -1.0
+ } else if self.consec_down >= self.count {
+ 1.0
+ } else {
+ 0.0
+ };
+ self.last = Some(v);
+ Some(v)
+ }
+
+ fn reset(&mut self) {
+ self.prev_close = None;
+ self.consec_up = 0;
+ self.consec_down = 0;
+ self.last = None;
+ }
+
+ fn warmup_period(&self) -> usize {
+ 2
+ }
+
+ fn is_ready(&self) -> bool {
+ self.last.is_some()
+ }
+
+ fn name(&self) -> &'static str {
+ "NewPriceLines"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+
+ fn c(close: f64) -> Candle {
+ Candle::new_unchecked(close, close, close, close, 1_000.0, 0)
+ }
+
+ #[test]
+ fn rejects_small_count() {
+ assert!(matches!(
+ NewPriceLines::new(1),
+ Err(Error::InvalidPeriod { .. })
+ ));
+ assert!(NewPriceLines::new(2).is_ok());
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let n = NewPriceLines::new(8).unwrap();
+ assert_eq!(n.count(), 8);
+ assert_eq!(n.streak(), (0, 0));
+ assert_eq!(n.warmup_period(), 2);
+ assert_eq!(n.name(), "NewPriceLines");
+ assert!(!n.is_ready());
+ assert_eq!(n.value(), None);
+ }
+
+ #[test]
+ fn first_bar_seeds_without_signal() {
+ let mut n = NewPriceLines::new(3).unwrap();
+ assert_eq!(n.update(c(100.0)), None);
+ assert!(n.update(c(101.0)).is_some());
+ }
+
+ #[test]
+ fn eight_higher_closes_signal_sell() {
+ let mut n = NewPriceLines::new(8).unwrap();
+ // 11 consecutive higher closes -> by the 9th the count reaches 8 -> -1.
+ let candles: Vec = (0..12).map(|i| c(100.0 + f64::from(i))).collect();
+ let last = n.batch(&candles).into_iter().flatten().last().unwrap();
+ assert_eq!(last, -1.0);
+ }
+
+ #[test]
+ fn eight_lower_closes_signal_buy() {
+ let mut n = NewPriceLines::new(8).unwrap();
+ let candles: Vec = (0..12).map(|i| c(200.0 - f64::from(i))).collect();
+ let last = n.batch(&candles).into_iter().flatten().last().unwrap();
+ assert_eq!(last, 1.0);
+ }
+
+ #[test]
+ fn break_in_streak_clears_signal() {
+ let mut n = NewPriceLines::new(3).unwrap();
+ n.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]); // streak 3 -> -1
+ assert_eq!(n.value(), Some(-1.0));
+ // A lower close breaks the up streak.
+ assert_eq!(n.update(c(102.0)), Some(0.0));
+ assert_eq!(n.streak(), (0, 1));
+ }
+
+ #[test]
+ fn unchanged_close_resets_streak() {
+ let mut n = NewPriceLines::new(3).unwrap();
+ n.batch(&[c(100.0), c(101.0), c(102.0)]);
+ assert_eq!(n.update(c(102.0)), Some(0.0)); // equal -> reset
+ assert_eq!(n.streak(), (0, 0));
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut n = NewPriceLines::new(3).unwrap();
+ n.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]);
+ assert!(n.is_ready());
+ n.reset();
+ assert!(!n.is_ready());
+ assert_eq!(n.value(), None);
+ assert_eq!(n.streak(), (0, 0));
+ }
+
+ #[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 = NewPriceLines::new(8).unwrap().batch(&candles);
+ let mut b = NewPriceLines::new(8).unwrap();
+ let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
+ assert_eq!(batch, streamed);
+ }
+}
diff --git a/crates/wickra-core/src/indicators/tower_top_bottom.rs b/crates/wickra-core/src/indicators/tower_top_bottom.rs
new file mode 100644
index 00000000..436e558c
--- /dev/null
+++ b/crates/wickra-core/src/indicators/tower_top_bottom.rs
@@ -0,0 +1,224 @@
+#![allow(clippy::doc_markdown)]
+
+//! Tower Top / Tower Bottom — a tall bar, a pause, then a tall opposite bar.
+//!
+//! A Tower is a reversal where a strong directional bar is followed by a small
+//! "pause" bar and then a strong bar in the *opposite* direction, like two towers
+//! flanking a low wall. This is the compact three-bar form of the classic
+//! multi-bar Tower pattern.
+//!
+//! - **Tower Bottom** (`+1.0`): a tall **bearish** bar, a small-bodied bar, then a
+//! tall **bullish** bar.
+//! - **Tower Top** (`-1.0`): a tall **bullish** bar, a small-bodied bar, then a
+//! tall **bearish** bar.
+//! - Otherwise the output is `0.0`.
+//!
+//! "Tall" = body `>= 0.5 * range`; "small" = body `<= 0.3 * range`. The three-bar
+//! lookback means the first value lands on the third candle.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+fn body_fraction(candle: Candle) -> f64 {
+ let range = candle.high - candle.low;
+ if range > 0.0 {
+ (candle.close - candle.open).abs() / range
+ } else {
+ 0.0
+ }
+}
+
+fn is_tall(candle: Candle) -> bool {
+ body_fraction(candle) >= 0.5
+}
+
+fn is_small(candle: Candle) -> bool {
+ body_fraction(candle) <= 0.3
+}
+
+/// Tower Top / Bottom — three-bar reversal detector.
+#[derive(Debug, Clone, Default)]
+pub struct TowerTopBottom {
+ c1: Option,
+ c2: Option,
+ last_value: Option,
+}
+
+impl TowerTopBottom {
+ /// Construct a new `TowerTopBottom`.
+ #[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 TowerTopBottom {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ let (Some(first), Some(middle)) = (self.c1, self.c2) else {
+ self.c1 = self.c2;
+ self.c2 = Some(candle);
+ self.last_value = Some(0.0);
+ return Some(0.0);
+ };
+ let pause = is_small(middle);
+ let first_tall = is_tall(first);
+ let last_tall = is_tall(candle);
+ let v = if pause && first_tall && last_tall {
+ let first_up = first.close > first.open;
+ let last_up = candle.close > candle.open;
+ if !first_up && last_up {
+ 1.0
+ } else if first_up && !last_up {
+ -1.0
+ } else {
+ 0.0
+ }
+ } else {
+ 0.0
+ };
+ self.c1 = self.c2;
+ self.c2 = Some(candle);
+ self.last_value = Some(v);
+ Some(v)
+ }
+
+ fn reset(&mut self) {
+ self.c1 = None;
+ self.c2 = 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 {
+ "TowerTopBottom"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+
+ /// A tall candle from `open` to `close` (body fills most of the range).
+ fn tall(open: f64, close: f64) -> Candle {
+ Candle::new_unchecked(
+ open,
+ open.max(close) + 0.1,
+ open.min(close) - 0.1,
+ close,
+ 0.0,
+ 0,
+ )
+ }
+
+ /// A small-bodied candle (long shadows, tiny body).
+ fn small(mid: f64) -> Candle {
+ Candle::new_unchecked(mid, mid + 2.0, mid - 2.0, mid + 0.1, 0.0, 0)
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let t = TowerTopBottom::new();
+ assert_eq!(t.warmup_period(), 3);
+ assert_eq!(t.name(), "TowerTopBottom");
+ assert!(!t.is_ready());
+ assert_eq!(t.value(), None);
+ }
+
+ #[test]
+ fn first_two_bars_seed_without_signal() {
+ let mut t = TowerTopBottom::new();
+ assert_eq!(t.update(tall(100.0, 110.0)), Some(0.0));
+ assert_eq!(t.update(small(105.0)), Some(0.0));
+ assert!(t.update(tall(110.0, 100.0)).is_some());
+ }
+
+ #[test]
+ fn tower_top() {
+ // tall bullish, small pause, tall bearish -> top -> -1.
+ let mut t = TowerTopBottom::new();
+ t.update(tall(100.0, 110.0));
+ t.update(small(110.0));
+ assert_eq!(t.update(tall(110.0, 100.0)), Some(-1.0));
+ }
+
+ #[test]
+ fn tower_bottom() {
+ let mut t = TowerTopBottom::new();
+ t.update(tall(110.0, 100.0));
+ t.update(small(100.0));
+ assert_eq!(t.update(tall(100.0, 110.0)), Some(1.0));
+ }
+
+ #[test]
+ fn same_direction_is_zero() {
+ let mut t = TowerTopBottom::new();
+ t.update(tall(100.0, 110.0));
+ t.update(small(110.0));
+ // last bar also bullish -> not a tower -> 0.
+ assert_eq!(t.update(tall(110.0, 120.0)), Some(0.0));
+ }
+
+ #[test]
+ fn no_pause_is_zero() {
+ let mut t = TowerTopBottom::new();
+ t.update(tall(100.0, 110.0));
+ t.update(tall(110.0, 120.0)); // middle is tall, not a pause
+ assert_eq!(t.update(tall(120.0, 110.0)), Some(0.0));
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = TowerTopBottom::new();
+ t.update(tall(100.0, 110.0));
+ t.update(small(110.0));
+ t.update(tall(110.0, 100.0));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(tall(100.0, 110.0)), Some(0.0));
+ }
+
+ #[test]
+ fn zero_range_bar_has_zero_body_fraction() {
+ // A flat bar (high == low) exercises the zero-range body-fraction branch;
+ // it counts as a small "pause" bar, so tall-flat-tall still reverses.
+ fn flat(mid: f64) -> Candle {
+ Candle::new_unchecked(mid, mid, mid, mid, 0.0, 0)
+ }
+ let mut t = TowerTopBottom::new();
+ t.update(tall(100.0, 110.0));
+ t.update(flat(110.0));
+ assert_eq!(t.update(tall(110.0, 100.0)), Some(-1.0));
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..30)
+ .map(|i| match i % 3 {
+ 0 => tall(100.0, 110.0),
+ 1 => small(110.0),
+ _ => tall(110.0, 100.0),
+ })
+ .collect();
+ let batch = TowerTopBottom::new().batch(&candles);
+ let mut b = TowerTopBottom::new();
+ let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
+ assert_eq!(batch, streamed);
+ }
+}
diff --git a/crates/wickra-core/src/indicators/tristar.rs b/crates/wickra-core/src/indicators/tristar.rs
new file mode 100644
index 00000000..6d43077d
--- /dev/null
+++ b/crates/wickra-core/src/indicators/tristar.rs
@@ -0,0 +1,189 @@
+#![allow(clippy::doc_markdown)]
+
+//! Tristar — a three-doji reversal pattern.
+//!
+//! A Tristar is three consecutive Doji candles where the middle one gaps away
+//! from its neighbours, forming a star. A bearish Tristar (top) has the middle
+//! doji sitting above the other two; a bullish Tristar (bottom) has it below.
+//!
+//! - **Bullish** (`+1.0`): three dojis, the middle doji's body centre below both
+//! neighbours' body centres.
+//! - **Bearish** (`-1.0`): three dojis, the middle above both neighbours.
+//! - Otherwise the output is `0.0`.
+//!
+//! A doji is a candle whose body is `<= 0.1 * range`. The three-bar lookback means
+//! the first value lands on the third candle.
+
+use crate::ohlcv::Candle;
+use crate::traits::Indicator;
+
+/// Body-centre of a candle.
+fn body_mid(candle: Candle) -> f64 {
+ f64::midpoint(candle.open, candle.close)
+}
+
+/// Whether a candle is a doji (body small relative to range).
+fn is_doji(candle: Candle) -> bool {
+ let body = (candle.close - candle.open).abs();
+ let range = candle.high - candle.low;
+ range > 0.0 && body <= 0.1 * range
+}
+
+/// Tristar — three-doji star reversal detector.
+#[derive(Debug, Clone, Default)]
+pub struct Tristar {
+ c1: Option,
+ c2: Option,
+ last_value: Option,
+}
+
+impl Tristar {
+ /// Construct a new `Tristar`.
+ #[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 Tristar {
+ type Input = Candle;
+ type Output = f64;
+
+ fn update(&mut self, candle: Candle) -> Option {
+ let (Some(first), Some(middle)) = (self.c1, self.c2) else {
+ self.c1 = self.c2;
+ self.c2 = Some(candle);
+ self.last_value = Some(0.0);
+ return Some(0.0);
+ };
+ let v = if is_doji(first) && is_doji(middle) && is_doji(candle) {
+ let mid = body_mid(middle);
+ let n1 = body_mid(first);
+ let n3 = body_mid(candle);
+ if mid > n1 && mid > n3 {
+ -1.0
+ } else if mid < n1 && mid < n3 {
+ 1.0
+ } else {
+ 0.0
+ }
+ } else {
+ 0.0
+ };
+ self.c1 = self.c2;
+ self.c2 = Some(candle);
+ self.last_value = Some(v);
+ Some(v)
+ }
+
+ fn reset(&mut self) {
+ self.c1 = None;
+ self.c2 = 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 {
+ "Tristar"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+
+ /// A doji centred at `mid` (tiny body, symmetric shadows).
+ fn doji(mid: f64) -> Candle {
+ Candle::new_unchecked(mid, mid + 1.0, mid - 1.0, mid + 0.02, 0.0, 0)
+ }
+
+ /// A non-doji (big body).
+ fn solid(open: f64, close: f64) -> Candle {
+ Candle::new_unchecked(
+ open,
+ open.max(close) + 0.1,
+ open.min(close) - 0.1,
+ close,
+ 0.0,
+ 0,
+ )
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let t = Tristar::new();
+ assert_eq!(t.warmup_period(), 3);
+ assert_eq!(t.name(), "Tristar");
+ assert!(!t.is_ready());
+ assert_eq!(t.value(), None);
+ }
+
+ #[test]
+ fn first_two_bars_seed_without_signal() {
+ let mut t = Tristar::new();
+ assert_eq!(t.update(doji(100.0)), Some(0.0));
+ assert_eq!(t.update(doji(100.0)), Some(0.0));
+ assert!(t.update(doji(100.0)).is_some());
+ }
+
+ #[test]
+ fn bearish_tristar_top() {
+ // middle doji centred above the two neighbours -> top -> -1.
+ let mut t = Tristar::new();
+ t.update(doji(100.0));
+ t.update(doji(105.0)); // middle, highest
+ assert_eq!(t.update(doji(100.0)), Some(-1.0));
+ }
+
+ #[test]
+ fn bullish_tristar_bottom() {
+ let mut t = Tristar::new();
+ t.update(doji(100.0));
+ t.update(doji(95.0)); // middle, lowest
+ assert_eq!(t.update(doji(100.0)), Some(1.0));
+ }
+
+ #[test]
+ fn non_doji_is_zero() {
+ let mut t = Tristar::new();
+ t.update(doji(100.0));
+ t.update(solid(100.0, 110.0)); // not a doji
+ assert_eq!(t.update(doji(100.0)), Some(0.0));
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = Tristar::new();
+ t.update(doji(100.0));
+ t.update(doji(105.0));
+ t.update(doji(100.0));
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.update(doji(100.0)), Some(0.0));
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let candles: Vec = (0..40)
+ .map(|i| doji(100.0 + (f64::from(i) * 0.4).sin() * 5.0))
+ .collect();
+ let batch = Tristar::new().batch(&candles);
+ let mut b = Tristar::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 5f8ea216..b002fc15 100644
--- a/crates/wickra-core/src/lib.rs
+++ b/crates/wickra-core/src/lib.rs
@@ -78,24 +78,25 @@ pub use indicators::{
DecyclerOscillator, Dema, DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope,
DerivativeOscillator, DetrendedStdDev, DisparityIndex, DistanceSsd, Doji, DojiStar, Donchian,
DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput,
- DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, Dx,
- DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse,
- ElderRay, ElderRayOutput, ElderSafeZone, ElderSafeZoneOutput, Ema, EmpiricalModeDecomposition,
- Engulfing, Equivolume, EquivolumeOutput, EvenBetterSinewave, EveningDojiStar, Evwma,
- EwmaVolatility, Expectancy, FallingThreeMethods, Fama, FibArcs, FibArcsOutput, FibChannel,
- FibChannelOutput, FibConfluence, FibConfluenceOutput, FibExtension, FibExtensionOutput, FibFan,
- FibFanOutput, FibProjection, FibProjectionOutput, FibRetracement, FibRetracementOutput,
- FibTimeZones, FibTimeZonesOutput, FibonacciPivots, FibonacciPivotsOutput, FisherRsi,
- FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex, FractalChaosBands,
- FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean, FundingRateZScore,
- GainLossRatio, GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator,
- GatorOscillatorOutput, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput,
- GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi,
- HeikinAshiOscillator, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave,
- HighpassFilter, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
- HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
- HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
- Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
+ DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, DumplingTop,
+ Dx, DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma,
+ ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone, ElderSafeZoneOutput, Ema,
+ EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput, EvenBetterSinewave,
+ EveningDojiStar, Evwma, EwmaVolatility, Expectancy, FallingThreeMethods, Fama, FibArcs,
+ FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput, FibExtension,
+ FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput, FibRetracement,
+ FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots, FibonacciPivotsOutput,
+ FisherRsi, FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex,
+ FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom, FundingBasis, FundingRate,
+ FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, Garch11,
+ GarmanKlassVolatility, Gartley, GatorOscillator, GatorOscillatorOutput, GeneralizedDema,
+ GeometricMa, GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer,
+ HangingMan, Harami, HaramiCross, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator,
+ HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, HighpassFilter, Hikkake,
+ HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon,
+ HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput,
+ HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia,
+ InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile,
IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, JarqueBera, Jma,
JumpIndicator, KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop,
@@ -110,10 +111,10 @@ pub use indicators::{
McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianChannelOutput, MedianMa,
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop,
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines,
- MurreyMathLinesOutput, Natr, NewHighsNewLows, Nrtr, NrtrOutput, Nvi, OIPriceDivergence,
- OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange,
- OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN,
- OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
+ MurreyMathLinesOutput, Natr, NewHighsNewLows, NewPriceLines, Nrtr, NrtrOutput, Nvi,
+ OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu,
+ OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1,
+ OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo,
@@ -139,20 +140,20 @@ pub use indicators::{
TdSequentialOutput, TdSetup, TdTrap, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth,
Thrusting, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput,
- TpoProfile, TpoProfileOutput, TradeImbalance, TradeVolumeIndex, TrendLabel, TrendStrengthIndex,
- Trendflex, TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Trix, TrueRange, Tsf,
- TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend, TurnOfMonth, Tweezer,
- TwiggsMoneyFlow, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver,
- UniversalOscillator, UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea,
- ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya,
- VolatilityCone, VolatilityConeOutput, VolatilityOfVolatility, VolatilityRatio, VoltyStop,
- VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend,
- VolumeProfile, VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput,
- VolumeWeightedSr, VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
- VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, WaveTrend, WaveTrendOutput, Wedge,
- WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma,
- WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
- ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
+ TowerTopBottom, TpoProfile, TpoProfileOutput, TradeImbalance, TradeVolumeIndex, TrendLabel,
+ TrendStrengthIndex, Trendflex, TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Tristar,
+ Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend,
+ TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
+ UniqueThreeRiver, UniversalOscillator, UpDownVolumeRatio, UpsideGapThreeMethods,
+ UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio,
+ VerticalHorizontalFilter, Vidya, VolatilityCone, VolatilityConeOutput, VolatilityOfVolatility,
+ VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator,
+ VolumePriceTrend, VolumeProfile, VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd,
+ VolumeWeightedMacdOutput, VolumeWeightedSr, VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin,
+ Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, WaveTrend,
+ WaveTrendOutput, Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput,
+ WilliamsR, WinRate, Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit,
+ ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
};
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
// line so the indicator-count tooling (which scans the braced block above and
diff --git a/docs/README.md b/docs/README.md
index 41daa815..963f6331 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 **479 indicators** across
+- A per-indicator deep dive for every one of the **485 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 06407450..08ca6c1b 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, CandleVolume, 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, Equivolume, 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, HeikinAshiOscillator, 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, SmoothedHeikinAshi, 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, ThreeLineBreak, 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, CandleVolume, 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, DumplingTop, Dx, EaseOfMovement, ElderRay, ElderSafeZone, Engulfing, Equivolume, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, FryPanBottom, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator, 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, NewPriceLines, 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, SmoothedHeikinAshi, 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, ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeBasedStop, TimeOfDayReturnProfile, TowerTopBottom, TpoProfile, TradeVolumeIndex, Triangle, TripleTopBottom, Tristar, 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,12 @@ fuzz_target!(|data: Vec| {
}
// --- Candlestick Patterns (family 14) ---
+ drive(TowerTopBottom::new, &candles);
+ drive(HaramiCross::new, &candles);
+ drive(Tristar::new, &candles);
+ drive(|| FryPanBottom::new(9).unwrap(), &candles);
+ drive(|| DumplingTop::new(9).unwrap(), &candles);
+ drive(|| NewPriceLines::new(5).unwrap(), &candles);
drive(TdTrap::new, &candles);
drive(TdPropulsion::new, &candles);
drive(TdClopwin::new, &candles);