diff --git a/CHANGELOG.md b/CHANGELOG.md index d82cc1b0..e3dbfb26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ 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] +- **Composite Profile** — multi-session composite volume profile exposing POC, VAH and VAL (`CompositeProfile`). +- **High/Low Volume Nodes** — highest- and lowest-volume price nodes in the profile (`HighLowVolumeNodes`). +- **Profile Shape** — profile shape classification (b/P/D normal) as a numeric code (`ProfileShape`). +- **Single Prints** — count of single-print (low-activity) price levels in the profile (`SinglePrints`). +- **Naked POC** — most recent untouched (naked) point of control level (`NakedPoc`). ## [0.7.1] - 2026-06-08 - **Open-Interest Momentum** — rate-of-change of open interest over a rolling window (`OpenInterestMomentum`). diff --git a/README.md b/README.md index 571a9f67..68a49006 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

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

[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml) @@ -48,7 +48,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**: [Node](https://docs.wickra.org/Quickstart-Node), [WASM](https://docs.wickra.org/Quickstart-WASM). - **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for - every one of the 493 indicators; start at the + every one of the 498 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.** 493 indicators across 24 +- **The biggest streaming-native catalogue, period.** 498 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 493 indicators**. + for all 498 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** | **493** | **yes** | +| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **498** | **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 -493 streaming-first indicators across twenty-four families. Every one passes the +498 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). @@ -155,7 +155,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview). | Fibonacci | Fibonacci Retracement, Fibonacci Extension, Fibonacci Projection, Auto-Fibonacci, Golden Pocket, Fibonacci Confluence, Fibonacci Fan, Fibonacci Arcs, Fibonacci Channel, Fibonacci Time Zones | | Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint, Order Flow Imbalance, VPIN, Amihud Illiquidity, Roll Measure, Trade-Sign Autocorrelation, Hasbrouck Information Share | | Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread, Estimated Leverage Ratio, OI-to-Volume Ratio, Perpetual Premium Index, Funding-Implied APR, Open-Interest Momentum | -| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range | +| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range, Naked POC, Single Prints, Profile Shape, High/Low Volume Nodes, Composite Profile | | Market Breadth | Advance/Decline Line, Advance/Decline Ratio, Advance/Decline Volume Line, McClellan Oscillator, McClellan Summation Index, TRIN / Arms Index, Breadth Thrust, New Highs - New Lows, High-Low Index, Percent Above Moving Average, Up/Down Volume Ratio, Bullish Percent Index, Cumulative Volume Index, Absolute Breadth Index, TICK Index | | Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) | | Seasonality & Session | Session VWAP, Session High/Low, Session Range, Average Daily Range, Overnight Gap, Overnight/Intraday Return, Turn-of-Month, Seasonal Z-Score, Time-of-Day Return Profile, Day-of-Week Profile, Intraday Volatility Profile, Volume-by-Time Profile | @@ -237,7 +237,7 @@ A Python live-trading example using the public `websockets` package lives at ``` wickra/ ├── crates/ -│ ├── wickra-core/ core engine + all 493 indicators +│ ├── wickra-core/ core engine + all 498 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 c9075e10..e2341ba5 100644 --- a/bindings/node/__tests__/indicators.test.js +++ b/bindings/node/__tests__/indicators.test.js @@ -392,6 +392,9 @@ const candleScalar = { 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) }, + NakedPoc: { make: () => new wickra.NakedPoc(20, 24), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, + SinglePrints: { make: () => new wickra.SinglePrints(20, 24), step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, + ProfileShape: { make: () => new wickra.ProfileShape(20, 24), step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) }, }; for (const [name, d] of Object.entries(candleScalar)) { @@ -497,6 +500,8 @@ const multi = { SmoothedHeikinAshi: { make: () => new wickra.SmoothedHeikinAshi(5), fields: ['open', 'high', 'low', 'close'], step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) }, Equivolume: { make: () => new wickra.Equivolume(20), fields: ['height', 'width'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) }, CandleVolume: { make: () => new wickra.CandleVolume(20), fields: ['body', 'width'], step: (ind, i) => ind.update(open[i], close[i], volume[i]), batch: (ind) => ind.batch(open, close, volume) }, + HighLowVolumeNodes: { make: () => new wickra.HighLowVolumeNodes(20, 24), fields: ['hvn', 'lvn'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) }, + CompositeProfile: { make: () => new wickra.CompositeProfile(20, 24, 0.7), fields: ['poc', 'vah', 'val'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) }, }; for (const [name, d] of Object.entries(multi)) { diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index e58b0423..f0dde457 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -409,6 +409,15 @@ export interface VolumeProfileValue { priceHigh: number bins: Array } +export interface HighLowVolumeNodesValue { + hvn: number + lvn: number +} +export interface CompositeProfileValue { + poc: number + vah: number + val: number +} export interface TpoProfileValue { priceLow: number priceHigh: number @@ -3470,6 +3479,51 @@ export declare class ValueArea { update(high: number, low: number, volume: number): ValueAreaValue | null batch(high: Array, low: Array, volume: Array): Array } +export type NakedPocNode = NakedPoc +export declare class NakedPoc { + constructor(sessionLen: number, binCount: number) + update(high: number, low: number, close: number, volume: number): number | null + batch(high: Array, low: Array, close: Array, volume: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type SinglePrintsNode = SinglePrints +export declare class SinglePrints { + constructor(period: number, binCount: number) + update(high: number, low: number): number | null + batch(high: Array, low: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type ProfileShapeNode = ProfileShape +export declare class ProfileShape { + constructor(period: number, binCount: number) + update(high: number, low: number, volume: number): number | null + batch(high: Array, low: Array, volume: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type HighLowVolumeNodesNode = HighLowVolumeNodes +export declare class HighLowVolumeNodes { + constructor(period: number, binCount: number) + update(high: number, low: number, volume: number): HighLowVolumeNodesValue | null + batch(high: Array, low: Array, volume: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type CompositeProfileNode = CompositeProfile +export declare class CompositeProfile { + constructor(period: number, binCount: number, valueAreaPct: number) + update(high: number, low: number, volume: number): CompositeProfileValue | null + batch(high: Array, low: Array, volume: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} export type VolumeProfileNode = VolumeProfile export declare class VolumeProfile { constructor(period: number, binCount: number) diff --git a/bindings/node/index.js b/bindings/node/index.js index 6cecb793..a1193461 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, HasbrouckInformationShare, 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, TradeSignAutocorrelation, Pin, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, EstimatedLeverageRatio, OiToVolumeRatio, PerpetualPremiumIndex, FundingImpliedApr, OpenInterestMomentum, 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, HasbrouckInformationShare, 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, NakedPoc, SinglePrints, ProfileShape, HighLowVolumeNodes, CompositeProfile, 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, TradeSignAutocorrelation, Pin, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, EstimatedLeverageRatio, OiToVolumeRatio, PerpetualPremiumIndex, FundingImpliedApr, OpenInterestMomentum, 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 @@ -618,6 +618,11 @@ module.exports.FryPanBottom = FryPanBottom module.exports.DumplingTop = DumplingTop module.exports.NewPriceLines = NewPriceLines module.exports.ValueArea = ValueArea +module.exports.NakedPoc = NakedPoc +module.exports.SinglePrints = SinglePrints +module.exports.ProfileShape = ProfileShape +module.exports.HighLowVolumeNodes = HighLowVolumeNodes +module.exports.CompositeProfile = CompositeProfile module.exports.VolumeProfile = VolumeProfile module.exports.TpoProfile = TpoProfile module.exports.InitialBalance = InitialBalance diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index 0567b72e..1001fc9d 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -12874,6 +12874,332 @@ pub struct VolumeProfileValue { pub bins: Vec, } +// Naked POC: most recent untouched point-of-control level (Candle -> f64). +#[napi(js_name = "NakedPoc")] +pub struct NakedPocNode { + inner: wc::NakedPoc, +} + +#[napi] +impl NakedPocNode { + #[napi(constructor)] + pub fn new(session_len: u32, bin_count: u32) -> napi::Result { + Ok(Self { + inner: wc::NakedPoc::new(session_len as usize, bin_count as usize).map_err(map_err)?, + }) + } + #[napi] + pub fn update( + &mut self, + high: f64, + low: f64, + close: f64, + volume: f64, + ) -> napi::Result> { + Ok(self.inner.update(cnd(high, low, close, volume)?)) + } + #[napi] + pub fn batch( + &mut self, + high: Vec, + low: Vec, + close: Vec, + volume: Vec, + ) -> napi::Result> { + if high.len() != low.len() || low.len() != close.len() || close.len() != volume.len() { + return Err(NapiError::from_reason( + "high, low, close, volume must be equal length".to_string(), + )); + } + let mut out = Vec::with_capacity(close.len()); + for i in 0..close.len() { + out.push( + self.inner + .update(cnd(high[i], low[i], close[i], volume[i])?) + .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 + } +} + +// Single Prints: count of single-print price levels (Candle -> f64). +#[napi(js_name = "SinglePrints")] +pub struct SinglePrintsNode { + inner: wc::SinglePrints, +} + +#[napi] +impl SinglePrintsNode { + #[napi(constructor)] + pub fn new(period: u32, bin_count: u32) -> napi::Result { + Ok(Self { + inner: wc::SinglePrints::new(period as usize, bin_count as usize).map_err(map_err)?, + }) + } + #[napi] + pub fn update(&mut self, high: f64, low: f64) -> napi::Result> { + let mid = f64::midpoint(high, low); + Ok(self + .inner + .update(wc::Candle::new(mid, high, low, mid, 0.0, 0).map_err(map_err)?)) + } + #[napi] + pub fn batch(&mut self, high: Vec, low: Vec) -> napi::Result> { + if high.len() != low.len() { + return Err(NapiError::from_reason( + "high and low must be equal length".to_string(), + )); + } + let mut out = Vec::with_capacity(high.len()); + for i in 0..high.len() { + let mid = f64::midpoint(high[i], low[i]); + out.push( + self.inner + .update(wc::Candle::new(mid, high[i], low[i], mid, 0.0, 0).map_err(map_err)?) + .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 + } +} + +// Profile Shape: b/P/D classification as a numeric code (Candle -> f64). +#[napi(js_name = "ProfileShape")] +pub struct ProfileShapeNode { + inner: wc::ProfileShape, +} + +#[napi] +impl ProfileShapeNode { + #[napi(constructor)] + pub fn new(period: u32, bin_count: u32) -> napi::Result { + Ok(Self { + inner: wc::ProfileShape::new(period as usize, bin_count as usize).map_err(map_err)?, + }) + } + #[napi] + pub fn update(&mut self, high: f64, low: f64, volume: f64) -> napi::Result> { + let mid = f64::midpoint(high, low); + Ok(self + .inner + .update(wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?)) + } + #[napi] + pub fn batch( + &mut self, + high: Vec, + low: Vec, + volume: Vec, + ) -> napi::Result> { + if high.len() != low.len() || low.len() != volume.len() { + return Err(NapiError::from_reason( + "high, low, volume must be equal length".to_string(), + )); + } + let mut out = Vec::with_capacity(high.len()); + for i in 0..high.len() { + let mid = f64::midpoint(high[i], low[i]); + out.push( + self.inner + .update( + wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0) + .map_err(map_err)?, + ) + .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 + } +} + +#[napi(object)] +pub struct HighLowVolumeNodesValue { + pub hvn: f64, + pub lvn: f64, +} + +// High/Low Volume Nodes: highest- and lowest-volume price nodes (Candle -> struct). +#[napi(js_name = "HighLowVolumeNodes")] +pub struct HighLowVolumeNodesNode { + inner: wc::HighLowVolumeNodes, +} + +#[napi] +impl HighLowVolumeNodesNode { + #[napi(constructor)] + pub fn new(period: u32, bin_count: u32) -> napi::Result { + Ok(Self { + inner: wc::HighLowVolumeNodes::new(period as usize, bin_count as usize) + .map_err(map_err)?, + }) + } + #[napi] + pub fn update( + &mut self, + high: f64, + low: f64, + volume: f64, + ) -> napi::Result> { + let mid = f64::midpoint(high, low); + let candle = wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?; + Ok(self.inner.update(candle).map(|o| HighLowVolumeNodesValue { + hvn: o.hvn, + lvn: o.lvn, + })) + } + #[napi] + pub fn batch( + &mut self, + high: Vec, + low: Vec, + volume: Vec, + ) -> napi::Result> { + if high.len() != low.len() || low.len() != volume.len() { + return Err(NapiError::from_reason( + "high, low, volume must be equal length".to_string(), + )); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 2]; + for i in 0..n { + let mid = f64::midpoint(high[i], low[i]); + let candle = + wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0).map_err(map_err)?; + if let Some(o) = self.inner.update(candle) { + out[i * 2] = o.hvn; + out[i * 2 + 1] = o.lvn; + } + } + 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 + } +} + +#[napi(object)] +pub struct CompositeProfileValue { + pub poc: f64, + pub vah: f64, + pub val: f64, +} + +// Composite Profile: multi-session composite volume profile (Candle -> struct). +#[napi(js_name = "CompositeProfile")] +pub struct CompositeProfileNode { + inner: wc::CompositeProfile, +} + +#[napi] +impl CompositeProfileNode { + #[napi(constructor)] + pub fn new(period: u32, bin_count: u32, value_area_pct: f64) -> napi::Result { + Ok(Self { + inner: wc::CompositeProfile::new(period as usize, bin_count as usize, value_area_pct) + .map_err(map_err)?, + }) + } + #[napi] + pub fn update( + &mut self, + high: f64, + low: f64, + volume: f64, + ) -> napi::Result> { + let mid = f64::midpoint(high, low); + let candle = wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?; + Ok(self.inner.update(candle).map(|o| CompositeProfileValue { + poc: o.poc, + vah: o.vah, + val: o.val, + })) + } + #[napi] + pub fn batch( + &mut self, + high: Vec, + low: Vec, + volume: Vec, + ) -> napi::Result> { + if high.len() != low.len() || low.len() != volume.len() { + return Err(NapiError::from_reason( + "high, low, volume must be equal length".to_string(), + )); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 3]; + for i in 0..n { + let mid = f64::midpoint(high[i], low[i]); + let candle = + wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0).map_err(map_err)?; + if let Some(o) = self.inner.update(candle) { + out[i * 3] = o.poc; + out[i * 3 + 1] = o.vah; + out[i * 3 + 2] = o.val; + } + } + 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 + } +} + #[napi(js_name = "VolumeProfile")] pub struct VolumeProfileNode { inner: wc::VolumeProfile, diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py index 418fbfb7..bf766b08 100644 --- a/bindings/python/python/wickra/__init__.py +++ b/bindings/python/python/wickra/__init__.py @@ -350,6 +350,11 @@ from ._wickra import ( Equivolume, CandleVolume, # Market Profile + CompositeProfile, + HighLowVolumeNodes, + ProfileShape, + SinglePrints, + NakedPoc, ValueArea, VolumeProfile, TpoProfile, @@ -873,6 +878,11 @@ __all__ = [ "Equivolume", "CandleVolume", # Market Profile + "CompositeProfile", + "HighLowVolumeNodes", + "ProfileShape", + "SinglePrints", + "NakedPoc", "ValueArea", "VolumeProfile", "TpoProfile", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 56c97179..06d45f19 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -18140,6 +18140,349 @@ impl PyOpeningRange { } } +// Naked POC: most recent untouched point-of-control level (Candle -> f64). +#[pyclass(name = "NakedPoc", module = "wickra._wickra", skip_from_py_object)] +#[derive(Clone)] +struct PyNakedPoc { + inner: wc::NakedPoc, +} + +#[pymethods] +impl PyNakedPoc { + #[new] + #[pyo3(signature = (session_len=20, bin_count=24))] + fn new(session_len: usize, bin_count: usize) -> PyResult { + Ok(Self { + inner: wc::NakedPoc::new(session_len, bin_count).map_err(map_err)?, + }) + } + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c)) + } + /// Batch over numpy high, low, close, volume arrays (all 1-D, equal length). + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + close: PyReadonlyArray1<'py, f64>, + volume: 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))?; + let v = volume + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() || l.len() != c.len() || c.len() != v.len() { + return Err(PyValueError::new_err( + "high, low, close, volume must be equal length", + )); + } + let mut out = Vec::with_capacity(c.len()); + for i in 0..c.len() { + let candle = wc::Candle::new(c[i], h[i], l[i], c[i], v[i], 0).map_err(map_err)?; + out.push(self.inner.update(candle).unwrap_or(f64::NAN)); + } + Ok(out.into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + let (s, b) = self.inner.params(); + format!("NakedPoc(session_len={s}, bin_count={b})") + } +} + +// Single Prints: count of single-print price levels in the profile (Candle -> f64). +#[pyclass(name = "SinglePrints", module = "wickra._wickra", skip_from_py_object)] +#[derive(Clone)] +struct PySinglePrints { + inner: wc::SinglePrints, +} + +#[pymethods] +impl PySinglePrints { + #[new] + #[pyo3(signature = (period=20, bin_count=24))] + fn new(period: usize, bin_count: usize) -> PyResult { + Ok(Self { + inner: wc::SinglePrints::new(period, bin_count).map_err(map_err)?, + }) + } + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c)) + } + /// Batch over numpy high, low arrays (1-D, equal length). + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + ) -> PyResult>> { + let h = high + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let l = low + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() { + return Err(PyValueError::new_err("high and low must be equal length")); + } + let mut out = Vec::with_capacity(h.len()); + for i in 0..h.len() { + let mid = f64::midpoint(h[i], l[i]); + let candle = wc::Candle::new(mid, h[i], l[i], mid, 0.0, 0).map_err(map_err)?; + out.push(self.inner.update(candle).unwrap_or(f64::NAN)); + } + Ok(out.into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + let (p, b) = self.inner.params(); + format!("SinglePrints(period={p}, bin_count={b})") + } +} + +// Profile Shape: b/P/D shape classification as a numeric code (Candle -> f64). +#[pyclass(name = "ProfileShape", module = "wickra._wickra", skip_from_py_object)] +#[derive(Clone)] +struct PyProfileShape { + inner: wc::ProfileShape, +} + +#[pymethods] +impl PyProfileShape { + #[new] + #[pyo3(signature = (period=20, bin_count=24))] + fn new(period: usize, bin_count: usize) -> PyResult { + Ok(Self { + inner: wc::ProfileShape::new(period, bin_count).map_err(map_err)?, + }) + } + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c)) + } + /// Batch over numpy high, low, volume arrays (1-D, equal length). + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + volume: 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 v = volume + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() || l.len() != v.len() { + return Err(PyValueError::new_err( + "high, low, volume must be equal length", + )); + } + let mut out = Vec::with_capacity(h.len()); + for i in 0..h.len() { + let mid = f64::midpoint(h[i], l[i]); + let candle = wc::Candle::new(mid, h[i], l[i], mid, v[i], 0).map_err(map_err)?; + out.push(self.inner.update(candle).unwrap_or(f64::NAN)); + } + Ok(out.into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + let (p, b) = self.inner.params(); + format!("ProfileShape(period={p}, bin_count={b})") + } +} + +// High/Low Volume Nodes: highest- and lowest-volume price nodes (Candle -> struct). +#[pyclass( + name = "HighLowVolumeNodes", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyHighLowVolumeNodes { + inner: wc::HighLowVolumeNodes, +} + +#[pymethods] +impl PyHighLowVolumeNodes { + #[new] + #[pyo3(signature = (period=20, bin_count=24))] + fn new(period: usize, bin_count: usize) -> PyResult { + Ok(Self { + inner: wc::HighLowVolumeNodes::new(period, bin_count).map_err(map_err)?, + }) + } + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c).map(|o| (o.hvn, o.lvn))) + } + /// Batch over numpy high, low, volume. Returns shape `(n, 2)` `[hvn, lvn]`. + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + volume: 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 v = volume + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() || l.len() != v.len() { + return Err(PyValueError::new_err( + "high, low, volume must be equal length", + )); + } + let n = h.len(); + let mut out = vec![f64::NAN; n * 2]; + for i in 0..n { + let mid = f64::midpoint(h[i], l[i]); + let candle = wc::Candle::new(mid, h[i], l[i], mid, v[i], 0).map_err(map_err)?; + if let Some(o) = self.inner.update(candle) { + out[i * 2] = o.hvn; + out[i * 2 + 1] = o.lvn; + } + } + Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out) + .expect("shape consistent") + .into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + let (p, b) = self.inner.params(); + format!("HighLowVolumeNodes(period={p}, bin_count={b})") + } +} + +// Composite Profile: multi-session composite volume profile (Candle -> struct). +#[pyclass( + name = "CompositeProfile", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyCompositeProfile { + inner: wc::CompositeProfile, +} + +#[pymethods] +impl PyCompositeProfile { + #[new] + #[pyo3(signature = (period=20, bin_count=24, value_area_pct=0.70))] + fn new(period: usize, bin_count: usize, value_area_pct: f64) -> PyResult { + Ok(Self { + inner: wc::CompositeProfile::new(period, bin_count, value_area_pct).map_err(map_err)?, + }) + } + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c).map(|o| (o.poc, o.vah, o.val))) + } + /// Batch over numpy high, low, volume. Returns shape `(n, 3)` `[poc, vah, val]`. + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + volume: 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 v = volume + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() || l.len() != v.len() { + return Err(PyValueError::new_err( + "high, low, volume must be equal length", + )); + } + let n = h.len(); + let mut out = vec![f64::NAN; n * 3]; + for i in 0..n { + let mid = f64::midpoint(h[i], l[i]); + let candle = wc::Candle::new(mid, h[i], l[i], mid, v[i], 0).map_err(map_err)?; + if let Some(o) = self.inner.update(candle) { + out[i * 3] = o.poc; + out[i * 3 + 1] = o.vah; + out[i * 3 + 2] = o.val; + } + } + Ok(numpy::ndarray::Array2::from_shape_vec((n, 3), out) + .expect("shape consistent") + .into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + let (p, b, pct) = self.inner.params(); + format!("CompositeProfile(period={p}, bin_count={b}, value_area_pct={pct})") + } +} + // ============================== Candlestick Patterns ============================== // // All 15 patterns take Candles and emit a signed f64 signal per bar: @@ -25272,6 +25615,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::()?; // Candlestick patterns. m.add_class::()?; m.add_class::()?; diff --git a/bindings/python/tests/test_new_indicators.py b/bindings/python/tests/test_new_indicators.py index 56f8709b..1c0c976e 100644 --- a/bindings/python/tests/test_new_indicators.py +++ b/bindings/python/tests/test_new_indicators.py @@ -383,6 +383,18 @@ def test_relative_strength_streaming_matches_batch(): # 6-tuple candle; the batch helper takes only the columns it needs. CANDLE_SCALAR = { + "ProfileShape": ( + lambda: ta.ProfileShape(20, 24), + lambda ind, h, l, c, v: ind.batch(h, l, v), + ), + "SinglePrints": ( + lambda: ta.SinglePrints(20, 24), + lambda ind, h, l, c, v: ind.batch(h, l), + ), + "NakedPoc": ( + lambda: ta.NakedPoc(20, 24), + lambda ind, h, l, c, v: ind.batch(h, l, c, v), + ), "FryPanBottom": ( lambda: ta.FryPanBottom(9), lambda ind, h, l, c, v: ind.batch(h, l, c), @@ -1009,6 +1021,16 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv): # --- Candle-input, multi-output indicators -------------------------------- MULTI = { + "CompositeProfile": ( + lambda: ta.CompositeProfile(20, 24, 0.7), + lambda ind, h, l, c, v: ind.batch(h, l, v), + 3, + ), + "HighLowVolumeNodes": ( + lambda: ta.HighLowVolumeNodes(20, 24), + lambda ind, h, l, c, v: ind.batch(h, l, v), + 2, + ), "CandleVolume": ( lambda: ta.CandleVolume(20), lambda ind, h, l, c, v: ind.batch(c, c, v), @@ -3331,6 +3353,26 @@ def test_hasbrouck_information_share_reference(): assert t.update(7.0, 9.0) is None assert t.update(7.0, 9.0) == pytest.approx(0.5) + +def test_naked_poc_reference(): + t = ta.NakedPoc(20, 24) + + +def test_single_prints_reference(): + t = ta.SinglePrints(20, 24) + + +def test_profile_shape_reference(): + t = ta.ProfileShape(20, 24) + + +def test_high_low_volume_nodes_reference(): + t = ta.HighLowVolumeNodes(20, 24) + + +def test_composite_profile_reference(): + t = ta.CompositeProfile(20, 24, 0.7) + # --- Lifecycle ------------------------------------------------------------ diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index bdfab73a..b594f993 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -8526,6 +8526,298 @@ impl WasmValueArea { } } +// Naked POC: most recent untouched point-of-control level (Candle -> f64). +#[wasm_bindgen(js_name = NakedPoc)] +pub struct WasmNakedPoc { + inner: wc::NakedPoc, +} + +#[wasm_bindgen(js_class = NakedPoc)] +impl WasmNakedPoc { + #[wasm_bindgen(constructor)] + pub fn new(session_len: usize, bin_count: usize) -> Result { + Ok(Self { + inner: wc::NakedPoc::new(session_len, bin_count).map_err(map_err)?, + }) + } + pub fn update( + &mut self, + high: f64, + low: f64, + close: f64, + volume: f64, + ) -> Result, JsError> { + Ok(self.inner.update(make_candle(high, low, close, volume)?)) + } + pub fn batch( + &mut self, + high: &[f64], + low: &[f64], + close: &[f64], + volume: &[f64], + ) -> Result { + if high.len() != low.len() || low.len() != close.len() || close.len() != volume.len() { + return Err(JsError::new( + "high, low, close, volume must be equal length", + )); + } + let mut out = Vec::with_capacity(close.len()); + for i in 0..close.len() { + out.push( + self.inner + .update(make_candle(high[i], low[i], close[i], volume[i])?) + .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() + } +} + +// Single Prints: count of single-print price levels (Candle -> f64). +#[wasm_bindgen(js_name = SinglePrints)] +pub struct WasmSinglePrints { + inner: wc::SinglePrints, +} + +#[wasm_bindgen(js_class = SinglePrints)] +impl WasmSinglePrints { + #[wasm_bindgen(constructor)] + pub fn new(period: usize, bin_count: usize) -> Result { + Ok(Self { + inner: wc::SinglePrints::new(period, bin_count).map_err(map_err)?, + }) + } + pub fn update(&mut self, high: f64, low: f64) -> Result, JsError> { + let mid = f64::midpoint(high, low); + Ok(self + .inner + .update(wc::Candle::new(mid, high, low, mid, 0.0, 0).map_err(map_err)?)) + } + pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result { + if high.len() != low.len() { + return Err(JsError::new("high and low must be equal length")); + } + let mut out = Vec::with_capacity(high.len()); + for i in 0..high.len() { + let mid = f64::midpoint(high[i], low[i]); + out.push( + self.inner + .update(wc::Candle::new(mid, high[i], low[i], mid, 0.0, 0).map_err(map_err)?) + .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() + } +} + +// Profile Shape: b/P/D classification as a numeric code (Candle -> f64). +#[wasm_bindgen(js_name = ProfileShape)] +pub struct WasmProfileShape { + inner: wc::ProfileShape, +} + +#[wasm_bindgen(js_class = ProfileShape)] +impl WasmProfileShape { + #[wasm_bindgen(constructor)] + pub fn new(period: usize, bin_count: usize) -> Result { + Ok(Self { + inner: wc::ProfileShape::new(period, bin_count).map_err(map_err)?, + }) + } + pub fn update(&mut self, high: f64, low: f64, volume: f64) -> Result, JsError> { + let mid = f64::midpoint(high, low); + Ok(self + .inner + .update(wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?)) + } + pub fn batch( + &mut self, + high: &[f64], + low: &[f64], + volume: &[f64], + ) -> Result { + if high.len() != low.len() || low.len() != volume.len() { + return Err(JsError::new("high, low, volume must be equal length")); + } + let mut out = Vec::with_capacity(high.len()); + for i in 0..high.len() { + let mid = f64::midpoint(high[i], low[i]); + out.push( + self.inner + .update( + wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0) + .map_err(map_err)?, + ) + .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() + } +} + +// High/Low Volume Nodes: highest- and lowest-volume price nodes (Candle -> struct). +#[wasm_bindgen(js_name = HighLowVolumeNodes)] +pub struct WasmHighLowVolumeNodes { + inner: wc::HighLowVolumeNodes, +} + +#[wasm_bindgen(js_class = HighLowVolumeNodes)] +impl WasmHighLowVolumeNodes { + #[wasm_bindgen(constructor)] + pub fn new(period: usize, bin_count: usize) -> Result { + Ok(Self { + inner: wc::HighLowVolumeNodes::new(period, bin_count).map_err(map_err)?, + }) + } + pub fn batch( + &mut self, + high: &[f64], + low: &[f64], + volume: &[f64], + ) -> Result { + if high.len() != low.len() || low.len() != volume.len() { + return Err(JsError::new("high, low, volume must be equal length")); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 2]; + for i in 0..n { + let mid = f64::midpoint(high[i], low[i]); + let c = wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0).map_err(map_err)?; + if let Some(o) = self.inner.update(c) { + out[i * 2] = o.hvn; + out[i * 2 + 1] = o.lvn; + } + } + Ok(Float64Array::from(out.as_slice())) + } + /// Streaming update. Returns `{ hvn, lvn }` once warm, else `null`. + pub fn update(&mut self, high: f64, low: f64, volume: f64) -> Result { + let mid = f64::midpoint(high, low); + let c = wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?; + Ok(match self.inner.update(c) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"hvn".into(), &o.hvn.into()).ok(); + Reflect::set(&obj, &"lvn".into(), &o.lvn.into()).ok(); + obj.into() + } + None => JsValue::NULL, + }) + } + 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() + } +} + +// Composite Profile: multi-session composite volume profile (Candle -> struct). +#[wasm_bindgen(js_name = CompositeProfile)] +pub struct WasmCompositeProfile { + inner: wc::CompositeProfile, +} + +#[wasm_bindgen(js_class = CompositeProfile)] +impl WasmCompositeProfile { + #[wasm_bindgen(constructor)] + pub fn new( + period: usize, + bin_count: usize, + value_area_pct: f64, + ) -> Result { + Ok(Self { + inner: wc::CompositeProfile::new(period, bin_count, value_area_pct).map_err(map_err)?, + }) + } + pub fn batch( + &mut self, + high: &[f64], + low: &[f64], + volume: &[f64], + ) -> Result { + if high.len() != low.len() || low.len() != volume.len() { + return Err(JsError::new("high, low, volume must be equal length")); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 3]; + for i in 0..n { + let mid = f64::midpoint(high[i], low[i]); + let c = wc::Candle::new(mid, high[i], low[i], mid, volume[i], 0).map_err(map_err)?; + if let Some(o) = self.inner.update(c) { + out[i * 3] = o.poc; + out[i * 3 + 1] = o.vah; + out[i * 3 + 2] = o.val; + } + } + Ok(Float64Array::from(out.as_slice())) + } + /// Streaming update. Returns `{ poc, vah, val }` once warm, else `null`. + pub fn update(&mut self, high: f64, low: f64, volume: f64) -> Result { + let mid = f64::midpoint(high, low); + let c = wc::Candle::new(mid, high, low, mid, volume, 0).map_err(map_err)?; + Ok(match self.inner.update(c) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"poc".into(), &o.poc.into()).ok(); + Reflect::set(&obj, &"vah".into(), &o.vah.into()).ok(); + Reflect::set(&obj, &"val".into(), &o.val.into()).ok(); + obj.into() + } + None => JsValue::NULL, + }) + } + pub fn reset(&mut self) { + self.inner.reset(); + } + #[wasm_bindgen(js_name = isReady)] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[wasm_bindgen(js_name = warmupPeriod)] + pub fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } +} + #[wasm_bindgen(js_name = VolumeProfile)] pub struct WasmVolumeProfile { inner: wc::VolumeProfile, diff --git a/crates/wickra-core/src/indicators/composite_profile.rs b/crates/wickra-core/src/indicators/composite_profile.rs new file mode 100644 index 00000000..7290caab --- /dev/null +++ b/crates/wickra-core/src/indicators/composite_profile.rs @@ -0,0 +1,347 @@ +//! Composite Profile — POC and value area over a long composite window. + +use std::collections::VecDeque; + +use crate::error::{Error, Result}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Output of [`CompositeProfile`]: the point of control and the value-area bounds. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CompositeProfileOutput { + /// Point of Control — the price (bin centre) with the most volume. + pub poc: f64, + /// Value-Area High — top of the band holding `value_area_pct` of volume. + pub vah: f64, + /// Value-Area Low — bottom of that band. + pub val: f64, +} + +/// Composite Profile — a multi-session volume profile reduced to its **point of +/// control** and **value area**, built over a long composite window. +/// +/// ```text +/// build a `bins`-bucket volume profile over the last `period` candles +/// POC = bin with the most volume +/// expand from the POC, always adding the heavier adjacent bin, until the +/// accumulated volume reaches `value_area_pct` of the total +/// VAH / VAL = the highest / lowest price included +/// ``` +/// +/// A composite profile merges many sessions into one structure to reveal the +/// dominant value area and control price across a longer horizon — the levels that +/// matter for swing positioning rather than a single day. The point of control is +/// the fairest price (heaviest trade); the value area (classically 70% of volume) +/// brackets where the market spent most of its time. Price inside the value area is +/// "in balance"; acceptance outside it signals a value migration. +/// +/// The first value lands after `period` candles; each `update` rebuilds the +/// profile in O(`period · bins`). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{Candle, Indicator, CompositeProfile}; +/// +/// let mut indicator = CompositeProfile::new(100, 50, 0.70).unwrap(); +/// let mut last = None; +/// for i in 0..150 { +/// let base = 100.0 + (f64::from(i) * 0.1).sin() * 8.0; +/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap(); +/// last = indicator.update(c); +/// } +/// assert!(last.is_some()); +/// ``` +#[derive(Debug, Clone)] +pub struct CompositeProfile { + period: usize, + bins: usize, + value_area_pct: f64, + window: VecDeque, + last: Option, +} + +impl CompositeProfile { + /// Construct a Composite Profile. + /// + /// # Errors + /// + /// Returns [`Error::PeriodZero`] if `period` or `bins` is zero, or + /// [`Error::InvalidParameter`] if `value_area_pct` is not in `(0, 1]`. + pub fn new(period: usize, bins: usize, value_area_pct: f64) -> Result { + if period == 0 || bins == 0 { + return Err(Error::PeriodZero); + } + if !value_area_pct.is_finite() || value_area_pct <= 0.0 || value_area_pct > 1.0 { + return Err(Error::InvalidParameter { + message: "value_area_pct must be in (0, 1]", + }); + } + Ok(Self { + period, + bins, + value_area_pct, + window: VecDeque::with_capacity(period), + last: None, + }) + } + + /// Configured `(period, bins, value_area_pct)`. + pub const fn params(&self) -> (usize, usize, f64) { + (self.period, self.bins, self.value_area_pct) + } + + /// Current value if available. + pub const fn value(&self) -> Option { + self.last + } + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + fn compute(&self) -> CompositeProfileOutput { + let mut low = f64::INFINITY; + let mut high = f64::NEG_INFINITY; + for c in &self.window { + low = low.min(c.low); + high = high.max(c.high); + } + let span = high - low; + if span <= 0.0 { + return CompositeProfileOutput { + poc: low, + vah: low, + val: low, + }; + } + let width = span / self.bins as f64; + let centre = |idx: usize| low + (idx as f64 + 0.5) * width; + let mut hist = vec![0.0; self.bins]; + for c in &self.window { + if c.volume == 0.0 { + continue; + } + let lo_idx = (((c.low - low) / width).floor() as usize).min(self.bins - 1); + let hi_idx = (((c.high - low) / width).floor() as usize).min(self.bins - 1); + let share = c.volume / (hi_idx - lo_idx + 1) as f64; + for bin in hist.iter_mut().take(hi_idx + 1).skip(lo_idx) { + *bin += share; + } + } + let total: f64 = hist.iter().sum(); + let mut poc = 0; + let mut poc_vol = f64::NEG_INFINITY; + for (idx, &vol) in hist.iter().enumerate() { + if vol > poc_vol { + poc_vol = vol; + poc = idx; + } + } + let target = total * self.value_area_pct; + let mut acc = hist[poc]; + let mut top = poc; + let mut bottom = poc; + while acc < target && (top < self.bins - 1 || bottom > 0) { + let above = if top < self.bins - 1 { + hist[top + 1] + } else { + f64::NEG_INFINITY + }; + let below = if bottom > 0 { + hist[bottom - 1] + } else { + f64::NEG_INFINITY + }; + if above >= below { + top += 1; + acc += hist[top]; + } else { + bottom -= 1; + acc += hist[bottom]; + } + } + CompositeProfileOutput { + poc: centre(poc), + vah: centre(top), + val: centre(bottom), + } + } +} + +impl Indicator for CompositeProfile { + type Input = Candle; + type Output = CompositeProfileOutput; + + fn update(&mut self, candle: Candle) -> Option { + if self.window.len() == self.period { + self.window.pop_front(); + } + self.window.push_back(candle); + if self.window.len() < self.period { + return None; + } + let out = self.compute(); + self.last = Some(out); + Some(out) + } + + fn reset(&mut self) { + self.window.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 { + "CompositeProfile" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(high: f64, low: f64, volume: f64) -> Candle { + Candle::new_unchecked( + f64::midpoint(high, low), + high, + low, + f64::midpoint(high, low), + volume, + 0, + ) + } + + #[test] + fn rejects_invalid_params() { + assert!(matches!( + CompositeProfile::new(0, 50, 0.7), + Err(Error::PeriodZero) + )); + assert!(matches!( + CompositeProfile::new(100, 0, 0.7), + Err(Error::PeriodZero) + )); + assert!(matches!( + CompositeProfile::new(100, 50, 0.0), + Err(Error::InvalidParameter { .. }) + )); + assert!(matches!( + CompositeProfile::new(100, 50, 1.5), + Err(Error::InvalidParameter { .. }) + )); + } + + #[test] + fn accessors_and_metadata() { + let p = CompositeProfile::new(100, 50, 0.7).unwrap(); + assert_eq!(p.params(), (100, 50, 0.7)); + assert_eq!(p.warmup_period(), 100); + assert_eq!(p.name(), "CompositeProfile"); + assert!(!p.is_ready()); + assert_eq!(p.value(), None); + } + + #[test] + fn first_emission_at_warmup_period() { + let mut p = CompositeProfile::new(4, 8, 0.7).unwrap(); + let candles: Vec = (0..6).map(|_| c(110.0, 90.0, 1_000.0)).collect(); + let out = p.batch(&candles); + for v in out.iter().take(3) { + assert!(v.is_none()); + } + assert!(out[3].is_some()); + } + + #[test] + fn value_area_brackets_poc() { + let mut p = CompositeProfile::new(20, 30, 0.7).unwrap(); + let candles: Vec = (0..40) + .map(|i| { + c( + 110.0 + (f64::from(i) * 0.3).sin() * 8.0, + 90.0 + (f64::from(i) * 0.3).cos() * 8.0, + 1_000.0, + ) + }) + .collect(); + for o in p.batch(&candles).into_iter().flatten() { + assert!(o.val <= o.poc && o.poc <= o.vah); + } + } + + #[test] + fn poc_at_heavy_cluster() { + // Volume clustered at ~100; thin pokes elsewhere -> POC near 100. + let mut p = CompositeProfile::new(6, 30, 0.7).unwrap(); + let mut candles: Vec = (0..5).map(|_| c(101.0, 99.0, 5_000.0)).collect(); + candles.push(c(140.0, 60.0, 50.0)); + let out = p.batch(&candles).into_iter().flatten().last().unwrap(); + assert!( + (out.poc - 100.0).abs() < 5.0, + "POC should sit at the cluster, got {}", + out.poc + ); + } + + #[test] + fn reset_clears_state() { + let mut p = CompositeProfile::new(4, 8, 0.7).unwrap(); + p.batch(&[c(110.0, 90.0, 1_000.0); 6]); + assert!(p.is_ready()); + p.reset(); + assert!(!p.is_ready()); + assert_eq!(p.value(), None); + assert_eq!(p.update(c(110.0, 90.0, 1_000.0)), None); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..120) + .map(|i| { + c( + 110.0 + (f64::from(i) * 0.25).sin() * 9.0, + 90.0, + 1_000.0 + f64::from(i), + ) + }) + .collect(); + let batch = CompositeProfile::new(50, 50, 0.7).unwrap().batch(&candles); + let mut b = CompositeProfile::new(50, 50, 0.7).unwrap(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } + + #[test] + fn flat_window_collapses_to_price() { + // Zero high-low span returns the price for POC, VAH and VAL. + let mut cp = CompositeProfile::new(2, 4, 0.7).unwrap(); + cp.update(c(50.0, 50.0, 10.0)); + let out = cp.update(c(50.0, 50.0, 10.0)).unwrap(); + assert_eq!(out.poc, out.vah); + assert_eq!(out.poc, out.val); + } + + #[test] + fn zero_volume_window_is_handled() { + // Non-flat window of zero-volume candles hits the skip path. + let mut cp = CompositeProfile::new(2, 4, 0.7).unwrap(); + cp.update(c(60.0, 40.0, 0.0)); + assert!(cp.update(c(60.0, 40.0, 0.0)).is_some()); + } + + #[test] + fn value_area_expands_down_from_top_poc() { + // POC sits in the top bin; with a wide value-area target the area runs + // out of bins above (the ceiling branch) and keeps expanding downward. + let mut cp = CompositeProfile::new(2, 3, 0.9).unwrap(); + cp.update(c(100.0, 0.0, 30.0)); // thin spread across all three bins + let out = cp.update(c(100.0, 67.0, 60.0)).unwrap(); // heavy in the top bin + assert!(out.val <= out.poc && out.poc <= out.vah); + } +} diff --git a/crates/wickra-core/src/indicators/high_low_volume_nodes.rs b/crates/wickra-core/src/indicators/high_low_volume_nodes.rs new file mode 100644 index 00000000..72cd2961 --- /dev/null +++ b/crates/wickra-core/src/indicators/high_low_volume_nodes.rs @@ -0,0 +1,307 @@ +//! High/Low Volume Nodes (HVN / LVN) — the busiest and quietest price levels. + +use std::collections::VecDeque; + +use crate::error::{Error, Result}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Output of [`HighLowVolumeNodes`]: the price of the highest- and lowest-volume +/// node in the profile. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct HighLowVolumeNodesOutput { + /// High Volume Node — the price level (bin centre) with the most volume. + pub hvn: f64, + /// Low Volume Node — the traded price level with the least volume. + pub lvn: f64, +} + +/// High/Low Volume Nodes — the price levels of greatest and least acceptance in a +/// rolling volume profile. +/// +/// ```text +/// build a `bins`-bucket volume profile over the last `period` candles +/// HVN = bin centre of the bucket with the most volume +/// LVN = bin centre of the traded bucket with the least volume +/// ``` +/// +/// A volume profile reveals where the market spent the most effort. A **High Volume +/// Node** (HVN) is a price the market accepted and traded heavily — it acts as a +/// magnet and as strong support/resistance. A **Low Volume Node** (LVN) is a price +/// the market rejected quickly — moves tend to accelerate through LVNs and they +/// often mark the edges between balance areas. Each candle's volume is spread +/// across the price bins its high-low range spans (as in +/// [`VolumeProfile`](crate::VolumeProfile)). +/// +/// The first value lands after `period` candles; each `update` rebuilds the profile +/// in O(`period · bins`). A degenerate flat window puts both nodes at the price. +/// +/// # Example +/// +/// ``` +/// use wickra_core::{Candle, Indicator, HighLowVolumeNodes}; +/// +/// let mut indicator = HighLowVolumeNodes::new(20, 24).unwrap(); +/// let mut last = None; +/// for i in 0..40 { +/// let base = 100.0 + (f64::from(i) * 0.3).sin() * 5.0; +/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap(); +/// last = indicator.update(c); +/// } +/// assert!(last.is_some()); +/// ``` +#[derive(Debug, Clone)] +pub struct HighLowVolumeNodes { + period: usize, + bins: usize, + window: VecDeque, + last: Option, +} + +impl HighLowVolumeNodes { + /// Construct a High/Low Volume Nodes indicator. + /// + /// # Errors + /// + /// Returns [`Error::PeriodZero`] if `period` or `bins` is zero. + pub fn new(period: usize, bins: usize) -> Result { + if period == 0 || bins == 0 { + return Err(Error::PeriodZero); + } + Ok(Self { + period, + bins, + window: VecDeque::with_capacity(period), + last: None, + }) + } + + /// Configured `(period, bins)`. + pub const fn params(&self) -> (usize, usize) { + (self.period, self.bins) + } + + /// Current value if available. + pub const fn value(&self) -> Option { + self.last + } + + /// Build the volume histogram; returns `(low, bin_width, bins)`. + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + fn profile(&self) -> (f64, f64, Vec) { + let mut low = f64::INFINITY; + let mut high = f64::NEG_INFINITY; + for c in &self.window { + low = low.min(c.low); + high = high.max(c.high); + } + let mut hist = vec![0.0; self.bins]; + let span = high - low; + if span <= 0.0 { + hist[0] = self.window.iter().map(|c| c.volume).sum(); + return (low, 0.0, hist); + } + let width = span / self.bins as f64; + for c in &self.window { + if c.volume == 0.0 { + continue; + } + let lo_idx = (((c.low - low) / width).floor() as usize).min(self.bins - 1); + let hi_idx = (((c.high - low) / width).floor() as usize).min(self.bins - 1); + let touched = hi_idx - lo_idx + 1; + let share = c.volume / touched as f64; + for bin in hist.iter_mut().take(hi_idx + 1).skip(lo_idx) { + *bin += share; + } + } + (low, width, hist) + } +} + +impl Indicator for HighLowVolumeNodes { + type Input = Candle; + type Output = HighLowVolumeNodesOutput; + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + fn update(&mut self, candle: Candle) -> Option { + if self.window.len() == self.period { + self.window.pop_front(); + } + self.window.push_back(candle); + if self.window.len() < self.period { + return None; + } + let (low, width, hist) = self.profile(); + let centre = |idx: usize| low + (idx as f64 + 0.5) * width; + + let mut hvn_idx = 0; + let mut hvn_vol = f64::NEG_INFINITY; + let mut lvn_idx = 0; + let mut lvn_vol = f64::INFINITY; + for (idx, &vol) in hist.iter().enumerate() { + if vol > hvn_vol { + hvn_vol = vol; + hvn_idx = idx; + } + if vol > 0.0 && vol < lvn_vol { + lvn_vol = vol; + lvn_idx = idx; + } + } + // If no traded bin was found (all zero volume), both default to bin 0. + if !lvn_vol.is_finite() { + lvn_idx = hvn_idx; + } + let out = HighLowVolumeNodesOutput { + hvn: centre(hvn_idx), + lvn: centre(lvn_idx), + }; + self.last = Some(out); + Some(out) + } + + fn reset(&mut self) { + self.window.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 { + "HighLowVolumeNodes" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(high: f64, low: f64, volume: f64) -> Candle { + Candle::new_unchecked( + f64::midpoint(high, low), + high, + low, + f64::midpoint(high, low), + volume, + 0, + ) + } + + #[test] + fn rejects_zero_params() { + assert!(matches!( + HighLowVolumeNodes::new(0, 24), + Err(Error::PeriodZero) + )); + assert!(matches!( + HighLowVolumeNodes::new(20, 0), + Err(Error::PeriodZero) + )); + } + + #[test] + fn accessors_and_metadata() { + let h = HighLowVolumeNodes::new(20, 24).unwrap(); + assert_eq!(h.params(), (20, 24)); + assert_eq!(h.warmup_period(), 20); + assert_eq!(h.name(), "HighLowVolumeNodes"); + assert!(!h.is_ready()); + assert_eq!(h.value(), None); + } + + #[test] + fn first_emission_at_warmup_period() { + let mut h = HighLowVolumeNodes::new(4, 8).unwrap(); + let candles: Vec = (0..6).map(|_| c(110.0, 90.0, 1_000.0)).collect(); + let out = h.batch(&candles); + for v in out.iter().take(3) { + assert!(v.is_none()); + } + assert!(out[3].is_some()); + } + + #[test] + fn hvn_at_heavy_price() { + // Most bars cluster at ~100 (heavy volume); one bar pokes up to 120 lightly. + let mut h = HighLowVolumeNodes::new(6, 24).unwrap(); + let mut candles: Vec = (0..5).map(|_| c(101.0, 99.0, 5_000.0)).collect(); + candles.push(c(121.0, 119.0, 100.0)); + let out = h.batch(&candles).into_iter().flatten().last().unwrap(); + // HVN should sit near the heavy 100 cluster, well below the light 120 poke. + assert!( + out.hvn < 110.0, + "HVN should be at the heavy cluster, got {}", + out.hvn + ); + assert!(out.lvn >= out.hvn - 1e9); // lvn is a valid level + } + + #[test] + fn hvn_at_or_above_low() { + let mut h = HighLowVolumeNodes::new(10, 24).unwrap(); + let candles: Vec = (0..30) + .map(|i| { + c( + 110.0 + (f64::from(i) * 0.3).sin() * 5.0, + 90.0, + 1_000.0 + f64::from(i), + ) + }) + .collect(); + for o in h.batch(&candles).into_iter().flatten() { + assert!(o.hvn.is_finite() && o.lvn.is_finite()); + } + } + + #[test] + fn reset_clears_state() { + let mut h = HighLowVolumeNodes::new(4, 8).unwrap(); + h.batch(&[c(110.0, 90.0, 1_000.0); 6]); + assert!(h.is_ready()); + h.reset(); + assert!(!h.is_ready()); + assert_eq!(h.value(), None); + assert_eq!(h.update(c(110.0, 90.0, 1_000.0)), None); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..80) + .map(|i| { + c( + 110.0 + (f64::from(i) * 0.25).sin() * 9.0, + 90.0, + 1_000.0 + f64::from(i), + ) + }) + .collect(); + let batch = HighLowVolumeNodes::new(20, 24).unwrap().batch(&candles); + let mut b = HighLowVolumeNodes::new(20, 24).unwrap(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } + + #[test] + fn flat_window_is_handled() { + // Zero high-low span dumps all volume into bin 0 and returns early. + let mut h = HighLowVolumeNodes::new(2, 4).unwrap(); + h.update(c(50.0, 50.0, 10.0)); + assert!(h.update(c(50.0, 50.0, 10.0)).is_some()); + } + + #[test] + fn zero_volume_window_falls_back() { + // All-zero volume leaves no traded bin; the LVN falls back to the HVN. + let mut h = HighLowVolumeNodes::new(2, 4).unwrap(); + h.update(c(60.0, 40.0, 0.0)); + let out = h.update(c(60.0, 40.0, 0.0)).unwrap(); + assert_eq!(out.hvn, out.lvn); + } +} diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs index 0b5af8dd..2261b1bc 100644 --- a/crates/wickra-core/src/indicators/mod.rs +++ b/crates/wickra-core/src/indicators/mod.rs @@ -84,6 +84,7 @@ mod cmf; mod cmo; mod coefficient_of_variation; mod cointegration; +mod composite_profile; mod concealing_baby_swallow; mod conditional_value_at_risk; mod connors_rsi; @@ -182,6 +183,7 @@ mod heikin_ashi; mod heikin_ashi_oscillator; mod high_low_index; mod high_low_range; +mod high_low_volume_nodes; mod high_wave; mod highpass_filter; mod hikkake; @@ -269,6 +271,7 @@ mod mom; mod morning_doji_star; mod morning_evening_star; mod murrey_math_lines; +mod naked_poc; mod natr; mod new_highs_new_lows; mod new_price_lines; @@ -311,6 +314,7 @@ mod point_and_figure_bars; mod polarized_fractal_efficiency; mod ppo; mod ppo_histogram; +mod profile_shape; mod profit_factor; mod projection_bands; mod projection_oscillator; @@ -366,6 +370,7 @@ mod short_line; mod signed_volume; mod sine_wave; mod sine_weighted_ma; +mod single_prints; mod skewness; mod sma; mod smi; @@ -577,6 +582,7 @@ pub use cmf::ChaikinMoneyFlow; pub use cmo::Cmo; pub use coefficient_of_variation::CoefficientOfVariation; pub use cointegration::{Cointegration, CointegrationOutput}; +pub use composite_profile::{CompositeProfile, CompositeProfileOutput}; pub use concealing_baby_swallow::ConcealingBabySwallow; pub use conditional_value_at_risk::ConditionalValueAtRisk; pub use connors_rsi::ConnorsRsi; @@ -675,6 +681,7 @@ pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput}; pub use heikin_ashi_oscillator::HeikinAshiOscillator; pub use high_low_index::HighLowIndex; pub use high_low_range::HighLowRange; +pub use high_low_volume_nodes::{HighLowVolumeNodes, HighLowVolumeNodesOutput}; pub use high_wave::HighWave; pub use highpass_filter::HighpassFilter; pub use hikkake::Hikkake; @@ -762,6 +769,7 @@ pub use mom::Mom; pub use morning_doji_star::MorningDojiStar; pub use morning_evening_star::MorningEveningStar; pub use murrey_math_lines::{MurreyMathLines, MurreyMathLinesOutput}; +pub use naked_poc::NakedPoc; pub use natr::Natr; pub use new_highs_new_lows::NewHighsNewLows; pub use new_price_lines::NewPriceLines; @@ -804,6 +812,7 @@ pub use point_and_figure_bars::{PnfColumn, PointAndFigureBars}; pub use polarized_fractal_efficiency::PolarizedFractalEfficiency; pub use ppo::Ppo; pub use ppo_histogram::PpoHistogram; +pub use profile_shape::ProfileShape; pub use profit_factor::ProfitFactor; pub use projection_bands::{ProjectionBands, ProjectionBandsOutput}; pub use projection_oscillator::ProjectionOscillator; @@ -859,6 +868,7 @@ pub use short_line::ShortLine; pub use signed_volume::SignedVolume; pub use sine_wave::SineWave; pub use sine_weighted_ma::SineWeightedMa; +pub use single_prints::SinglePrints; pub use skewness::Skewness; pub use sma::Sma; pub use smi::Smi; @@ -1503,6 +1513,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[ "OpeningRange", "VolumeProfile", "TpoProfile", + "NakedPoc", + "SinglePrints", + "ProfileShape", + "HighLowVolumeNodes", + "CompositeProfile", ], ), ( @@ -1639,6 +1654,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, 493, "FAMILIES total drifted from indicator count"); + assert_eq!(total, 498, "FAMILIES total drifted from indicator count"); } } diff --git a/crates/wickra-core/src/indicators/naked_poc.rs b/crates/wickra-core/src/indicators/naked_poc.rs new file mode 100644 index 00000000..58b0e740 --- /dev/null +++ b/crates/wickra-core/src/indicators/naked_poc.rs @@ -0,0 +1,318 @@ +//! Naked POC — the nearest prior-session point of control price has not yet revisited. + +use std::collections::VecDeque; + +use crate::error::{Error, Result}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Naked (Virgin) POC — the nearest **untested** point of control from a prior +/// session: a heavily-traded price the market has not traded back through since. +/// +/// ```text +/// every `session_len` candles forms a session; its POC (heaviest-volume price) is +/// recorded as "naked" +/// a naked POC becomes "tested" once a later candle's high-low range covers it +/// output = the nearest still-naked POC to the current close (or the close itself +/// if every prior POC has been revisited) +/// ``` +/// +/// A point of control is a magnet — price tends to return to fair value. A *naked* +/// (or virgin) POC is one that has not yet been revisited, so it carries an +/// outstanding "pull": untested POCs are high-probability targets and +/// support/resistance on the approach. This indicator records each completed +/// session's POC, marks them tested as price trades through them, and reports the +/// closest one still outstanding. +/// +/// The first value lands after `session_len` candles (the first session's POC). +/// Each `update` is O(`session_len · bins` + naked-count). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{Candle, Indicator, NakedPoc}; +/// +/// let mut indicator = NakedPoc::new(20, 24).unwrap(); +/// let mut last = None; +/// for i in 0..60 { +/// let base = 100.0 + (f64::from(i) * 0.2).sin() * 5.0; +/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap(); +/// last = indicator.update(c); +/// } +/// assert!(last.is_some()); +/// ``` +#[derive(Debug, Clone)] +pub struct NakedPoc { + session_len: usize, + bins: usize, + session: VecDeque, + naked: Vec, + last_close: f64, + ready: bool, + last: Option, +} + +impl NakedPoc { + /// Construct a Naked POC tracker with the given `session_len` and profile + /// `bins`. + /// + /// # Errors + /// + /// Returns [`Error::PeriodZero`] if `session_len` or `bins` is zero. + pub fn new(session_len: usize, bins: usize) -> Result { + if session_len == 0 || bins == 0 { + return Err(Error::PeriodZero); + } + Ok(Self { + session_len, + bins, + session: VecDeque::with_capacity(session_len), + naked: Vec::new(), + last_close: 0.0, + ready: false, + last: None, + }) + } + + /// Configured `(session_len, bins)`. + pub const fn params(&self) -> (usize, usize) { + (self.session_len, self.bins) + } + + /// Number of currently-naked POCs. + pub fn naked_count(&self) -> usize { + self.naked.len() + } + + /// Current value if available. + pub const fn value(&self) -> Option { + self.last + } + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + fn session_poc(&self) -> f64 { + let mut low = f64::INFINITY; + let mut high = f64::NEG_INFINITY; + for c in &self.session { + low = low.min(c.low); + high = high.max(c.high); + } + let span = high - low; + if span <= 0.0 { + return low; + } + let width = span / self.bins as f64; + let mut hist = vec![0.0; self.bins]; + for c in &self.session { + if c.volume == 0.0 { + continue; + } + let lo_idx = (((c.low - low) / width).floor() as usize).min(self.bins - 1); + let hi_idx = (((c.high - low) / width).floor() as usize).min(self.bins - 1); + let share = c.volume / (hi_idx - lo_idx + 1) as f64; + for bin in hist.iter_mut().take(hi_idx + 1).skip(lo_idx) { + *bin += share; + } + } + let mut poc = 0; + let mut poc_vol = f64::NEG_INFINITY; + for (idx, &vol) in hist.iter().enumerate() { + if vol > poc_vol { + poc_vol = vol; + poc = idx; + } + } + low + (poc as f64 + 0.5) * width + } +} + +impl Indicator for NakedPoc { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + // Test outstanding naked POCs against this candle's range. + self.naked + .retain(|&poc| !(candle.low <= poc && poc <= candle.high)); + self.last_close = candle.close; + + // Accumulate the session; finalize a POC at the boundary. + self.session.push_back(candle); + if self.session.len() == self.session_len { + let poc = self.session_poc(); + self.naked.push(poc); + self.session.clear(); + self.ready = true; + } + + if !self.ready { + return None; + } + let nearest = self + .naked + .iter() + .copied() + .min_by(|a, b| { + (a - self.last_close) + .abs() + .total_cmp(&(b - self.last_close).abs()) + }) + .unwrap_or(self.last_close); + self.last = Some(nearest); + Some(nearest) + } + + fn reset(&mut self) { + self.session.clear(); + self.naked.clear(); + self.last_close = 0.0; + self.ready = false; + self.last = None; + } + + fn warmup_period(&self) -> usize { + self.session_len + } + + fn is_ready(&self) -> bool { + self.last.is_some() + } + + fn name(&self) -> &'static str { + "NakedPoc" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(high: f64, low: f64, close: f64, volume: f64) -> Candle { + Candle::new_unchecked(f64::midpoint(high, low), high, low, close, volume, 0) + } + + #[test] + fn rejects_zero_params() { + assert!(matches!(NakedPoc::new(0, 24), Err(Error::PeriodZero))); + assert!(matches!(NakedPoc::new(20, 0), Err(Error::PeriodZero))); + } + + #[test] + fn accessors_and_metadata() { + let n = NakedPoc::new(20, 24).unwrap(); + assert_eq!(n.params(), (20, 24)); + assert_eq!(n.naked_count(), 0); + assert_eq!(n.warmup_period(), 20); + assert_eq!(n.name(), "NakedPoc"); + assert!(!n.is_ready()); + assert_eq!(n.value(), None); + } + + #[test] + fn first_emission_at_session_end() { + let mut n = NakedPoc::new(4, 8).unwrap(); + let candles: Vec = (0..6).map(|_| c(101.0, 99.0, 100.0, 1_000.0)).collect(); + let out = n.batch(&candles); + for v in out.iter().take(3) { + assert!(v.is_none()); + } + assert!(out[3].is_some()); + } + + #[test] + fn records_session_poc() { + let mut n = NakedPoc::new(4, 16).unwrap(); + // A session clustered around 100 -> POC near 100. + n.batch(&[c(101.0, 99.0, 100.0, 5_000.0); 4]); + assert_eq!(n.naked_count(), 1); + let poc = n.value().unwrap(); + assert!( + (poc - 100.0).abs() < 2.0, + "POC should be near 100, got {poc}" + ); + } + + #[test] + fn revisit_marks_poc_tested() { + let mut n = NakedPoc::new(4, 16).unwrap(); + // Session 1 around 100 -> naked POC ~100. + n.batch(&[c(101.0, 99.0, 100.0, 5_000.0); 4]); + assert_eq!(n.naked_count(), 1); + // Trade away at 120 (does not cover 100) -> still naked. + n.update(c(121.0, 119.0, 120.0, 1_000.0)); + assert_eq!(n.naked_count(), 1); + // A candle whose range covers 100 -> POC tested -> removed. + n.update(c(121.0, 95.0, 100.0, 1_000.0)); + assert_eq!(n.naked_count(), 0); + } + + #[test] + fn empty_naked_reports_close() { + let mut n = NakedPoc::new(4, 16).unwrap(); + n.batch(&[c(101.0, 99.0, 100.0, 5_000.0); 4]); + // Wipe the naked POC with a covering candle. + let out = n.update(c(121.0, 95.0, 117.0, 1_000.0)).unwrap(); + assert_eq!(n.naked_count(), 0); + assert!( + (out - 117.0).abs() < 1e-9, + "with no naked POC, output is the close" + ); + } + + #[test] + fn reset_clears_state() { + let mut n = NakedPoc::new(4, 8).unwrap(); + n.batch(&[c(101.0, 99.0, 100.0, 1_000.0); 6]); + assert!(n.is_ready()); + n.reset(); + assert!(!n.is_ready()); + assert_eq!(n.value(), None); + assert_eq!(n.naked_count(), 0); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..80) + .map(|i| { + let b = 100.0 + (f64::from(i) * 0.25).sin() * 9.0; + c(b + 1.0, b - 1.0, b, 1_000.0 + f64::from(i)) + }) + .collect(); + let batch = NakedPoc::new(20, 24).unwrap().batch(&candles); + let mut b = NakedPoc::new(20, 24).unwrap(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } + + #[test] + fn flat_session_reports_price() { + // A session with zero high-low span returns the session price directly. + let mut n = NakedPoc::new(2, 4).unwrap(); + n.update(c(50.0, 50.0, 50.0, 10.0)); + assert_eq!(n.update(c(50.0, 50.0, 50.0, 10.0)), Some(50.0)); + } + + #[test] + fn zero_volume_session_is_handled() { + // Zero-volume candles are skipped in the histogram; a POC still emits. + let mut n = NakedPoc::new(2, 4).unwrap(); + n.update(c(60.0, 40.0, 50.0, 0.0)); + assert!(n.update(c(60.0, 40.0, 50.0, 0.0)).is_some()); + } + + #[test] + fn nearest_of_two_naked_pocs() { + // Two untouched POCs at distant prices accumulate; the one nearest the + // last close is reported (exercises the min-by comparison). + let mut n = NakedPoc::new(2, 4).unwrap(); + n.update(c(11.0, 9.0, 10.0, 100.0)); + n.update(c(11.0, 9.0, 10.0, 100.0)); // POC near 10 + n.update(c(101.0, 99.0, 100.0, 100.0)); + let v = n.update(c(101.0, 99.0, 100.0, 100.0)).unwrap(); // POC near 100 + assert!( + v > 50.0, + "nearest to close 100 should be the upper POC, got {v}" + ); + } +} diff --git a/crates/wickra-core/src/indicators/profile_shape.rs b/crates/wickra-core/src/indicators/profile_shape.rs new file mode 100644 index 00000000..cb38f0a3 --- /dev/null +++ b/crates/wickra-core/src/indicators/profile_shape.rs @@ -0,0 +1,285 @@ +//! Profile Shape — classifies the volume profile as b-shape, P-shape, or D/normal. + +use std::collections::VecDeque; + +use crate::error::{Error, Result}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Profile Shape — classifies a rolling volume profile by where its point of +/// control (POC) sits within the range: `b`, `P`, or `D` (normal). +/// +/// ```text +/// build a `bins`-bucket volume profile over the last `period` candles +/// poc_idx = bin with the most volume +/// +1 P-shape : POC in the upper third (heavy top, thin tail down) — short-covering / accumulation +/// −1 b-shape : POC in the lower third (heavy bottom, thin tail up) — long-liquidation / distribution +/// 0 D/normal: POC in the middle third (balanced bell) +/// ``` +/// +/// Market Profile readers classify the day's shape by the location of the heaviest +/// trading. A **P-shape** (control high, a thin tail beneath) typically marks +/// short-covering or the start of accumulation; a **b-shape** (control low, thin +/// tail above) marks long liquidation or distribution; a **D-shape** is a balanced, +/// two-sided day. Reducing the profile to this three-way code gives a compact, +/// streaming read of market posture. +/// +/// The output is `+1` / `0` / `−1`. The first value lands after `period` candles; +/// each `update` rebuilds the profile in O(`period · bins`). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{Candle, Indicator, ProfileShape}; +/// +/// let mut indicator = ProfileShape::new(20, 24).unwrap(); +/// let mut last = None; +/// for i in 0..40 { +/// let base = 100.0 + (f64::from(i) * 0.3).sin() * 5.0; +/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap(); +/// last = indicator.update(c); +/// } +/// assert!(last.is_some()); +/// ``` +#[derive(Debug, Clone)] +pub struct ProfileShape { + period: usize, + bins: usize, + window: VecDeque, + last: Option, +} + +impl ProfileShape { + /// Construct a Profile Shape classifier. + /// + /// # Errors + /// + /// Returns [`Error::PeriodZero`] if `period` is zero, or + /// [`Error::InvalidPeriod`] if `bins < 3` (the three-way split needs three + /// zones). + pub fn new(period: usize, bins: usize) -> Result { + if period == 0 { + return Err(Error::PeriodZero); + } + if bins < 3 { + return Err(Error::InvalidPeriod { + message: "profile shape needs bins >= 3", + }); + } + Ok(Self { + period, + bins, + window: VecDeque::with_capacity(period), + last: None, + }) + } + + /// Configured `(period, bins)`. + pub const fn params(&self) -> (usize, usize) { + (self.period, self.bins) + } + + /// Current value if available. + pub const fn value(&self) -> Option { + self.last + } + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + fn poc_index(&self) -> usize { + let mut low = f64::INFINITY; + let mut high = f64::NEG_INFINITY; + for c in &self.window { + low = low.min(c.low); + high = high.max(c.high); + } + let mut hist = vec![0.0; self.bins]; + let span = high - low; + if span > 0.0 { + let width = span / self.bins as f64; + for c in &self.window { + if c.volume == 0.0 { + continue; + } + let lo_idx = (((c.low - low) / width).floor() as usize).min(self.bins - 1); + let hi_idx = (((c.high - low) / width).floor() as usize).min(self.bins - 1); + let share = c.volume / (hi_idx - lo_idx + 1) as f64; + for bin in hist.iter_mut().take(hi_idx + 1).skip(lo_idx) { + *bin += share; + } + } + } + let mut poc_idx = 0; + let mut poc_vol = f64::NEG_INFINITY; + for (idx, &vol) in hist.iter().enumerate() { + if vol > poc_vol { + poc_vol = vol; + poc_idx = idx; + } + } + poc_idx + } +} + +impl Indicator for ProfileShape { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + if self.window.len() == self.period { + self.window.pop_front(); + } + self.window.push_back(candle); + if self.window.len() < self.period { + return None; + } + let poc = self.poc_index(); + let lower = self.bins / 3; + let upper = self.bins - self.bins / 3; + let shape = if poc >= upper { + 1.0 + } else if poc < lower { + -1.0 + } else { + 0.0 + }; + self.last = Some(shape); + Some(shape) + } + + fn reset(&mut self) { + self.window.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 { + "ProfileShape" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(high: f64, low: f64, volume: f64) -> Candle { + Candle::new_unchecked( + f64::midpoint(high, low), + high, + low, + f64::midpoint(high, low), + volume, + 0, + ) + } + + #[test] + fn rejects_invalid_params() { + assert!(matches!(ProfileShape::new(0, 24), Err(Error::PeriodZero))); + assert!(matches!( + ProfileShape::new(20, 2), + Err(Error::InvalidPeriod { .. }) + )); + } + + #[test] + fn accessors_and_metadata() { + let p = ProfileShape::new(20, 24).unwrap(); + assert_eq!(p.params(), (20, 24)); + assert_eq!(p.warmup_period(), 20); + assert_eq!(p.name(), "ProfileShape"); + assert!(!p.is_ready()); + assert_eq!(p.value(), None); + } + + #[test] + fn first_emission_at_warmup_period() { + let mut p = ProfileShape::new(4, 9).unwrap(); + let candles: Vec = (0..6).map(|_| c(110.0, 90.0, 1_000.0)).collect(); + let out = p.batch(&candles); + for v in out.iter().take(3) { + assert!(v.is_none()); + } + assert!(out[3].is_some()); + } + + #[test] + fn heavy_top_is_p_shape() { + // Volume concentrated near the top of the range -> P-shape -> +1. + let mut p = ProfileShape::new(6, 9).unwrap(); + let mut candles: Vec = (0..5).map(|_| c(119.0, 117.0, 5_000.0)).collect(); + candles.push(c(119.0, 80.0, 50.0)); // a thin tail down to 80 + let last = p.batch(&candles).into_iter().flatten().last().unwrap(); + assert_eq!(last, 1.0); + } + + #[test] + fn heavy_bottom_is_b_shape() { + let mut p = ProfileShape::new(6, 9).unwrap(); + let mut candles: Vec = (0..5).map(|_| c(83.0, 81.0, 5_000.0)).collect(); + candles.push(c(120.0, 81.0, 50.0)); // a thin tail up to 120 + let last = p.batch(&candles).into_iter().flatten().last().unwrap(); + assert_eq!(last, -1.0); + } + + #[test] + fn balanced_is_d_shape() { + // Volume concentrated in the middle -> D/normal -> 0. + let mut p = ProfileShape::new(6, 9).unwrap(); + let mut candles: Vec = (0..5).map(|_| c(101.0, 99.0, 5_000.0)).collect(); + candles.push(c(120.0, 80.0, 50.0)); // thin tails both ways, POC in the middle + let last = p.batch(&candles).into_iter().flatten().last().unwrap(); + assert_eq!(last, 0.0); + } + + #[test] + fn reset_clears_state() { + let mut p = ProfileShape::new(4, 9).unwrap(); + p.batch(&[c(110.0, 90.0, 1_000.0); 6]); + assert!(p.is_ready()); + p.reset(); + assert!(!p.is_ready()); + assert_eq!(p.value(), None); + assert_eq!(p.update(c(110.0, 90.0, 1_000.0)), None); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..80) + .map(|i| { + c( + 110.0 + (f64::from(i) * 0.25).sin() * 9.0, + 90.0, + 1_000.0 + f64::from(i), + ) + }) + .collect(); + let batch = ProfileShape::new(20, 24).unwrap().batch(&candles); + let mut b = ProfileShape::new(20, 24).unwrap(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } + + #[test] + fn flat_window_is_handled() { + // Zero high-low span skips the histogram pass entirely. + let mut p = ProfileShape::new(2, 4).unwrap(); + p.update(c(50.0, 50.0, 10.0)); + assert!(p.update(c(50.0, 50.0, 10.0)).is_some()); + } + + #[test] + fn zero_volume_window_is_handled() { + // Non-flat window of zero-volume candles hits the skip path. + let mut p = ProfileShape::new(2, 4).unwrap(); + p.update(c(60.0, 40.0, 0.0)); + assert!(p.update(c(60.0, 40.0, 0.0)).is_some()); + } +} diff --git a/crates/wickra-core/src/indicators/single_prints.rs b/crates/wickra-core/src/indicators/single_prints.rs new file mode 100644 index 00000000..96510c96 --- /dev/null +++ b/crates/wickra-core/src/indicators/single_prints.rs @@ -0,0 +1,253 @@ +//! Single Prints — count of price levels touched by exactly one bar (low acceptance). + +use std::collections::VecDeque; + +use crate::error::{Error, Result}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Single Prints — the number of price levels (bins) in the rolling profile that +/// were touched by **exactly one** bar, marking zones of low acceptance / fast +/// movement. +/// +/// ```text +/// for each of `bins` price levels over the last `period` candles: +/// touches = number of bars whose high-low range covers that level +/// SinglePrints = count of levels with touches == 1 +/// ``` +/// +/// In Market Profile a "single print" is a price the market traded through so +/// quickly that only one time-period printed there — a footprint of an aggressive, +/// one-sided move with little two-way trade. Single prints often act as support or +/// resistance on a retest (the imbalance gets "repaired") and mark the edges of +/// rapid moves. Counting them per profile gives a streaming gauge of how much of +/// the recent range was traversed without acceptance: a high count means a fast, +/// trending, low-rotation market; a low count means a balanced, well-traded range. +/// +/// The output is a non-negative count. The first value lands after `period` +/// candles; each `update` rebuilds the touch histogram in O(`period · bins`). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{Candle, Indicator, SinglePrints}; +/// +/// let mut indicator = SinglePrints::new(20, 24).unwrap(); +/// let mut last = None; +/// for i in 0..40 { +/// let base = 100.0 + f64::from(i); // a one-directional ramp -> many single prints +/// let c = Candle::new(base, base + 0.5, base - 0.5, base, 1_000.0, 0).unwrap(); +/// last = indicator.update(c); +/// } +/// assert!(last.is_some()); +/// ``` +#[derive(Debug, Clone)] +pub struct SinglePrints { + period: usize, + bins: usize, + window: VecDeque, + last: Option, +} + +impl SinglePrints { + /// Construct a Single Prints counter. + /// + /// # Errors + /// + /// Returns [`Error::PeriodZero`] if `period` or `bins` is zero. + pub fn new(period: usize, bins: usize) -> Result { + if period == 0 || bins == 0 { + return Err(Error::PeriodZero); + } + Ok(Self { + period, + bins, + window: VecDeque::with_capacity(period), + last: None, + }) + } + + /// Configured `(period, bins)`. + pub const fn params(&self) -> (usize, usize) { + (self.period, self.bins) + } + + /// Current value if available. + pub const fn value(&self) -> Option { + self.last + } + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + fn count_single_prints(&self) -> usize { + let mut low = f64::INFINITY; + let mut high = f64::NEG_INFINITY; + for c in &self.window { + low = low.min(c.low); + high = high.max(c.high); + } + let span = high - low; + if span <= 0.0 { + return 0; + } + let width = span / self.bins as f64; + let mut touches = vec![0u32; self.bins]; + for c in &self.window { + let lo_idx = (((c.low - low) / width).floor() as usize).min(self.bins - 1); + let hi_idx = (((c.high - low) / width).floor() as usize).min(self.bins - 1); + for t in touches.iter_mut().take(hi_idx + 1).skip(lo_idx) { + *t += 1; + } + } + touches.iter().filter(|&&t| t == 1).count() + } +} + +impl Indicator for SinglePrints { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + if self.window.len() == self.period { + self.window.pop_front(); + } + self.window.push_back(candle); + if self.window.len() < self.period { + return None; + } + let count = self.count_single_prints() as f64; + self.last = Some(count); + Some(count) + } + + fn reset(&mut self) { + self.window.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 { + "SinglePrints" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(high: f64, low: f64) -> Candle { + Candle::new_unchecked( + f64::midpoint(high, low), + high, + low, + f64::midpoint(high, low), + 1_000.0, + 0, + ) + } + + #[test] + fn rejects_zero_params() { + assert!(matches!(SinglePrints::new(0, 24), Err(Error::PeriodZero))); + assert!(matches!(SinglePrints::new(20, 0), Err(Error::PeriodZero))); + } + + #[test] + fn accessors_and_metadata() { + let s = SinglePrints::new(20, 24).unwrap(); + assert_eq!(s.params(), (20, 24)); + assert_eq!(s.warmup_period(), 20); + assert_eq!(s.name(), "SinglePrints"); + assert!(!s.is_ready()); + assert_eq!(s.value(), None); + } + + #[test] + fn first_emission_at_warmup_period() { + let mut s = SinglePrints::new(4, 8).unwrap(); + let candles: Vec = (0..6) + .map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i))) + .collect(); + let out = s.batch(&candles); + for v in out.iter().take(3) { + assert!(v.is_none()); + } + assert!(out[3].is_some()); + } + + #[test] + fn flat_range_has_no_single_prints() { + // Every bar covers the same single price -> zero span -> 0. + let mut s = SinglePrints::new(4, 8).unwrap(); + let last = s + .batch(&[c(100.0, 100.0); 6]) + .into_iter() + .flatten() + .last() + .unwrap(); + assert_eq!(last, 0.0); + } + + #[test] + fn ramp_has_many_single_prints() { + // A one-directional ramp visits most levels exactly once. + let mut s = SinglePrints::new(10, 24).unwrap(); + let candles: Vec = (0..10) + .map(|i| c(100.5 + f64::from(i), 99.5 + f64::from(i))) + .collect(); + let last = s.batch(&candles).into_iter().flatten().last().unwrap(); + assert!( + last > 0.0, + "a ramp should produce single prints, got {last}" + ); + } + + #[test] + fn output_non_negative() { + let mut s = SinglePrints::new(14, 24).unwrap(); + for v in s + .batch( + &(0..60) + .map(|i| c(110.0 + (f64::from(i) * 0.3).sin() * 8.0, 90.0)) + .collect::>(), + ) + .into_iter() + .flatten() + { + assert!(v >= 0.0); + } + } + + #[test] + fn reset_clears_state() { + let mut s = SinglePrints::new(4, 8).unwrap(); + s.batch( + &(0..6) + .map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i))) + .collect::>(), + ); + assert!(s.is_ready()); + s.reset(); + assert!(!s.is_ready()); + assert_eq!(s.value(), None); + assert_eq!(s.update(c(101.0, 99.0)), None); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..80) + .map(|i| c(110.0 + (f64::from(i) * 0.25).sin() * 9.0, 90.0)) + .collect(); + let batch = SinglePrints::new(20, 24).unwrap().batch(&candles); + let mut b = SinglePrints::new(20, 24).unwrap(); + 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 9f21e2e5..44a2e001 100644 --- a/crates/wickra-core/src/lib.rs +++ b/crates/wickra-core/src/lib.rs @@ -72,37 +72,38 @@ pub use indicators::{ ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, CloseVsOpen, ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput, - ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, CorrelationTrendIndicator, - Counterattack, Crab, CumulativeVolumeDelta, CumulativeVolumeIndex, CupAndHandle, - CyberneticCycle, Cypher, DayOfWeekProfile, DayOfWeekProfileOutput, Decycler, - DecyclerOscillator, Dema, DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope, - DerivativeOscillator, DetrendedStdDev, DisparityIndex, DistanceSsd, Doji, DojiStar, Donchian, - DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, - DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, DumplingTop, - Dx, DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma, - ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone, ElderSafeZoneOutput, Ema, - EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput, EstimatedLeverageRatio, - 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, - FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio, - GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator, + CompositeProfile, CompositeProfileOutput, ConcealingBabySwallow, ConditionalValueAtRisk, + ConnorsRsi, Coppock, CorrelationTrendIndicator, Counterattack, Crab, CumulativeVolumeDelta, + CumulativeVolumeIndex, CupAndHandle, CyberneticCycle, Cypher, DayOfWeekProfile, + DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex, DemarkPivots, + DemarkPivotsOutput, DepthSlope, DerivativeOscillator, DetrendedStdDev, DisparityIndex, + DistanceSsd, Doji, DojiStar, Donchian, DonchianOutput, DonchianStop, DonchianStopOutput, + DoubleBollinger, DoubleBollingerOutput, DoubleTopBottom, DownsideGapThreeMethods, Dpo, + DragonflyDoji, DrawdownDuration, DumplingTop, Dx, DynamicMomentumIndex, EaseOfMovement, + EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone, + ElderSafeZoneOutput, Ema, EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput, + EstimatedLeverageRatio, 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, FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore, + GainLossRatio, GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator, GatorOscillatorOutput, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross, HasbrouckInformationShare, 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, - KaseDevStopOutput, KasePermissionStochastic, KasePermissionStochasticOutput, KellyCriterion, - Keltner, KeltnerOutput, KendallTau, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, + HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighLowVolumeNodes, + HighLowVolumeNodesOutput, 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, KaseDevStopOutput, + KasePermissionStochastic, KasePermissionStochasticOutput, KellyCriterion, Keltner, + KeltnerOutput, KendallTau, Kicking, KickingByLength, Kst, KstOutput, Kurtosis, Kvo, KylesLambda, LadderBottom, LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel, LinRegChannelOutput, LinRegIntercept, LinRegSlope, LinearRegression, LiquidationFeatures, LiquidationFeaturesOutput, LogReturn, LongLeggedDoji, @@ -112,7 +113,7 @@ pub use indicators::{ McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianChannelOutput, MedianMa, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop, ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines, - MurreyMathLinesOutput, Natr, NewHighsNewLows, NewPriceLines, Nrtr, NrtrOutput, Nvi, + MurreyMathLinesOutput, NakedPoc, Natr, NewHighsNewLows, NewPriceLines, Nrtr, NrtrOutput, Nvi, OIPriceDivergence, OIWeighted, Obv, OiToVolumeRatio, OmegaRatio, OnNeck, OpenInterestDelta, OpenInterestMomentum, OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance, @@ -120,39 +121,39 @@ pub use indicators::{ PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, PerpetualPremiumIndex, Pgo, PiercingDarkCloud, Pin, PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo, - PpoHistogram, ProfitFactor, ProjectionBands, ProjectionBandsOutput, ProjectionOscillator, Psar, - Pvi, Qqe, QqeOutput, Qstick, QuartileBands, QuartileBandsOutput, QuotedSpread, RSquared, - RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange, Reflex, RegimeLabel, - RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, RickshawMan, - RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility, RollMeasure, - RollingCorrelation, RollingCovariance, RollingIqr, RollingMinMaxScaler, RollingPercentileRank, - RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rsx, Rvi, RviVolatility, Rwi, RwiOutput, - SampleEntropy, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionHighLowOutput, - SessionRange, SessionRangeOutput, SessionVwap, ShannonEntropy, Shark, SharpeRatio, - ShootingStar, ShortLine, SignedVolume, SineWave, SineWeightedMa, Skewness, Sma, Smi, Smma, - SmoothedHeikinAshi, SmoothedHeikinAshiOutput, SortinoRatio, SpearmanCorrelation, SpinningTop, - SpreadAr1Coefficient, SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, - StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput, StarcBands, - StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi, Stochastic, - StochasticCci, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput, - TakerBuySellRatio, Takuri, TasukiGap, TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, - TdDWave, TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdMovingAverage, - TdMovingAverageOutput, TdOpen, TdPressure, TdPropulsion, TdRangeProjection, - TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential, - TdSequentialOutput, TdSetup, TdTrap, Tema, TermStructureBasis, ThreeDrives, ThreeInside, - ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, - Thrusting, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, - TowerTopBottom, TpoProfile, TpoProfileOutput, TradeImbalance, TradeSignAutocorrelation, - 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, + PpoHistogram, ProfileShape, ProfitFactor, ProjectionBands, ProjectionBandsOutput, + ProjectionOscillator, Psar, Pvi, Qqe, QqeOutput, Qstick, QuartileBands, QuartileBandsOutput, + QuotedSpread, RSquared, RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange, + Reflex, RegimeLabel, RelativeStrengthAB, RelativeStrengthOutput, RenkoBars, RenkoTrailingStop, + RickshawMan, RisingThreeMethods, Rmi, Roc, Rocp, Rocr, Rocr100, RogersSatchellVolatility, + RollMeasure, RollingCorrelation, RollingCovariance, RollingIqr, RollingMinMaxScaler, + RollingPercentileRank, RollingQuantile, RollingVwap, RoofingFilter, Rsi, Rsx, Rvi, + RviVolatility, Rwi, RwiOutput, SampleEntropy, SarExt, SeasonalZScore, SeparatingLines, + SessionHighLow, SessionHighLowOutput, SessionRange, SessionRangeOutput, SessionVwap, + ShannonEntropy, Shark, SharpeRatio, ShootingStar, ShortLine, SignedVolume, SineWave, + SineWeightedMa, SinglePrints, Skewness, Sma, Smi, Smma, SmoothedHeikinAshi, + SmoothedHeikinAshiOutput, SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient, + SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, + StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, + StepTrailingStop, StickSandwich, StochRsi, Stochastic, StochasticCci, StochasticOutput, + SuperSmoother, SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, + TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker, TdDifferential, + TdLines, TdLinesOutput, TdMovingAverage, TdMovingAverageOutput, TdOpen, TdPressure, + TdPropulsion, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel, + TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, TdTrap, Tema, TermStructureBasis, + ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, + ThreeStarsInSouth, Thrusting, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile, + TimeOfDayReturnProfileOutput, TowerTopBottom, TpoProfile, TpoProfileOutput, TradeImbalance, + TradeSignAutocorrelation, 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, diff --git a/docs/README.md b/docs/README.md index b31d700f..bd989b82 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 **493 indicators** across +- A per-indicator deep dive for every one of the **498 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 08ca6c1b..3e2f21f1 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, 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}; +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, CompositeProfile, 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, HighLowVolumeNodes, 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, NakedPoc, Natr, NewPriceLines, Nrtr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PivotReversal, PlusDi, PlusDm, ProfileShape, ProjectionBands, ProjectionOscillator, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, SinglePrints, 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 @@ -188,6 +188,11 @@ fuzz_target!(|data: Vec| { } // --- Market Profile (multi-output) --- + drive(|| CompositeProfile::new(20, 24, 0.7).unwrap(), &candles); + drive(|| HighLowVolumeNodes::new(20, 24).unwrap(), &candles); + drive(|| NakedPoc::new(20, 24).unwrap(), &candles); + drive(|| SinglePrints::new(20, 24).unwrap(), &candles); + drive(|| ProfileShape::new(20, 24).unwrap(), &candles); drive(|| ValueArea::new(20, 50, 0.70).unwrap(), &candles); drive(|| VolumeProfile::new(20, 50).unwrap(), &candles); drive(|| TpoProfile::new(20, 50).unwrap(), &candles);