From e97c3389feb3f998e5335f077c1c685a3f3701f0 Mon Sep 17 00:00:00 2001 From: kingchenc Date: Mon, 8 Jun 2026 00:13:42 +0200 Subject: [PATCH] feat: add Pivots & S/R indicators (B11) (#201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds five support/resistance and pivot indicators, growing the catalog 462 -> 467. ## Indicators - **CentralPivotRange** (Candle -> struct) — the classic pivot `(H+L+C)/3` flanked by two central levels (TC/BC); range width gauges trending vs balanced days. - **MurreyMathLines** (Candle -> struct) — T. H. Murrey's eighths grid over a rolling high-low frame; nine levels (0/8 .. 8/8) acting as support/resistance. - **AndrewsPitchfork** (Candle -> struct) — median line and two parallels projected forward from the last three auto-detected swing pivots (symmetric fractal of half-width `strength`). - **VolumeWeightedSr** (Candle -> struct) — a band whose edges are the volume-weighted average of recent highs (resistance) and lows (support); falls back to equal weighting when window volume is zero. - **PivotReversal** (Candle -> f64) — a `+1`/`-1` breakout signal fired on the bar where price closes through the most recently confirmed swing pivot. ## Wiring Core structs with branch-complete unit tests, Python/Node/WASM bindings, fuzz drives, reference + streaming-vs-batch tests, README + docs counter sync (FAMILIES "Pivots & S/R"), and CHANGELOG entries. Verified locally: `cargo fmt`, `cargo test -p wickra-core` (3798 lib + 425 doc), `cargo clippy --workspace --all-targets --all-features -D warnings`, `npm run build && npm test` (542), `maturin develop` + `pytest` (891). --- CHANGELOG.md | 5 + README.md | 12 +- bindings/node/__tests__/indicators.test.js | 5 + bindings/node/index.d.ts | 70 ++++ bindings/node/index.js | 7 +- bindings/node/src/lib.rs | 365 ++++++++++++++++++ bindings/python/python/wickra/__init__.py | 10 + bindings/python/src/lib.rs | 354 +++++++++++++++++ bindings/python/tests/test_new_indicators.py | 62 +++ bindings/wasm/src/lib.rs | 302 +++++++++++++++ .../src/indicators/andrews_pitchfork.rs | 353 +++++++++++++++++ .../src/indicators/central_pivot_range.rs | 171 ++++++++ crates/wickra-core/src/indicators/mod.rs | 17 +- .../src/indicators/murrey_math_lines.rs | 272 +++++++++++++ .../src/indicators/pivot_reversal.rs | 293 ++++++++++++++ .../src/indicators/volume_weighted_sr.rs | 281 ++++++++++++++ crates/wickra-core/src/lib.rs | 67 ++-- docs/README.md | 2 +- fuzz/fuzz_targets/indicator_update_candle.rs | 9 +- 19 files changed, 2614 insertions(+), 43 deletions(-) create mode 100644 crates/wickra-core/src/indicators/andrews_pitchfork.rs create mode 100644 crates/wickra-core/src/indicators/central_pivot_range.rs create mode 100644 crates/wickra-core/src/indicators/murrey_math_lines.rs create mode 100644 crates/wickra-core/src/indicators/pivot_reversal.rs create mode 100644 crates/wickra-core/src/indicators/volume_weighted_sr.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 19fdae9c..a6acd70f 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] +- **Pivot Reversal** — a breakout signal when price closes through the most recently confirmed swing pivot (`PIVOT_REVERSAL`). +- **Volume-Weighted Support/Resistance** — a band whose edges are the volume-weighted average of recent highs and lows (`VOLUME_WEIGHTED_SR`). +- **Andrews Pitchfork** — median line and two parallels projected from the last three swing pivots (`ANDREWS_PITCHFORK`). +- **Murrey Math Lines** — T. H. Murrey's eighths grid over the recent trading range, each level acting as support/resistance (`MURREY_MATH_LINES`). +- **Central Pivot Range** — the classic pivot flanked by two central levels gauging the day's expected character (`CENTRAL_PIVOT_RANGE`). ## [0.6.5] - 2026-06-07 - **Autocorrelation Periodogram** — Ehlers autocorrelation periodogram: dominant cycle period estimate (`AUTOCORRPGRAM`). diff --git a/README.md b/README.md index 9f0227b1..127745a5 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 462 indicators; start at the + every one of the 467 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), @@ -79,7 +79,7 @@ Plenty of TA libraries are fast. Each one forces a trade-off Wickra does not: | finta | clean | no | Python | ~80 | stale | | talipp | clean | yes | Python | ~40 | yes | -Wickra's edge is **breadth with reach**: 462 indicators that all update in O(1) +Wickra's edge is **breadth with reach**: 467 indicators that all update in O(1) per tick and ship natively to Python, Node.js, WebAssembly and Rust from a single engine. @@ -188,7 +188,7 @@ python -m benchmarks.compare_libraries ## Indicators -462 streaming-first indicators across twenty-four families. Every one passes the +467 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). @@ -205,7 +205,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview). | Volume | OBV, VWAP (cumulative + rolling), ADL, Volume-Price Trend, Chaikin Money Flow, Chaikin Oscillator, Force Index, Ease of Movement, Klinger Volume Oscillator, Volume Oscillator, NVI, PVI, Williams A/D, Anchored VWAP, Demand Index, TSV, VZO, Market Facilitation Index, Volume RSI, Williams Accumulation/Distribution, Twiggs Money Flow, Trade Volume Index, Intraday Intensity Index, Better Volume, Volume-Weighted MACD | | Price Statistics | Typical Price, Median Price, Weighted Close, Linear Regression, Linear Regression Slope, Z-Score, Linear Regression Angle, Variance, Coefficient of Variation, Skewness, Kurtosis, Standard Error, Detrended StdDev, R², Median Absolute Deviation, Autocorrelation, Hurst Exponent, Pearson Correlation, Beta, Pairwise Beta, Pair Spread Z-Score, Lead-Lag Cross-Correlation, Cointegration, Relative Strength A-vs-B, Spearman Correlation, Mid Price, Mid Point, Average Price, Linear Regression Intercept, Time Series Forecast, Rolling Correlation, Rolling Covariance, OU Half-Life, Spread Hurst, Distance SSD, Beta-Neutral Spread, Variance Ratio, Granger Causality, Kalman Hedge Ratio, Spread Bollinger Bands, Spread AR(1) Coefficient, Jarque-Bera, Rolling Min-Max Scaler, Shannon Entropy, Sample Entropy, Kendall Tau | | Ehlers / Cycle (DSP) | MAMA, FAMA, Fisher Transform, Inverse Fisher Transform, SuperSmoother, Hilbert Dominant Cycle, Hilbert Phasor, Hilbert DC Phase, Hilbert Trend Mode, Sine Wave, Decycler, Decycler Oscillator, Roofing Filter, Center of Gravity, Cybernetic Cycle, Adaptive Cycle, Empirical Mode Decomposition, Ehlers Stochastic, Instantaneous Trendline, Highpass Filter, Reflex, Trendflex, Correlation Trend Indicator, Adaptive RSI, Universal Oscillator, Adaptive CCI, Bandpass Filter, Even Better Sinewave, Autocorrelation Periodogram | -| Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag | +| Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag, Central Pivot Range, Murrey Math Lines, Andrews Pitchfork, Volume-Weighted Support/Resistance, Pivot Reversal | | DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level | | Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi | | Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns) | @@ -297,7 +297,7 @@ A Python live-trading example using the public `websockets` package lives at ``` wickra/ ├── crates/ -│ ├── wickra-core/ core engine + all 462 indicators +│ ├── wickra-core/ core engine + all 467 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 602e3275..265f89ec 100644 --- a/bindings/node/__tests__/indicators.test.js +++ b/bindings/node/__tests__/indicators.test.js @@ -377,6 +377,7 @@ const candleScalar = { IntradayIntensity: { make: () => new wickra.IntradayIntensity(), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, BetterVolume: { make: () => new wickra.BetterVolume(14), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) }, ADAPTIVECCI: { make: () => new wickra.ADAPTIVECCI(20), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, + PivotReversal: { make: () => new wickra.PivotReversal(1, 1), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, }; for (const [name, d] of Object.entries(candleScalar)) { @@ -474,6 +475,10 @@ const multi = { Nrtr: { make: () => new wickra.Nrtr(2.0), fields: ['value', 'direction'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, ModifiedMaStop: { make: () => new wickra.ModifiedMaStop(14), fields: ['value', 'direction'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, VolumeWeightedMacd: { make: () => new wickra.VolumeWeightedMacd(12, 26, 9), fields: ['macd', 'signal', 'histogram'], step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) }, + CentralPivotRange: { make: () => new wickra.CentralPivotRange(), fields: ['pivot', 'tc', 'bc'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) }, + MurreyMathLines: { make: () => new wickra.MurreyMathLines(4), fields: ['mm8_8', 'mm7_8', 'mm6_8', 'mm5_8', 'mm4_8', 'mm3_8', 'mm2_8', 'mm1_8', 'mm0_8'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, + AndrewsPitchfork: { make: () => new wickra.AndrewsPitchfork(2), fields: ['median', 'upper', 'lower'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) }, + VolumeWeightedSr: { make: () => new wickra.VolumeWeightedSr(3), fields: ['support', 'resistance'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) }, }; for (const [name, d] of Object.entries(multi)) { diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index aca3b897..a83e35f6 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -239,6 +239,31 @@ export interface ProjectionBandsValue { middle: number lower: number } +export interface CentralPivotRangeValue { + pivot: number + tc: number + bc: number +} +export interface MurreyMathLinesValue { + mm8_8: number + mm7_8: number + mm6_8: number + mm5_8: number + mm4_8: number + mm3_8: number + mm2_8: number + mm1_8: number + mm0_8: number +} +export interface AndrewsPitchforkValue { + median: number + upper: number + lower: number +} +export interface VolumeWeightedSrValue { + support: number + resistance: number +} export interface DoubleBollingerValue { upperOuter: number upperInner: number @@ -2930,6 +2955,51 @@ export declare class ProjectionBands { isReady(): boolean warmupPeriod(): number } +export type CentralPivotRangeNode = CentralPivotRange +export declare class CentralPivotRange { + constructor() + update(high: number, low: number, close: number): CentralPivotRangeValue | null + batch(high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type MurreyMathLinesNode = MurreyMathLines +export declare class MurreyMathLines { + constructor(period: number) + update(high: number, low: number): MurreyMathLinesValue | null + batch(high: Array, low: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type AndrewsPitchforkNode = AndrewsPitchfork +export declare class AndrewsPitchfork { + constructor(strength: number) + update(high: number, low: number): AndrewsPitchforkValue | null + batch(high: Array, low: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type VolumeWeightedSrNode = VolumeWeightedSr +export declare class VolumeWeightedSr { + constructor(period: number) + update(high: number, low: number, volume: number): VolumeWeightedSrValue | null + batch(high: Array, low: Array, volume: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type PivotReversalNode = PivotReversal +export declare class PivotReversal { + constructor(left: number, right: number) + update(high: number, low: number, close: number): number | null + batch(high: Array, low: Array, close: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} export type DoubleBollingerNode = DoubleBollinger export declare class DoubleBollinger { constructor(period: number, kInner: number, kOuter: number) diff --git a/bindings/node/index.js b/bindings/node/index.js index 0164e6d7..d9e9f74b 100644 --- a/bindings/node/index.js +++ b/bindings/node/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, LogReturn, RealizedVolatility, RollingIqr, RollingPercentileRank, TrendLabel, WinRate, Expectancy, SWMA, GMA, EHMA, MedianMA, AdaptiveLaguerre, DisparityIndex, FisherRSI, RSX, DynamicMomentumIndex, TREND_STRENGTH_INDEX, TsfOscillator, BipowerVariation, JARQUEBERA, ROLLINGMINMAX, HIGHPASS, REFLEX, TRENDFLEX, CTI, ADAPTIVERSI, UNIVERSALOSC, BANDPASS, EVENBETTERSINE, AUTOCORRPGRAM, SHANNONENT, SAMPLEENT, EwmaVolatility, Garch11, VolatilityOfVolatility, VolatilityCone, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, KendallTau, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, VolatilityRatio, ProjectionOscillator, TimeBasedStop, ADAPTIVECCI, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, GD, HoltWinters, RMI, DerivativeOscillator, MacdHistogram, PpoHistogram, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, KaseDevStop, ElderSafeZone, AtrRatchet, Nrtr, ModifiedMaStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, QuartileBands, BomarBands, MedianChannel, ProjectionBands, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, DoubleTopBottom, TripleTopBottom, HeadAndShoulders, Triangle, Wedge, FlagPennant, RectangleRange, CupAndHandle, Abcd, Gartley, Butterfly, Bat, Crab, Shark, Cypher, ThreeDrives, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn, FibRetracement, FibExtension, FibProjection, AutoFib, GoldenPocket, FibConfluence, FibFan, FibArcs, FibChannel, FibTimeZones, VolumeRsi, Wad, TwiggsMoneyFlow, TradeVolumeIndex, IntradayIntensity, BetterVolume, VolumeWeightedMacd } = nativeBinding +const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, LogReturn, RealizedVolatility, RollingIqr, RollingPercentileRank, TrendLabel, WinRate, Expectancy, SWMA, GMA, EHMA, MedianMA, AdaptiveLaguerre, DisparityIndex, FisherRSI, RSX, DynamicMomentumIndex, TREND_STRENGTH_INDEX, TsfOscillator, BipowerVariation, JARQUEBERA, ROLLINGMINMAX, HIGHPASS, REFLEX, TRENDFLEX, CTI, ADAPTIVERSI, UNIVERSALOSC, BANDPASS, EVENBETTERSINE, AUTOCORRPGRAM, SHANNONENT, SAMPLEENT, EwmaVolatility, Garch11, VolatilityOfVolatility, VolatilityCone, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, KendallTau, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, VolatilityRatio, ProjectionOscillator, TimeBasedStop, ADAPTIVECCI, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, GD, HoltWinters, RMI, DerivativeOscillator, MacdHistogram, PpoHistogram, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, KaseDevStop, ElderSafeZone, AtrRatchet, Nrtr, ModifiedMaStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, QuartileBands, BomarBands, MedianChannel, ProjectionBands, CentralPivotRange, MurreyMathLines, AndrewsPitchfork, VolumeWeightedSr, PivotReversal, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, DoubleTopBottom, TripleTopBottom, HeadAndShoulders, Triangle, Wedge, FlagPennant, RectangleRange, CupAndHandle, Abcd, Gartley, Butterfly, Bat, Crab, Shark, Cypher, ThreeDrives, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn, FibRetracement, FibExtension, FibProjection, AutoFib, GoldenPocket, FibConfluence, FibFan, FibArcs, FibChannel, FibTimeZones, VolumeRsi, Wad, TwiggsMoneyFlow, TradeVolumeIndex, IntradayIntensity, BetterVolume, VolumeWeightedMacd } = nativeBinding module.exports.version = version module.exports.SMA = SMA @@ -565,6 +565,11 @@ module.exports.QuartileBands = QuartileBands module.exports.BomarBands = BomarBands module.exports.MedianChannel = MedianChannel module.exports.ProjectionBands = ProjectionBands +module.exports.CentralPivotRange = CentralPivotRange +module.exports.MurreyMathLines = MurreyMathLines +module.exports.AndrewsPitchfork = AndrewsPitchfork +module.exports.VolumeWeightedSr = VolumeWeightedSr +module.exports.PivotReversal = PivotReversal module.exports.DoubleBollinger = DoubleBollinger module.exports.TtmSqueeze = TtmSqueeze module.exports.FractalChaosBands = FractalChaosBands diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index 94720b3a..984cc8ca 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -9491,6 +9491,371 @@ impl ProjectionBandsNode { } } +// ---------- Central Pivot Range ---------- + +#[napi(object)] +pub struct CentralPivotRangeValue { + pub pivot: f64, + pub tc: f64, + pub bc: f64, +} + +#[napi(js_name = "CentralPivotRange")] +pub struct CentralPivotRangeNode { + inner: wc::CentralPivotRange, +} + +impl Default for CentralPivotRangeNode { + fn default() -> Self { + Self::new() + } +} + +#[napi] +impl CentralPivotRangeNode { + #[napi(constructor)] + pub fn new() -> Self { + Self { + inner: wc::CentralPivotRange::new(), + } + } + #[napi] + pub fn update( + &mut self, + high: f64, + low: f64, + close: f64, + ) -> napi::Result> { + Ok(self + .inner + .update(cnd(high, low, close, 0.0)?) + .map(|o| CentralPivotRangeValue { + pivot: o.pivot, + tc: o.tc, + bc: o.bc, + })) + } + #[napi] + pub fn batch( + &mut self, + high: Vec, + low: Vec, + close: Vec, + ) -> napi::Result> { + if high.len() != low.len() || low.len() != close.len() { + return Err(NapiError::from_reason( + "high, low, close must be equal length".to_string(), + )); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 3]; + for i in 0..n { + if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) { + out[i * 3] = o.pivot; + out[i * 3 + 1] = o.tc; + out[i * 3 + 2] = o.bc; + } + } + 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 + } +} + +// ---------- Murrey Math Lines ---------- + +#[napi(object)] +pub struct MurreyMathLinesValue { + #[napi(js_name = "mm8_8")] + pub mm8_8: f64, + #[napi(js_name = "mm7_8")] + pub mm7_8: f64, + #[napi(js_name = "mm6_8")] + pub mm6_8: f64, + #[napi(js_name = "mm5_8")] + pub mm5_8: f64, + #[napi(js_name = "mm4_8")] + pub mm4_8: f64, + #[napi(js_name = "mm3_8")] + pub mm3_8: f64, + #[napi(js_name = "mm2_8")] + pub mm2_8: f64, + #[napi(js_name = "mm1_8")] + pub mm1_8: f64, + #[napi(js_name = "mm0_8")] + pub mm0_8: f64, +} + +#[napi(js_name = "MurreyMathLines")] +pub struct MurreyMathLinesNode { + inner: wc::MurreyMathLines, +} + +#[napi] +impl MurreyMathLinesNode { + #[napi(constructor)] + pub fn new(period: u32) -> napi::Result { + Ok(Self { + inner: wc::MurreyMathLines::new(period as usize).map_err(map_err)?, + }) + } + #[napi] + pub fn update(&mut self, high: f64, low: f64) -> napi::Result> { + Ok(self + .inner + .update(cnd(high, low, low, 0.0)?) + .map(|o| MurreyMathLinesValue { + mm8_8: o.mm8_8, + mm7_8: o.mm7_8, + mm6_8: o.mm6_8, + mm5_8: o.mm5_8, + mm4_8: o.mm4_8, + mm3_8: o.mm3_8, + mm2_8: o.mm2_8, + mm1_8: o.mm1_8, + mm0_8: o.mm0_8, + })) + } + #[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 n = high.len(); + let mut out = vec![f64::NAN; n * 9]; + for i in 0..n { + if let Some(o) = self.inner.update(cnd(high[i], low[i], low[i], 0.0)?) { + out[i * 9] = o.mm8_8; + out[i * 9 + 1] = o.mm7_8; + out[i * 9 + 2] = o.mm6_8; + out[i * 9 + 3] = o.mm5_8; + out[i * 9 + 4] = o.mm4_8; + out[i * 9 + 5] = o.mm3_8; + out[i * 9 + 6] = o.mm2_8; + out[i * 9 + 7] = o.mm1_8; + out[i * 9 + 8] = o.mm0_8; + } + } + 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 + } +} + +// ---------- Andrews Pitchfork ---------- + +#[napi(object)] +pub struct AndrewsPitchforkValue { + pub median: f64, + pub upper: f64, + pub lower: f64, +} + +#[napi(js_name = "AndrewsPitchfork")] +pub struct AndrewsPitchforkNode { + inner: wc::AndrewsPitchfork, +} + +#[napi] +impl AndrewsPitchforkNode { + #[napi(constructor)] + pub fn new(strength: u32) -> napi::Result { + Ok(Self { + inner: wc::AndrewsPitchfork::new(strength as usize).map_err(map_err)?, + }) + } + #[napi] + pub fn update(&mut self, high: f64, low: f64) -> napi::Result> { + Ok(self + .inner + .update(cnd(high, low, low, 0.0)?) + .map(|o| AndrewsPitchforkValue { + median: o.median, + upper: o.upper, + lower: o.lower, + })) + } + #[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 n = high.len(); + let mut out = vec![f64::NAN; n * 3]; + for i in 0..n { + if let Some(o) = self.inner.update(cnd(high[i], low[i], low[i], 0.0)?) { + out[i * 3] = o.median; + out[i * 3 + 1] = o.upper; + out[i * 3 + 2] = o.lower; + } + } + 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 + } +} + +// ---------- Volume-Weighted S/R ---------- + +#[napi(object)] +pub struct VolumeWeightedSrValue { + pub support: f64, + pub resistance: f64, +} + +#[napi(js_name = "VolumeWeightedSr")] +pub struct VolumeWeightedSrNode { + inner: wc::VolumeWeightedSr, +} + +#[napi] +impl VolumeWeightedSrNode { + #[napi(constructor)] + pub fn new(period: u32) -> napi::Result { + Ok(Self { + inner: wc::VolumeWeightedSr::new(period as usize).map_err(map_err)?, + }) + } + #[napi] + pub fn update( + &mut self, + high: f64, + low: f64, + volume: f64, + ) -> napi::Result> { + Ok(self + .inner + .update(cnd(high, low, low, volume)?) + .map(|o| VolumeWeightedSrValue { + support: o.support, + resistance: o.resistance, + })) + } + #[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 { + if let Some(o) = self.inner.update(cnd(high[i], low[i], low[i], volume[i])?) { + out[i * 2] = o.support; + out[i * 2 + 1] = o.resistance; + } + } + 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 + } +} + +// ---------- Pivot Reversal ---------- + +#[napi(js_name = "PivotReversal")] +pub struct PivotReversalNode { + inner: wc::PivotReversal, +} + +#[napi] +impl PivotReversalNode { + #[napi(constructor)] + pub fn new(left: u32, right: u32) -> napi::Result { + Ok(Self { + inner: wc::PivotReversal::new(left as usize, right as usize).map_err(map_err)?, + }) + } + #[napi] + pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result> { + Ok(self.inner.update(cnd(high, low, close, 0.0)?)) + } + #[napi] + pub fn batch( + &mut self, + high: Vec, + low: Vec, + close: Vec, + ) -> napi::Result> { + if high.len() != low.len() || low.len() != close.len() { + return Err(NapiError::from_reason( + "high, low, close must be equal length".to_string(), + )); + } + let mut out = Vec::with_capacity(high.len()); + for i in 0..high.len() { + out.push( + self.inner + .update(cnd(high[i], low[i], close[i], 0.0)?) + .unwrap_or(f64::NAN), + ); + } + Ok(out) + } + #[napi] + pub fn reset(&mut self) { + self.inner.reset(); + } + #[napi(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[napi(js_name = "warmupPeriod")] + pub fn warmup_period(&self) -> u32 { + self.inner.warmup_period() as u32 + } +} + // ---------- Double Bollinger ---------- #[napi(object)] diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py index b72f8727..e5727f1d 100644 --- a/bindings/python/python/wickra/__init__.py +++ b/bindings/python/python/wickra/__init__.py @@ -309,6 +309,11 @@ from ._wickra import ( FractalChaosBands, VwapStdDevBands, # Pivots & S/R + PivotReversal, + VolumeWeightedSr, + AndrewsPitchfork, + MurreyMathLines, + CentralPivotRange, ClassicPivots, FibonacciPivots, Camarilla, @@ -801,6 +806,11 @@ __all__ = [ "FractalChaosBands", "VwapStdDevBands", # Pivots & S/R + "PivotReversal", + "VolumeWeightedSr", + "AndrewsPitchfork", + "MurreyMathLines", + "CentralPivotRange", "ClassicPivots", "FibonacciPivots", "Camarilla", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 4b8dbd3a..48516bf2 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -12513,6 +12513,355 @@ impl PyProjectionBands { } } +// ============================== Central Pivot Range ============================== + +#[pyclass( + name = "CentralPivotRange", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyCentralPivotRange { + inner: wc::CentralPivotRange, +} + +#[pymethods] +impl PyCentralPivotRange { + #[new] + fn new() -> Self { + Self { + inner: wc::CentralPivotRange::new(), + } + } + /// Returns `(pivot, tc, bc)`. + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c).map(|o| (o.pivot, o.tc, o.bc))) + } + fn batch<'py>( + &mut self, + py: Python<'py>, + high: PyReadonlyArray1<'py, f64>, + low: PyReadonlyArray1<'py, f64>, + close: PyReadonlyArray1<'py, f64>, + ) -> PyResult>> { + let h = high + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let l = low + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let c = close + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() || l.len() != c.len() { + return Err(PyValueError::new_err( + "high, low, close must be equal length", + )); + } + let n = h.len(); + let mut out = vec![f64::NAN; n * 3]; + for i in 0..n { + let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?; + if let Some(o) = self.inner.update(candle) { + out[i * 3] = o.pivot; + out[i * 3 + 1] = o.tc; + out[i * 3 + 2] = o.bc; + } + } + 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() + } +} + +// ============================== Murrey Math Lines ============================== + +#[pyclass( + name = "MurreyMathLines", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyMurreyMathLines { + inner: wc::MurreyMathLines, +} + +#[pymethods] +impl PyMurreyMathLines { + #[new] + #[pyo3(signature = (period=64))] + fn new(period: usize) -> PyResult { + Ok(Self { + inner: wc::MurreyMathLines::new(period).map_err(map_err)?, + }) + } + /// Returns `(mm8_8, mm7_8, mm6_8, mm5_8, mm4_8, mm3_8, mm2_8, mm1_8, mm0_8)`. + #[allow(clippy::type_complexity)] + fn update( + &mut self, + candle: &Bound<'_, PyAny>, + ) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c).map(|o| { + ( + o.mm8_8, o.mm7_8, o.mm6_8, o.mm5_8, o.mm4_8, o.mm3_8, o.mm2_8, o.mm1_8, o.mm0_8, + ) + })) + } + 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 n = h.len(); + let mut out = vec![f64::NAN; n * 9]; + for i in 0..n { + let candle = wc::Candle::new(l[i], h[i], l[i], l[i], 0.0, 0).map_err(map_err)?; + if let Some(o) = self.inner.update(candle) { + out[i * 9] = o.mm8_8; + out[i * 9 + 1] = o.mm7_8; + out[i * 9 + 2] = o.mm6_8; + out[i * 9 + 3] = o.mm5_8; + out[i * 9 + 4] = o.mm4_8; + out[i * 9 + 5] = o.mm3_8; + out[i * 9 + 6] = o.mm2_8; + out[i * 9 + 7] = o.mm1_8; + out[i * 9 + 8] = o.mm0_8; + } + } + Ok(numpy::ndarray::Array2::from_shape_vec((n, 9), 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() + } +} + +// ============================== Andrews Pitchfork ============================== + +#[pyclass( + name = "AndrewsPitchfork", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyAndrewsPitchfork { + inner: wc::AndrewsPitchfork, +} + +#[pymethods] +impl PyAndrewsPitchfork { + #[new] + #[pyo3(signature = (strength=2))] + fn new(strength: usize) -> PyResult { + Ok(Self { + inner: wc::AndrewsPitchfork::new(strength).map_err(map_err)?, + }) + } + /// Returns `(median, upper, lower)`. + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c).map(|o| (o.median, o.upper, o.lower))) + } + 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 n = h.len(); + let mut out = vec![f64::NAN; n * 3]; + for i in 0..n { + let candle = wc::Candle::new(l[i], h[i], l[i], l[i], 0.0, 0).map_err(map_err)?; + if let Some(o) = self.inner.update(candle) { + out[i * 3] = o.median; + out[i * 3 + 1] = o.upper; + out[i * 3 + 2] = o.lower; + } + } + 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() + } +} + +// ============================== Volume-Weighted S/R ============================== + +#[pyclass( + name = "VolumeWeightedSr", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyVolumeWeightedSr { + inner: wc::VolumeWeightedSr, +} + +#[pymethods] +impl PyVolumeWeightedSr { + #[new] + #[pyo3(signature = (period=20))] + fn new(period: usize) -> PyResult { + Ok(Self { + inner: wc::VolumeWeightedSr::new(period).map_err(map_err)?, + }) + } + /// Returns `(support, resistance)`. + fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult> { + let c = extract_candle(candle)?; + Ok(self.inner.update(c).map(|o| (o.support, o.resistance))) + } + 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 candle = wc::Candle::new(l[i], h[i], l[i], l[i], v[i], 0).map_err(map_err)?; + if let Some(o) = self.inner.update(candle) { + out[i * 2] = o.support; + out[i * 2 + 1] = o.resistance; + } + } + 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() + } +} + +// ============================== Pivot Reversal ============================== + +#[pyclass(name = "PivotReversal", module = "wickra._wickra", skip_from_py_object)] +#[derive(Clone)] +struct PyPivotReversal { + inner: wc::PivotReversal, +} + +#[pymethods] +impl PyPivotReversal { + #[new] + #[pyo3(signature = (left=2, right=2))] + fn new(left: usize, right: usize) -> PyResult { + Ok(Self { + inner: wc::PivotReversal::new(left, right).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 columns: high, low, close (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>, + ) -> PyResult>> { + let h = high + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let l = low + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + let c = close + .as_slice() + .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?; + if h.len() != l.len() || l.len() != c.len() { + return Err(PyValueError::new_err( + "high, low, close must be equal length", + )); + } + let mut out = Vec::with_capacity(h.len()); + for i in 0..h.len() { + let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?; + out.push(self.inner.update(candle).unwrap_or(f64::NAN)); + } + Ok(out.into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } +} + // ============================== Double Bollinger ============================== #[pyclass( @@ -23684,6 +24033,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; 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 f08a6f27..4bfbef8b 100644 --- a/bindings/python/tests/test_new_indicators.py +++ b/bindings/python/tests/test_new_indicators.py @@ -382,6 +382,10 @@ def test_relative_strength_streaming_matches_batch(): # 6-tuple candle; the batch helper takes only the columns it needs. CANDLE_SCALAR = { + "PivotReversal": ( + lambda: ta.PivotReversal(1, 1), + lambda ind, h, l, c, v: ind.batch(h, l, c), + ), "ADAPTIVECCI": (lambda: ta.ADAPTIVECCI(20), lambda ind, h, l, c, v: ind.batch(h, l, c)), "BetterVolume": ( lambda: ta.BetterVolume(14), @@ -948,6 +952,26 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv): # --- Candle-input, multi-output indicators -------------------------------- MULTI = { + "VolumeWeightedSr": ( + lambda: ta.VolumeWeightedSr(3), + lambda ind, h, l, c, v: ind.batch(h, l, v), + 2, + ), + "AndrewsPitchfork": ( + lambda: ta.AndrewsPitchfork(2), + lambda ind, h, l, c, v: ind.batch(h, l), + 3, + ), + "MurreyMathLines": ( + lambda: ta.MurreyMathLines(4), + lambda ind, h, l, c, v: ind.batch(h, l), + 9, + ), + "CentralPivotRange": ( + lambda: ta.CentralPivotRange(), + lambda ind, h, l, c, v: ind.batch(h, l, c), + 3, + ), "VolumeWeightedMacd": ( lambda: ta.VolumeWeightedMacd(12, 26, 9), lambda ind, h, l, c, v: ind.batch(c, v), @@ -3112,6 +3136,44 @@ def test_volume_weighted_macd_reference(): def test_kendall_tau_reference(): t = ta.KendallTau(20) + +def test_central_pivot_range_reference(): + t = ta.CentralPivotRange() + assert t.update((105.0, 110.0, 90.0, 105.0, 1.0, 0)) == pytest.approx((101.66666666666667, 103.33333333333334, 100.0)) + + +def test_murrey_math_lines_reference(): + t = ta.MurreyMathLines(4) + assert t.update((140.0, 180.0, 100.0, 140.0, 1.0, 0)) is None + assert t.update((140.0, 180.0, 100.0, 140.0, 1.0, 1)) is None + assert t.update((140.0, 180.0, 100.0, 140.0, 1.0, 2)) is None + assert t.update((140.0, 180.0, 100.0, 140.0, 1.0, 3)) == pytest.approx((180.0, 170.0, 160.0, 150.0, 140.0, 130.0, 120.0, 110.0, 100.0)) + + +def test_andrews_pitchfork_reference(): + t = ta.AndrewsPitchfork(2) + # Warmup: no pitchfork until three alternating swing pivots are confirmed. + assert t.update((100.0, 101.0, 99.0, 100.0, 1.0, 0)) is None + + +def test_volume_weighted_sr_reference(): + t = ta.VolumeWeightedSr(3) + assert t.update((100.0, 102.0, 98.0, 100.0, 1.0, 0)) is None + assert t.update((100.0, 104.0, 96.0, 100.0, 1.0, 1)) is None + assert t.update((100.0, 106.0, 94.0, 100.0, 1.0, 2)) == pytest.approx((96.0, 104.0)) + + +def test_pivot_reversal_reference(): + t = ta.PivotReversal(1, 1) + assert t.update((9.5, 10.0, 9.0, 9.5, 1.0, 0)) is None + assert t.update((11.5, 12.0, 11.0, 11.5, 1.0, 1)) is None + # Pivot high = 12 confirmed; close 9.5 has not crossed it. + assert t.update((9.5, 10.0, 9.0, 9.5, 1.0, 2)) == pytest.approx(0.0) + assert t.update((9.0, 11.0, 9.0, 9.0, 1.0, 3)) == pytest.approx(0.0) + # Close 13 > pivot high 12 with prev close 9 below it -> bullish reversal. + assert t.update((13.0, 14.0, 12.5, 13.0, 1.0, 4)) == pytest.approx(1.0) + + # --- Lifecycle ------------------------------------------------------------ diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 11734c11..15f260bf 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -6441,6 +6441,308 @@ impl WasmProjectionBands { } } +// ---------- Central Pivot Range (high/low/close input, 3 outputs) ---------- + +#[wasm_bindgen(js_name = CentralPivotRange)] +pub struct WasmCentralPivotRange { + inner: wc::CentralPivotRange, +} + +impl Default for WasmCentralPivotRange { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen(js_class = CentralPivotRange)] +impl WasmCentralPivotRange { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmCentralPivotRange { + Self { + inner: wc::CentralPivotRange::new(), + } + } + pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result { + let candle = make_candle(high, low, close, 0.0)?; + match self.inner.update(candle) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"pivot".into(), &o.pivot.into()).ok(); + Reflect::set(&obj, &"tc".into(), &o.tc.into()).ok(); + Reflect::set(&obj, &"bc".into(), &o.bc.into()).ok(); + Ok(obj.into()) + } + None => Ok(JsValue::NULL), + } + } + pub fn batch( + &mut self, + high: &[f64], + low: &[f64], + close: &[f64], + ) -> Result { + if high.len() != low.len() || low.len() != close.len() { + return Err(JsError::new("high, low, close must be equal length")); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 3]; + for i in 0..n { + let candle = make_candle(high[i], low[i], close[i], 0.0)?; + if let Some(o) = self.inner.update(candle) { + out[i * 3] = o.pivot; + out[i * 3 + 1] = o.tc; + out[i * 3 + 2] = o.bc; + } + } + 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() + } +} + +// ---------- Murrey Math Lines (high/low input, 9 outputs) ---------- + +#[wasm_bindgen(js_name = MurreyMathLines)] +pub struct WasmMurreyMathLines { + inner: wc::MurreyMathLines, +} + +#[wasm_bindgen(js_class = MurreyMathLines)] +impl WasmMurreyMathLines { + #[wasm_bindgen(constructor)] + pub fn new(period: usize) -> Result { + Ok(Self { + inner: wc::MurreyMathLines::new(period).map_err(map_err)?, + }) + } + pub fn update(&mut self, high: f64, low: f64) -> Result { + let candle = make_candle(high, low, low, 0.0)?; + match self.inner.update(candle) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"mm8_8".into(), &o.mm8_8.into()).ok(); + Reflect::set(&obj, &"mm7_8".into(), &o.mm7_8.into()).ok(); + Reflect::set(&obj, &"mm6_8".into(), &o.mm6_8.into()).ok(); + Reflect::set(&obj, &"mm5_8".into(), &o.mm5_8.into()).ok(); + Reflect::set(&obj, &"mm4_8".into(), &o.mm4_8.into()).ok(); + Reflect::set(&obj, &"mm3_8".into(), &o.mm3_8.into()).ok(); + Reflect::set(&obj, &"mm2_8".into(), &o.mm2_8.into()).ok(); + Reflect::set(&obj, &"mm1_8".into(), &o.mm1_8.into()).ok(); + Reflect::set(&obj, &"mm0_8".into(), &o.mm0_8.into()).ok(); + Ok(obj.into()) + } + None => Ok(JsValue::NULL), + } + } + pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result { + if high.len() != low.len() { + return Err(JsError::new("high and low must be equal length")); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 9]; + for i in 0..n { + let candle = make_candle(high[i], low[i], low[i], 0.0)?; + if let Some(o) = self.inner.update(candle) { + out[i * 9] = o.mm8_8; + out[i * 9 + 1] = o.mm7_8; + out[i * 9 + 2] = o.mm6_8; + out[i * 9 + 3] = o.mm5_8; + out[i * 9 + 4] = o.mm4_8; + out[i * 9 + 5] = o.mm3_8; + out[i * 9 + 6] = o.mm2_8; + out[i * 9 + 7] = o.mm1_8; + out[i * 9 + 8] = o.mm0_8; + } + } + 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() + } +} + +// ---------- Andrews Pitchfork (high/low input, 3 outputs) ---------- + +#[wasm_bindgen(js_name = AndrewsPitchfork)] +pub struct WasmAndrewsPitchfork { + inner: wc::AndrewsPitchfork, +} + +#[wasm_bindgen(js_class = AndrewsPitchfork)] +impl WasmAndrewsPitchfork { + #[wasm_bindgen(constructor)] + pub fn new(strength: usize) -> Result { + Ok(Self { + inner: wc::AndrewsPitchfork::new(strength).map_err(map_err)?, + }) + } + pub fn update(&mut self, high: f64, low: f64) -> Result { + let candle = make_candle(high, low, low, 0.0)?; + match self.inner.update(candle) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"median".into(), &o.median.into()).ok(); + Reflect::set(&obj, &"upper".into(), &o.upper.into()).ok(); + Reflect::set(&obj, &"lower".into(), &o.lower.into()).ok(); + Ok(obj.into()) + } + None => Ok(JsValue::NULL), + } + } + pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result { + if high.len() != low.len() { + return Err(JsError::new("high and low must be equal length")); + } + let n = high.len(); + let mut out = vec![f64::NAN; n * 3]; + for i in 0..n { + let candle = make_candle(high[i], low[i], low[i], 0.0)?; + if let Some(o) = self.inner.update(candle) { + out[i * 3] = o.median; + out[i * 3 + 1] = o.upper; + out[i * 3 + 2] = o.lower; + } + } + 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() + } +} + +// ---------- Volume-Weighted S/R (high/low/volume input, 2 outputs) ---------- + +#[wasm_bindgen(js_name = VolumeWeightedSr)] +pub struct WasmVolumeWeightedSr { + inner: wc::VolumeWeightedSr, +} + +#[wasm_bindgen(js_class = VolumeWeightedSr)] +impl WasmVolumeWeightedSr { + #[wasm_bindgen(constructor)] + pub fn new(period: usize) -> Result { + Ok(Self { + inner: wc::VolumeWeightedSr::new(period).map_err(map_err)?, + }) + } + pub fn update(&mut self, high: f64, low: f64, volume: f64) -> Result { + let candle = make_candle(high, low, low, volume)?; + match self.inner.update(candle) { + Some(o) => { + let obj = Object::new(); + Reflect::set(&obj, &"support".into(), &o.support.into()).ok(); + Reflect::set(&obj, &"resistance".into(), &o.resistance.into()).ok(); + Ok(obj.into()) + } + None => Ok(JsValue::NULL), + } + } + 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 candle = make_candle(high[i], low[i], low[i], volume[i])?; + if let Some(o) = self.inner.update(candle) { + out[i * 2] = o.support; + out[i * 2 + 1] = o.resistance; + } + } + 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() + } +} + +// ---------- Pivot Reversal (high/low/close input, scalar signal) ---------- + +#[wasm_bindgen(js_name = PivotReversal)] +pub struct WasmPivotReversal { + inner: wc::PivotReversal, +} + +#[wasm_bindgen(js_class = PivotReversal)] +impl WasmPivotReversal { + #[wasm_bindgen(constructor)] + pub fn new(left: usize, right: usize) -> Result { + Ok(Self { + inner: wc::PivotReversal::new(left, right).map_err(map_err)?, + }) + } + pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result, JsError> { + let candle = make_candle(high, low, close, 0.0)?; + Ok(self.inner.update(candle)) + } + pub fn batch( + &mut self, + high: &[f64], + low: &[f64], + close: &[f64], + ) -> Result { + if high.len() != low.len() || low.len() != close.len() { + return Err(JsError::new("high, low, close must be equal length")); + } + let mut out = Vec::with_capacity(high.len()); + for i in 0..high.len() { + let candle = make_candle(high[i], low[i], close[i], 0.0)?; + out.push(self.inner.update(candle).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() + } +} + // ---------- Double Bollinger (scalar input, 5 outputs) ---------- #[wasm_bindgen(js_name = DoubleBollinger)] diff --git a/crates/wickra-core/src/indicators/andrews_pitchfork.rs b/crates/wickra-core/src/indicators/andrews_pitchfork.rs new file mode 100644 index 00000000..c8b3ac1c --- /dev/null +++ b/crates/wickra-core/src/indicators/andrews_pitchfork.rs @@ -0,0 +1,353 @@ +//! Andrews Pitchfork — median line and parallels off the last three swing pivots. + +use std::collections::VecDeque; + +use crate::error::{Error, Result}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Output of [`AndrewsPitchfork`]: the three pitchfork lines projected to the +/// current bar. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct AndrewsPitchforkOutput { + /// The median line — from the handle pivot through the midpoint of the other two. + pub median: f64, + /// The upper parallel (through the higher of the two anchor pivots). + pub upper: f64, + /// The lower parallel (through the lower of the two anchor pivots). + pub lower: f64, +} + +/// A confirmed swing pivot: its bar index and price. +#[derive(Debug, Clone, Copy)] +struct Pivot { + index: f64, + price: f64, + is_high: bool, +} + +/// Andrews Pitchfork — Alan Andrews' median-line tool drawn from the three most +/// recent **swing pivots**, projected forward to the current bar. +/// +/// ```text +/// detect alternating swing highs/lows with a `strength`-bar fractal +/// P0 = handle (oldest of the last three), P1, P2 = the next two +/// M = midpoint of P1 and P2 +/// median(t) = P0 + slope·(t − t0) slope = (M − P0) / (M_t − t0) +/// upper / lower = median(t) offset by the vertical gap to the higher / lower anchor +/// ``` +/// +/// The pitchfork projects a "fork" of three parallel lines: a central **median +/// line** drawn from a starting pivot through the midpoint of a later swing, plus +/// two parallels passing through that swing's high and low. Price tends to +/// oscillate around the median line and find support/resistance at the parallels. +/// This streaming version detects the pivots automatically with a symmetric +/// fractal of half-width `strength` (so each pivot is confirmed `strength` bars +/// late) and keeps the three most recent alternating swings. +/// +/// Because it depends on swing structure, readiness is **data-dependent**: the +/// first output appears once three alternating pivots have been confirmed. +/// `warmup_period` returns the minimum bars to confirm a single pivot. Each +/// `update` is O(`strength`). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{Candle, Indicator, AndrewsPitchfork}; +/// +/// let mut indicator = AndrewsPitchfork::new(2).unwrap(); +/// let mut last = None; +/// for i in 0..120 { +/// let base = 100.0 + (f64::from(i) * 0.4).sin() * 10.0; +/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap(); +/// last = indicator.update(c); +/// } +/// // A swinging series eventually establishes a pitchfork. +/// let _ = last; +/// ``` +#[derive(Debug, Clone)] +pub struct AndrewsPitchfork { + strength: usize, + window: VecDeque, + pivots: Vec, + count: usize, + last: Option, +} + +impl AndrewsPitchfork { + /// Construct an Andrews Pitchfork with the given fractal `strength` (bars on + /// each side of a pivot). + /// + /// # Errors + /// + /// Returns [`Error::PeriodZero`] if `strength == 0`. + pub fn new(strength: usize) -> Result { + if strength == 0 { + return Err(Error::PeriodZero); + } + Ok(Self { + strength, + window: VecDeque::with_capacity(2 * strength + 1), + pivots: Vec::new(), + count: 0, + last: None, + }) + } + + /// Configured fractal strength. + pub const fn strength(&self) -> usize { + self.strength + } + + /// Current value if available. + pub const fn value(&self) -> Option { + self.last + } + + /// Record a freshly confirmed pivot, keeping the last three alternating swings. + fn record_pivot(&mut self, pivot: Pivot) { + if let Some(last) = self.pivots.last_mut() { + if last.is_high == pivot.is_high { + // Same kind: keep the more extreme one (and its index). + let more_extreme = if pivot.is_high { + pivot.price > last.price + } else { + pivot.price < last.price + }; + if more_extreme { + *last = pivot; + } + return; + } + } + self.pivots.push(pivot); + if self.pivots.len() > 3 { + self.pivots.remove(0); + } + } + + fn project(&self, tc: f64) -> Option { + let [p0, p1, p2] = self.pivots.as_slice() else { + return None; + }; + let mid_t = f64::midpoint(p1.index, p2.index); + let mid_p = f64::midpoint(p1.price, p2.price); + let slope = (mid_p - p0.price) / (mid_t - p0.index); + let median = p0.price + slope * (tc - p0.index); + let off1 = p1.price - (p0.price + slope * (p1.index - p0.index)); + let off2 = p2.price - (p0.price + slope * (p2.index - p0.index)); + Some(AndrewsPitchforkOutput { + median, + upper: median + off1.max(off2), + lower: median + off1.min(off2), + }) + } +} + +impl Indicator for AndrewsPitchfork { + type Input = Candle; + type Output = AndrewsPitchforkOutput; + + fn update(&mut self, candle: Candle) -> Option { + self.count += 1; + let span = 2 * self.strength + 1; + if self.window.len() == span { + self.window.pop_front(); + } + self.window.push_back(candle); + if self.window.len() == span { + let center = self.window[self.strength]; + let is_high = self + .window + .iter() + .enumerate() + .all(|(i, c)| i == self.strength || c.high < center.high); + let is_low = self + .window + .iter() + .enumerate() + .all(|(i, c)| i == self.strength || c.low > center.low); + // Absolute index of the center bar (1-based count minus the right span). + let center_index = (self.count - 1 - self.strength) as f64; + if is_high && !is_low { + self.record_pivot(Pivot { + index: center_index, + price: center.high, + is_high: true, + }); + } else if is_low && !is_high { + self.record_pivot(Pivot { + index: center_index, + price: center.low, + is_high: false, + }); + } + } + let tc = (self.count - 1) as f64; + if let Some(out) = self.project(tc) { + self.last = Some(out); + return Some(out); + } + None + } + + fn reset(&mut self) { + self.window.clear(); + self.pivots.clear(); + self.count = 0; + self.last = None; + } + + fn warmup_period(&self) -> usize { + 2 * self.strength + 1 + } + + fn is_ready(&self) -> bool { + self.last.is_some() + } + + fn name(&self) -> &'static str { + "AndrewsPitchfork" + } +} + +#[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, + ) + } + + /// A clean zig-zag that prints alternating swing highs and lows. + fn zigzag() -> Vec { + let mut out = Vec::new(); + for i in 0..120 { + let base = 100.0 + (f64::from(i) * 0.5).sin() * 10.0; + out.push(c(base + 1.0, base - 1.0)); + } + out + } + + #[test] + fn rejects_zero_strength() { + assert!(matches!(AndrewsPitchfork::new(0), Err(Error::PeriodZero))); + } + + #[test] + fn accessors_and_metadata() { + let p = AndrewsPitchfork::new(2).unwrap(); + assert_eq!(p.strength(), 2); + assert_eq!(p.warmup_period(), 5); + assert_eq!(p.name(), "AndrewsPitchfork"); + assert!(!p.is_ready()); + assert_eq!(p.value(), None); + } + + #[test] + fn none_before_three_pivots() { + let mut p = AndrewsPitchfork::new(2).unwrap(); + // Too few bars to ever confirm three alternating pivots. + let out = p.batch(&[c(101.0, 99.0), c(102.0, 100.0), c(101.0, 99.0)]); + assert!(out.iter().all(Option::is_none)); + } + + #[test] + fn eventually_emits_on_swings() { + let mut p = AndrewsPitchfork::new(2).unwrap(); + let out = p.batch(&zigzag()); + assert!( + out.iter().any(Option::is_some), + "a swinging series should form a pitchfork" + ); + assert!(p.is_ready()); + } + + #[test] + fn upper_at_or_above_lower() { + let mut p = AndrewsPitchfork::new(2).unwrap(); + for o in p.batch(&zigzag()).into_iter().flatten() { + assert!( + o.upper >= o.lower, + "upper {} below lower {}", + o.upper, + o.lower + ); + } + } + + #[test] + fn reset_clears_state() { + let mut p = AndrewsPitchfork::new(2).unwrap(); + p.batch(&zigzag()); + assert!(p.is_ready()); + p.reset(); + assert!(!p.is_ready()); + assert_eq!(p.value(), None); + assert_eq!(p.strength(), 2); + } + + #[test] + fn record_pivot_keeps_more_extreme_same_kind() { + let mut p = AndrewsPitchfork::new(2).unwrap(); + p.record_pivot(Pivot { + index: 0.0, + price: 100.0, + is_high: true, + }); + // A higher high of the same kind replaces the stored one. + p.record_pivot(Pivot { + index: 1.0, + price: 105.0, + is_high: true, + }); + assert_eq!(p.pivots.len(), 1); + assert_eq!(p.pivots[0].price, 105.0); + // A lower high of the same kind is ignored. + p.record_pivot(Pivot { + index: 2.0, + price: 102.0, + is_high: true, + }); + assert_eq!(p.pivots.len(), 1); + assert_eq!(p.pivots[0].price, 105.0); + // A low pivot of the other kind is appended. + p.record_pivot(Pivot { + index: 3.0, + price: 90.0, + is_high: false, + }); + assert_eq!(p.pivots.len(), 2); + // A lower low of the same kind replaces the stored low. + p.record_pivot(Pivot { + index: 4.0, + price: 85.0, + is_high: false, + }); + assert_eq!(p.pivots[1].price, 85.0); + // A higher low of the same kind is ignored. + p.record_pivot(Pivot { + index: 5.0, + price: 88.0, + is_high: false, + }); + assert_eq!(p.pivots[1].price, 85.0); + } + + #[test] + fn batch_equals_streaming() { + let candles = zigzag(); + let batch = AndrewsPitchfork::new(2).unwrap().batch(&candles); + let mut b = AndrewsPitchfork::new(2).unwrap(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/central_pivot_range.rs b/crates/wickra-core/src/indicators/central_pivot_range.rs new file mode 100644 index 00000000..97aee8f2 --- /dev/null +++ b/crates/wickra-core/src/indicators/central_pivot_range.rs @@ -0,0 +1,171 @@ +//! Central Pivot Range (CPR) — the pivot plus its two central levels. + +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Output of [`CentralPivotRange`]: the pivot and the two central lines that +/// bracket it. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CentralPivotRangeOutput { + /// Pivot point `(high + low + close) / 3`. + pub pivot: f64, + /// Top central line — the higher of the two central levels. + pub tc: f64, + /// Bottom central line — the lower of the two central levels. + pub bc: f64, +} + +/// Central Pivot Range (CPR) — the classic pivot point flanked by two "central" +/// levels whose separation gauges the day's expected character. +/// +/// ```text +/// pivot = (high + low + close) / 3 +/// bc' = (high + low) / 2 +/// tc' = 2·pivot − bc' +/// TC = max(tc', bc'), BC = min(tc', bc') +/// ``` +/// +/// The CPR is computed from the **previous** period's bar (feed it completed +/// daily/weekly bars). The width of the range `TC − BC` is the headline read: a +/// **narrow** CPR signals a likely trending day (price has little balance area to +/// chew through), while a **wide** CPR signals a likely range-bound, balanced +/// day. Price opening above the whole range is bullish, below it bearish, inside +/// it neutral. The `tc'`/`bc'` formulas are symmetric about the pivot; this +/// implementation labels the larger as `TC` and the smaller as `BC` so `TC >= BC` +/// always holds. +/// +/// There are no parameters and no warmup — each completed bar yields one CPR. +/// Each `update` is O(1). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{Candle, Indicator, CentralPivotRange}; +/// +/// let mut indicator = CentralPivotRange::new(); +/// let prev_day = Candle::new(101.0, 110.0, 90.0, 105.0, 1_000.0, 0).unwrap(); +/// let cpr = indicator.update(prev_day).unwrap(); +/// assert!(cpr.tc >= cpr.bc); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct CentralPivotRange { + ready: bool, +} + +impl CentralPivotRange { + /// Construct a new Central Pivot Range. The indicator is parameter-free. + #[must_use] + pub const fn new() -> Self { + Self { ready: false } + } +} + +impl Indicator for CentralPivotRange { + type Input = Candle; + type Output = CentralPivotRangeOutput; + + fn update(&mut self, candle: Candle) -> Option { + let pivot = (candle.high + candle.low + candle.close) / 3.0; + let bc_raw = f64::midpoint(candle.high, candle.low); + let tc_raw = 2.0 * pivot - bc_raw; + let tc = tc_raw.max(bc_raw); + let bc = tc_raw.min(bc_raw); + self.ready = true; + Some(CentralPivotRangeOutput { pivot, tc, bc }) + } + + fn reset(&mut self) { + self.ready = false; + } + + fn warmup_period(&self) -> usize { + 1 + } + + fn is_ready(&self) -> bool { + self.ready + } + + fn name(&self) -> &'static str { + "CentralPivotRange" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(high: f64, low: f64, close: f64) -> Candle { + Candle::new_unchecked(close, high, low, close, 1_000.0, 0) + } + + #[test] + fn accessors_and_metadata() { + let cpr = CentralPivotRange::new(); + assert_eq!(cpr.warmup_period(), 1); + assert_eq!(cpr.name(), "CentralPivotRange"); + assert!(!cpr.is_ready()); + } + + #[test] + fn formula_reference_values() { + // H=110, L=90, C=105 -> pivot = 305/3; bc' = 100; tc' = 2*pivot - 100. + let out = CentralPivotRange::new() + .update(c(110.0, 90.0, 105.0)) + .unwrap(); + let pivot = 305.0 / 3.0; + let bc_raw = 100.0; + let tc_raw = 2.0 * pivot - bc_raw; + assert!((out.pivot - pivot).abs() < 1e-12); + assert!((out.tc - tc_raw.max(bc_raw)).abs() < 1e-12); + assert!((out.bc - tc_raw.min(bc_raw)).abs() < 1e-12); + } + + #[test] + fn tc_never_below_bc() { + let out = CentralPivotRange::new() + .update(c(200.0, 100.0, 150.0)) + .unwrap(); + assert!(out.tc >= out.bc); + } + + #[test] + fn constant_bar_collapses_range() { + // H = L = C -> pivot = bc' = tc' = the price; range collapses. + let out = CentralPivotRange::new() + .update(c(50.0, 50.0, 50.0)) + .unwrap(); + assert_eq!(out.pivot, 50.0); + assert_eq!(out.tc, 50.0); + assert_eq!(out.bc, 50.0); + } + + #[test] + fn ready_after_first_update() { + let mut cpr = CentralPivotRange::new(); + assert!(!cpr.is_ready()); + cpr.update(c(11.0, 9.0, 10.0)); + assert!(cpr.is_ready()); + } + + #[test] + fn reset_clears_state() { + let mut cpr = CentralPivotRange::new(); + cpr.update(c(11.0, 9.0, 10.0)); + assert!(cpr.is_ready()); + cpr.reset(); + assert!(!cpr.is_ready()); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..40) + .map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0)) + .collect(); + let batch = CentralPivotRange::new().batch(&candles); + let mut b = CentralPivotRange::new(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs index ced93dd2..e729b365 100644 --- a/crates/wickra-core/src/indicators/mod.rs +++ b/crates/wickra-core/src/indicators/mod.rs @@ -32,6 +32,7 @@ mod alpha; mod amihud_illiquidity; mod anchored_rsi; mod anchored_vwap; +mod andrews_pitchfork; mod apo; mod aroon; mod aroon_oscillator; @@ -68,6 +69,7 @@ mod calmar_ratio; mod camarilla_pivots; mod cci; mod center_of_gravity; +mod central_pivot_range; mod cfo; mod chaikin_oscillator; mod chaikin_volatility; @@ -257,6 +259,7 @@ mod modified_ma_stop; mod mom; mod morning_doji_star; mod morning_evening_star; +mod murrey_math_lines; mod natr; mod new_highs_new_lows; mod nrtr; @@ -286,6 +289,7 @@ mod percent_b; mod percentage_trailing_stop; mod pgo; mod piercing_dark_cloud; +mod pivot_reversal; mod plus_di; mod plus_dm; mod pmo; @@ -446,6 +450,7 @@ mod volume_oscillator; mod volume_profile; mod volume_rsi; mod volume_weighted_macd; +mod volume_weighted_sr; mod vortex; mod vpin; mod vpt; @@ -494,6 +499,7 @@ pub use alpha::Alpha; pub use amihud_illiquidity::AmihudIlliquidity; pub use anchored_rsi::AnchoredRsi; pub use anchored_vwap::AnchoredVwap; +pub use andrews_pitchfork::{AndrewsPitchfork, AndrewsPitchforkOutput}; pub use apo::Apo; pub use aroon::{Aroon, AroonOutput}; pub use aroon_oscillator::AroonOscillator; @@ -530,6 +536,7 @@ pub use calmar_ratio::CalmarRatio; pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput}; pub use cci::Cci; pub use center_of_gravity::CenterOfGravity; +pub use central_pivot_range::{CentralPivotRange, CentralPivotRangeOutput}; pub use cfo::Cfo; pub use chaikin_oscillator::ChaikinOscillator; pub use chaikin_volatility::ChaikinVolatility; @@ -719,6 +726,7 @@ pub use modified_ma_stop::{ModifiedMaStop, ModifiedMaStopOutput}; 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 natr::Natr; pub use new_highs_new_lows::NewHighsNewLows; pub use nrtr::{Nrtr, NrtrOutput}; @@ -748,6 +756,7 @@ pub use percent_b::PercentB; pub use percentage_trailing_stop::PercentageTrailingStop; pub use pgo::Pgo; pub use piercing_dark_cloud::PiercingDarkCloud; +pub use pivot_reversal::PivotReversal; pub use plus_di::PlusDi; pub use plus_dm::PlusDm; pub use pmo::Pmo; @@ -908,6 +917,7 @@ pub use volume_oscillator::VolumeOscillator; pub use volume_profile::{VolumeProfile, VolumeProfileOutput}; pub use volume_rsi::VolumeRsi; pub use volume_weighted_macd::{VolumeWeightedMacd, VolumeWeightedMacdOutput}; +pub use volume_weighted_sr::{VolumeWeightedSr, VolumeWeightedSrOutput}; pub use vortex::{Vortex, VortexOutput}; pub use vpin::Vpin; pub use vpt::VolumePriceTrend; @@ -1272,6 +1282,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[ "DemarkPivots", "WilliamsFractals", "ZigZag", + "CentralPivotRange", + "MurreyMathLines", + "AndrewsPitchfork", + "VolumeWeightedSr", + "PivotReversal", ], ), ( @@ -1540,6 +1555,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, 462, "FAMILIES total drifted from indicator count"); + assert_eq!(total, 467, "FAMILIES total drifted from indicator count"); } } diff --git a/crates/wickra-core/src/indicators/murrey_math_lines.rs b/crates/wickra-core/src/indicators/murrey_math_lines.rs new file mode 100644 index 00000000..dc4eaefe --- /dev/null +++ b/crates/wickra-core/src/indicators/murrey_math_lines.rs @@ -0,0 +1,272 @@ +//! Murrey Math Lines — the eighths grid over the recent trading range. + +use std::collections::VecDeque; + +use crate::error::{Error, Result}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Output of [`MurreyMathLines`]: the nine Murrey Math levels from the bottom +/// (`mm0_8`, ultimate support) to the top (`mm8_8`, ultimate resistance). +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct MurreyMathLinesOutput { + /// 8/8 — ultimate resistance (top of the frame). + pub mm8_8: f64, + /// 7/8 — "weak, stall and reverse" (overbought). + pub mm7_8: f64, + /// 6/8 — upper pivot / reversal line. + pub mm6_8: f64, + /// 5/8 — top of the normal trading range. + pub mm5_8: f64, + /// 4/8 — the major pivot (mean) line. + pub mm4_8: f64, + /// 3/8 — bottom of the normal trading range. + pub mm3_8: f64, + /// 2/8 — lower pivot / reversal line. + pub mm2_8: f64, + /// 1/8 — "weak, stall and reverse" (oversold). + pub mm1_8: f64, + /// 0/8 — ultimate support (bottom of the frame). + pub mm0_8: f64, +} + +/// Murrey Math Lines — T. H. Murrey's grid that divides the recent trading range +/// into eighths, each acting as support/resistance. +/// +/// ```text +/// HH = highest high over `period`, LL = lowest low over `period` +/// step = (HH − LL) / 8 +/// mm{i}_8 = LL + i · step for i = 0..8 +/// ``` +/// +/// Murrey Math (a Gann-derived framework) holds that price gravitates to and +/// reverses at the eighth divisions of its range. The **4/8** line is the major +/// pivot (mean); **0/8** and **8/8** are the strongest support and resistance; +/// **3/8** and **5/8** bound the "normal" trading range, while **1/8**/**7/8** are +/// the weak "stall and reverse" lines. This implementation uses the price-derived +/// eighths over a rolling high-low frame (the practical core of the method) rather +/// than Murrey's full octave-quantised frame sizing, so the levels track the +/// instrument's actual recent range. +/// +/// The first value lands after `period` inputs; each `update` rescans the frame in +/// O(`period`). A degenerate flat frame (`HH == LL`) collapses every line onto the +/// price. +/// +/// # Example +/// +/// ``` +/// use wickra_core::{Candle, Indicator, MurreyMathLines}; +/// +/// let mut indicator = MurreyMathLines::new(64).unwrap(); +/// let mut last = None; +/// for i in 0..120 { +/// let base = 100.0 + (f64::from(i) * 0.3).sin() * 10.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 MurreyMathLines { + period: usize, + highs: VecDeque, + lows: VecDeque, + last: Option, +} + +impl MurreyMathLines { + /// Construct Murrey Math Lines over a `period`-bar high-low frame. + /// + /// # Errors + /// + /// Returns [`Error::PeriodZero`] if `period == 0`. + pub fn new(period: usize) -> Result { + if period == 0 { + return Err(Error::PeriodZero); + } + Ok(Self { + period, + highs: VecDeque::with_capacity(period), + lows: VecDeque::with_capacity(period), + last: None, + }) + } + + /// Configured frame period. + pub const fn period(&self) -> usize { + self.period + } + + /// Current value if available. + pub const fn value(&self) -> Option { + self.last + } +} + +impl Indicator for MurreyMathLines { + type Input = Candle; + type Output = MurreyMathLinesOutput; + + fn update(&mut self, candle: Candle) -> Option { + if self.highs.len() == self.period { + self.highs.pop_front(); + self.lows.pop_front(); + } + self.highs.push_back(candle.high); + self.lows.push_back(candle.low); + if self.highs.len() < self.period { + return None; + } + let hh = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max); + let ll = self.lows.iter().copied().fold(f64::INFINITY, f64::min); + let step = (hh - ll) / 8.0; + let level = |i: f64| ll + i * step; + let out = MurreyMathLinesOutput { + mm0_8: level(0.0), + mm1_8: level(1.0), + mm2_8: level(2.0), + mm3_8: level(3.0), + mm4_8: level(4.0), + mm5_8: level(5.0), + mm6_8: level(6.0), + mm7_8: level(7.0), + mm8_8: level(8.0), + }; + self.last = Some(out); + Some(out) + } + + fn reset(&mut self) { + self.highs.clear(); + self.lows.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 { + "MurreyMathLines" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + fn c(high: f64, low: f64) -> Candle { + Candle::new_unchecked(low, high, low, f64::midpoint(high, low), 1_000.0, 0) + } + + #[test] + fn rejects_zero_period() { + assert!(matches!(MurreyMathLines::new(0), Err(Error::PeriodZero))); + } + + #[test] + fn accessors_and_metadata() { + let m = MurreyMathLines::new(64).unwrap(); + assert_eq!(m.period(), 64); + assert_eq!(m.warmup_period(), 64); + assert_eq!(m.name(), "MurreyMathLines"); + assert!(!m.is_ready()); + assert_eq!(m.value(), None); + } + + #[test] + fn first_emission_at_warmup_period() { + let mut m = MurreyMathLines::new(4).unwrap(); + let candles: Vec = (0..6) + .map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i))) + .collect(); + let out = m.batch(&candles); + for v in out.iter().take(3) { + assert!(v.is_none()); + } + assert!(out[3].is_some()); + } + + #[test] + fn eighths_are_evenly_spaced() { + // Frame [100, 180] over the window -> step = 10. + let mut m = MurreyMathLines::new(2).unwrap(); + let out = m + .batch(&[c(180.0, 100.0), c(180.0, 100.0)]) + .into_iter() + .flatten() + .last() + .unwrap(); + assert_relative_eq!(out.mm0_8, 100.0, epsilon = 1e-9); + assert_relative_eq!(out.mm4_8, 140.0, epsilon = 1e-9); + assert_relative_eq!(out.mm8_8, 180.0, epsilon = 1e-9); + assert_relative_eq!(out.mm1_8 - out.mm0_8, 10.0, epsilon = 1e-9); + } + + #[test] + fn levels_are_ordered() { + let mut m = MurreyMathLines::new(10).unwrap(); + let candles: Vec = (0..30) + .map(|i| { + c( + 110.0 + (f64::from(i) * 0.3).sin() * 8.0, + 90.0 + (f64::from(i) * 0.3).cos() * 8.0, + ) + }) + .collect(); + for o in m.batch(&candles).into_iter().flatten() { + assert!(o.mm0_8 <= o.mm4_8 && o.mm4_8 <= o.mm8_8); + assert!(o.mm3_8 <= o.mm5_8); + } + } + + #[test] + fn flat_frame_collapses() { + let mut m = MurreyMathLines::new(3).unwrap(); + let out = m + .batch(&[c(50.0, 50.0), c(50.0, 50.0), c(50.0, 50.0)]) + .into_iter() + .flatten() + .last() + .unwrap(); + assert_relative_eq!(out.mm0_8, 50.0, epsilon = 1e-12); + assert_relative_eq!(out.mm8_8, 50.0, epsilon = 1e-12); + } + + #[test] + fn reset_clears_state() { + let mut m = MurreyMathLines::new(4).unwrap(); + m.batch( + &(0..6) + .map(|i| c(101.0 + f64::from(i), 99.0 + f64::from(i))) + .collect::>(), + ); + assert!(m.is_ready()); + m.reset(); + assert!(!m.is_ready()); + assert_eq!(m.value(), None); + assert_eq!(m.update(c(101.0, 99.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 + (f64::from(i) * 0.25).cos() * 9.0, + ) + }) + .collect(); + let batch = MurreyMathLines::new(64).unwrap().batch(&candles); + let mut b = MurreyMathLines::new(64).unwrap(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/pivot_reversal.rs b/crates/wickra-core/src/indicators/pivot_reversal.rs new file mode 100644 index 00000000..4ff9e468 --- /dev/null +++ b/crates/wickra-core/src/indicators/pivot_reversal.rs @@ -0,0 +1,293 @@ +//! Pivot Reversal — a breakout signal off the most recent confirmed swing pivots. + +use std::collections::VecDeque; + +use crate::error::{Error, Result}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Pivot Reversal — emits a reversal **breakout signal** when price closes through +/// the most recently confirmed swing pivot. +/// +/// ```text +/// pivot high: a bar whose high is strictly above the `left` bars before and the +/// `right` bars after it (confirmed `right` bars late) +/// pivot low : the mirror on lows +/// signal = +1 when close crosses above the last confirmed pivot high +/// signal = −1 when close crosses below the last confirmed pivot low +/// signal = 0 otherwise +/// ``` +/// +/// Unlike [`WilliamsFractals`](crate::WilliamsFractals), which merely *marks* the +/// swing points, Pivot Reversal turns them into an actionable entry: once a swing +/// high is confirmed it becomes a breakout trigger — a close back above it signals +/// a bullish reversal — and likewise a close below a confirmed swing low signals a +/// bearish reversal. This is the logic of the classic "Pivot Reversal" strategy. +/// Signals fire only on the **crossing** bar, not while price sits beyond the +/// level. +/// +/// The first signal can appear once `left + right + 1` bars exist (a pivot needs +/// neighbours on both sides). The output is `+1` / `0` / `−1`. Each `update` is +/// O(`left + right`). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{Candle, Indicator, PivotReversal}; +/// +/// let mut indicator = PivotReversal::new(2, 2).unwrap(); +/// let mut fired = false; +/// for i in 0..60 { +/// let base = 100.0 + (f64::from(i) * 0.4).sin() * 5.0; +/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap(); +/// match indicator.update(c) { +/// Some(s) if s != 0.0 => fired = true, +/// _ => {} +/// } +/// } +/// let _ = fired; +/// ``` +#[derive(Debug, Clone)] +pub struct PivotReversal { + left: usize, + right: usize, + window: VecDeque, + pivot_high: Option, + pivot_low: Option, + prev_close: Option, + last: Option, +} + +impl PivotReversal { + /// Construct a Pivot Reversal with `left` bars before and `right` bars after + /// the pivot. + /// + /// # Errors + /// + /// Returns [`Error::PeriodZero`] if `left` or `right` is `0`. + pub fn new(left: usize, right: usize) -> Result { + if left == 0 || right == 0 { + return Err(Error::PeriodZero); + } + Ok(Self { + left, + right, + window: VecDeque::with_capacity(left + right + 1), + pivot_high: None, + pivot_low: None, + prev_close: None, + last: None, + }) + } + + /// Configured `(left, right)` strengths. + pub const fn params(&self) -> (usize, usize) { + (self.left, self.right) + } + + /// Most recent confirmed pivot-high level, if any. + pub const fn pivot_high(&self) -> Option { + self.pivot_high + } + + /// Most recent confirmed pivot-low level, if any. + pub const fn pivot_low(&self) -> Option { + self.pivot_low + } + + /// Current value if available. + pub const fn value(&self) -> Option { + self.last + } +} + +impl Indicator for PivotReversal { + type Input = Candle; + type Output = f64; + + fn update(&mut self, candle: Candle) -> Option { + let close = candle.close; + if self.window.len() == self.left + self.right + 1 { + self.window.pop_front(); + } + self.window.push_back(candle); + if self.window.len() < self.left + self.right + 1 { + self.prev_close = Some(close); + return None; + } + + // Confirm the pivot candidate sitting `right` bars back. + let cand = self.window[self.left]; + let is_high = self + .window + .iter() + .enumerate() + .all(|(i, c)| i == self.left || c.high < cand.high); + let is_low = self + .window + .iter() + .enumerate() + .all(|(i, c)| i == self.left || c.low > cand.low); + if is_high { + self.pivot_high = Some(cand.high); + } + if is_low { + self.pivot_low = Some(cand.low); + } + + // Breakout crossing of the latest confirmed pivots by the current close. + let mut signal = 0.0; + if let (Some(ph), Some(prev)) = (self.pivot_high, self.prev_close) { + if close > ph && prev <= ph { + signal = 1.0; + } + } + if let (Some(pl), Some(prev)) = (self.pivot_low, self.prev_close) { + if close < pl && prev >= pl { + signal = -1.0; + } + } + self.prev_close = Some(close); + self.last = Some(signal); + Some(signal) + } + + fn reset(&mut self) { + self.window.clear(); + self.pivot_high = None; + self.pivot_low = None; + self.prev_close = None; + self.last = None; + } + + fn warmup_period(&self) -> usize { + self.left + self.right + 1 + } + + fn is_ready(&self) -> bool { + self.last.is_some() + } + + fn name(&self) -> &'static str { + "PivotReversal" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + + fn c(high: f64, low: f64, close: f64) -> Candle { + Candle::new_unchecked(close, high, low, close, 1_000.0, 0) + } + + #[test] + fn rejects_zero_params() { + assert!(matches!(PivotReversal::new(0, 2), Err(Error::PeriodZero))); + assert!(matches!(PivotReversal::new(2, 0), Err(Error::PeriodZero))); + } + + #[test] + fn accessors_and_metadata() { + let p = PivotReversal::new(2, 2).unwrap(); + assert_eq!(p.params(), (2, 2)); + assert_eq!(p.warmup_period(), 5); + assert_eq!(p.name(), "PivotReversal"); + assert!(!p.is_ready()); + assert_eq!(p.value(), None); + assert_eq!(p.pivot_high(), None); + assert_eq!(p.pivot_low(), None); + } + + #[test] + fn first_emission_at_warmup_period() { + let mut p = PivotReversal::new(1, 1).unwrap(); + let out = p.batch(&[c(10.0, 9.0, 9.5), c(12.0, 11.0, 11.5), c(10.0, 9.0, 9.5)]); + assert!(out[0].is_none()); + assert!(out[1].is_none()); + assert!(out[2].is_some()); + } + + #[test] + fn confirms_pivot_high() { + // bar1 is a local high; once bar2 arrives it is confirmed. + let mut p = PivotReversal::new(1, 1).unwrap(); + p.batch(&[c(10.0, 9.0, 9.5), c(12.0, 11.0, 11.5), c(10.0, 9.0, 9.5)]); + assert_eq!(p.pivot_high(), Some(12.0)); + } + + #[test] + fn confirms_pivot_low() { + let mut p = PivotReversal::new(1, 1).unwrap(); + p.batch(&[c(12.0, 11.0, 11.5), c(10.0, 8.0, 8.5), c(12.0, 11.0, 11.5)]); + assert_eq!(p.pivot_low(), Some(8.0)); + } + + #[test] + fn breakout_above_pivot_high_signals_plus_one() { + let mut p = PivotReversal::new(1, 1).unwrap(); + // Form a pivot high at 12, then a close above 12 crosses it. + let candles = [ + c(10.0, 9.0, 9.5), // index 0 + c(12.0, 11.0, 11.5), // pivot-high candidate + c(10.0, 9.0, 9.5), // confirms pivot high = 12 + c(11.0, 9.0, 9.0), // close 9.0 (below 12) + c(14.0, 12.5, 13.0), // close 13.0 > 12 and prev 9.0 <= 12 -> +1 + ]; + let out = p.batch(&candles); + assert_eq!(out.last().unwrap(), &Some(1.0)); + } + + #[test] + fn breakdown_below_pivot_low_signals_minus_one() { + let mut p = PivotReversal::new(1, 1).unwrap(); + let candles = [ + c(12.0, 11.0, 11.5), + c(10.0, 8.0, 8.5), // pivot-low candidate + c(12.0, 11.0, 11.5), // confirms pivot low = 8 + c(12.0, 9.0, 11.0), // close 11 (above 8) + c(9.0, 6.0, 7.0), // close 7 < 8 and prev 11 >= 8 -> -1 + ]; + let out = p.batch(&candles); + assert_eq!(out.last().unwrap(), &Some(-1.0)); + } + + #[test] + fn no_break_is_zero() { + let mut p = PivotReversal::new(1, 1).unwrap(); + let candles = [ + c(10.0, 9.0, 9.5), + c(12.0, 11.0, 11.5), + c(10.0, 9.0, 9.5), + c(10.5, 9.0, 9.8), + ]; + let out = p.batch(&candles); + assert_eq!(out.last().unwrap(), &Some(0.0)); + } + + #[test] + fn reset_clears_state() { + let mut p = PivotReversal::new(1, 1).unwrap(); + p.batch(&[c(10.0, 9.0, 9.5), c(12.0, 11.0, 11.5), c(10.0, 9.0, 9.5)]); + assert!(p.is_ready()); + p.reset(); + assert!(!p.is_ready()); + assert_eq!(p.value(), None); + assert_eq!(p.pivot_high(), None); + } + + #[test] + fn batch_equals_streaming() { + let candles: Vec = (0..80) + .map(|i| { + let base = 100.0 + (f64::from(i) * 0.4).sin() * 6.0; + c(base + 1.0, base - 1.0, base) + }) + .collect(); + let batch = PivotReversal::new(2, 2).unwrap().batch(&candles); + let mut b = PivotReversal::new(2, 2).unwrap(); + let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/volume_weighted_sr.rs b/crates/wickra-core/src/indicators/volume_weighted_sr.rs new file mode 100644 index 00000000..212b9d23 --- /dev/null +++ b/crates/wickra-core/src/indicators/volume_weighted_sr.rs @@ -0,0 +1,281 @@ +//! Volume-Weighted Support/Resistance — a volume-weighted high/low band. + +use std::collections::VecDeque; + +use crate::error::{Error, Result}; +use crate::ohlcv::Candle; +use crate::traits::Indicator; + +/// Output of [`VolumeWeightedSr`]: the volume-weighted support and resistance +/// levels over the lookback. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct VolumeWeightedSrOutput { + /// Volume-weighted average low — the support level. + pub support: f64, + /// Volume-weighted average high — the resistance level. + pub resistance: f64, +} + +/// Volume-Weighted Support/Resistance — a band whose edges are the +/// **volume-weighted** average of the recent highs (resistance) and lows +/// (support), so the levels gravitate toward the prices where trading actually +/// happened. +/// +/// ```text +/// support = Σ(low_i · volume_i) / Σ volume_i over the window +/// resistance = Σ(high_i · volume_i) / Σ volume_i over the window +/// ``` +/// +/// Plain high/low channels (e.g. [`Donchian`](crate::Donchian)) weight every bar +/// equally, so a thin spike sets the boundary. Volume-weighting pulls the support +/// and resistance toward the highs and lows that carried real volume — the prices +/// the market agreed mattered — giving levels that tend to hold better. The +/// distance between the two is a volume-aware range estimate. If the window's +/// volume is all zero the band falls back to the equal-weighted average high and +/// low. +/// +/// The first value lands after `period` inputs; each `update` is O(1). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{Candle, Indicator, VolumeWeightedSr}; +/// +/// let mut indicator = VolumeWeightedSr::new(20).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 + 2.0, base - 2.0, base, 1_000.0 + f64::from(i), 0).unwrap(); +/// last = indicator.update(c); +/// } +/// assert!(last.is_some()); +/// ``` +#[derive(Debug, Clone)] +pub struct VolumeWeightedSr { + period: usize, + highs: VecDeque, + lows: VecDeque, + volumes: VecDeque, + sum_hv: f64, + sum_lv: f64, + sum_v: f64, + sum_h: f64, + sum_l: f64, + last: Option, +} + +impl VolumeWeightedSr { + /// Construct a volume-weighted S/R band over `period` bars. + /// + /// # Errors + /// + /// Returns [`Error::PeriodZero`] if `period == 0`. + pub fn new(period: usize) -> Result { + if period == 0 { + return Err(Error::PeriodZero); + } + Ok(Self { + period, + highs: VecDeque::with_capacity(period), + lows: VecDeque::with_capacity(period), + volumes: VecDeque::with_capacity(period), + sum_hv: 0.0, + sum_lv: 0.0, + sum_v: 0.0, + sum_h: 0.0, + sum_l: 0.0, + last: None, + }) + } + + /// Configured lookback period. + pub const fn period(&self) -> usize { + self.period + } + + /// Current value if available. + pub const fn value(&self) -> Option { + self.last + } +} + +impl Indicator for VolumeWeightedSr { + type Input = Candle; + type Output = VolumeWeightedSrOutput; + + fn update(&mut self, candle: Candle) -> Option { + if self.highs.len() == self.period { + let h = self.highs.pop_front().expect("non-empty"); + let l = self.lows.pop_front().expect("non-empty"); + let v = self.volumes.pop_front().expect("non-empty"); + self.sum_hv -= h * v; + self.sum_lv -= l * v; + self.sum_v -= v; + self.sum_h -= h; + self.sum_l -= l; + } + self.highs.push_back(candle.high); + self.lows.push_back(candle.low); + self.volumes.push_back(candle.volume); + self.sum_hv += candle.high * candle.volume; + self.sum_lv += candle.low * candle.volume; + self.sum_v += candle.volume; + self.sum_h += candle.high; + self.sum_l += candle.low; + if self.highs.len() < self.period { + return None; + } + let n = self.period as f64; + let (support, resistance) = if self.sum_v > 0.0 { + (self.sum_lv / self.sum_v, self.sum_hv / self.sum_v) + } else { + (self.sum_l / n, self.sum_h / n) + }; + let out = VolumeWeightedSrOutput { + support, + resistance, + }; + self.last = Some(out); + Some(out) + } + + fn reset(&mut self) { + self.highs.clear(); + self.lows.clear(); + self.volumes.clear(); + self.sum_hv = 0.0; + self.sum_lv = 0.0; + self.sum_v = 0.0; + self.sum_h = 0.0; + self.sum_l = 0.0; + self.last = None; + } + + fn warmup_period(&self) -> usize { + self.period + } + + fn is_ready(&self) -> bool { + self.last.is_some() + } + + fn name(&self) -> &'static str { + "VolumeWeightedSr" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + fn c(high: f64, low: f64, volume: f64) -> Candle { + Candle::new_unchecked(low, high, low, f64::midpoint(high, low), volume, 0) + } + + #[test] + fn rejects_zero_period() { + assert!(matches!(VolumeWeightedSr::new(0), Err(Error::PeriodZero))); + } + + #[test] + fn accessors_and_metadata() { + let v = VolumeWeightedSr::new(20).unwrap(); + assert_eq!(v.period(), 20); + assert_eq!(v.warmup_period(), 20); + assert_eq!(v.name(), "VolumeWeightedSr"); + assert!(!v.is_ready()); + assert_eq!(v.value(), None); + } + + #[test] + fn first_emission_at_warmup_period() { + let mut v = VolumeWeightedSr::new(4).unwrap(); + let candles: Vec = (0..6).map(|_| c(102.0, 98.0, 1_000.0)).collect(); + let out = v.batch(&candles); + for o in out.iter().take(3) { + assert!(o.is_none()); + } + assert!(out[3].is_some()); + } + + #[test] + fn support_below_resistance() { + let mut v = VolumeWeightedSr::new(10).unwrap(); + let candles: Vec = (0..30) + .map(|i| { + c( + 110.0 + (f64::from(i) * 0.3).sin() * 5.0, + 90.0 + (f64::from(i) * 0.3).cos() * 5.0, + 1_000.0 + f64::from(i), + ) + }) + .collect(); + for o in v.batch(&candles).into_iter().flatten() { + assert!(o.support <= o.resistance); + } + } + + #[test] + fn weights_toward_high_volume_bars() { + // Three low-volume bars at [98,102] and one heavy bar at [108,112]; the + // resistance should be pulled toward the heavy bar's high. + let mut v = VolumeWeightedSr::new(4).unwrap(); + let candles = [ + c(102.0, 98.0, 100.0), + c(102.0, 98.0, 100.0), + c(102.0, 98.0, 100.0), + c(112.0, 108.0, 9_000.0), + ]; + let out = v.batch(&candles).into_iter().flatten().last().unwrap(); + // Volume-weighted resistance sits much closer to 112 than the simple mean (104.5). + assert!( + out.resistance > 108.0, + "resistance {} should lean to the heavy bar", + out.resistance + ); + } + + #[test] + fn zero_volume_falls_back_to_equal_weight() { + let mut v = VolumeWeightedSr::new(3).unwrap(); + let candles = [ + c(102.0, 98.0, 0.0), + c(104.0, 96.0, 0.0), + c(106.0, 94.0, 0.0), + ]; + let out = v.batch(&candles).into_iter().flatten().last().unwrap(); + // Equal-weight averages: high mean = 104, low mean = 96. + assert_relative_eq!(out.resistance, 104.0, epsilon = 1e-9); + assert_relative_eq!(out.support, 96.0, epsilon = 1e-9); + } + + #[test] + fn reset_clears_state() { + let mut v = VolumeWeightedSr::new(4).unwrap(); + v.batch(&(0..6).map(|_| c(102.0, 98.0, 1_000.0)).collect::>()); + assert!(v.is_ready()); + v.reset(); + assert!(!v.is_ready()); + assert_eq!(v.value(), None); + assert_eq!(v.update(c(102.0, 98.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 + (f64::from(i) * 0.25).cos() * 9.0, + 1_000.0 + f64::from(i), + ) + }) + .collect(); + let batch = VolumeWeightedSr::new(20).unwrap().batch(&candles); + let mut b = VolumeWeightedSr::new(20).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 44568c38..d83aa05f 100644 --- a/crates/wickra-core/src/lib.rs +++ b/crates/wickra-core/src/lib.rs @@ -60,14 +60,15 @@ pub use indicators::{ AcceleratorOscillator, AdOscillator, AdVolumeLine, AdaptiveCci, AdaptiveCycle, AdaptiveLaguerreFilter, AdaptiveRsi, Adl, AdvanceBlock, AdvanceDecline, AdvanceDeclineRatio, Adx, AdxOutput, Adxr, Alligator, AlligatorOutput, Alma, Alpha, AmihudIlliquidity, AnchoredRsi, - AnchoredVwap, Apo, Aroon, AroonOscillator, AroonOutput, Atr, AtrBands, AtrBandsOutput, - AtrRatchet, AtrRatchetOutput, AtrTrailingStop, AutoFib, AutoFibOutput, Autocorrelation, - AutocorrelationPeriodogram, AverageDailyRange, AverageDrawdown, AvgPrice, AwesomeOscillator, - AwesomeOscillatorHistogram, BalanceOfPower, BandpassFilter, Bat, BeltHold, Beta, - BetaNeutralSpread, BetterVolume, BipowerVariation, BodySizePct, BollingerBands, - BollingerBandwidth, BollingerOutput, BomarBands, BomarBandsOutput, BreadthThrust, Breakaway, - BullishPercentIndex, Butterfly, CalendarSpread, CalmarRatio, Camarilla, CamarillaPivotsOutput, - Cci, CenterOfGravity, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, + AnchoredVwap, AndrewsPitchfork, AndrewsPitchforkOutput, Apo, Aroon, AroonOscillator, + AroonOutput, Atr, AtrBands, AtrBandsOutput, AtrRatchet, AtrRatchetOutput, AtrTrailingStop, + AutoFib, AutoFibOutput, Autocorrelation, AutocorrelationPeriodogram, AverageDailyRange, + AverageDrawdown, AvgPrice, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, + BandpassFilter, Bat, BeltHold, Beta, BetaNeutralSpread, BetterVolume, BipowerVariation, + BodySizePct, BollingerBands, BollingerBandwidth, BollingerOutput, BomarBands, BomarBandsOutput, + BreadthThrust, Breakaway, BullishPercentIndex, Butterfly, CalendarSpread, CalmarRatio, + Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, CentralPivotRange, + CentralPivotRangeOutput, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit, ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, CloseVsOpen, ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput, ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, @@ -107,25 +108,25 @@ pub use indicators::{ MatHold, MatchingLow, MaxDrawdown, McClellanOscillator, McClellanSummationIndex, McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianChannelOutput, MedianMa, MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop, - ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, Natr, NewHighsNewLows, Nrtr, - NrtrOutput, Nvi, OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, - OpeningMarubozu, OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, - OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap, - OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, - PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa, PercentB, - PercentageTrailingStop, Pgo, PiercingDarkCloud, 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, SortinoRatio, - SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient, SpreadBollingerBands, + ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines, + MurreyMathLinesOutput, Natr, NewHighsNewLows, Nrtr, NrtrOutput, Nvi, OIPriceDivergence, + OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange, + OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, + OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn, + OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, + PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud, + 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, + SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient, SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi, Stochastic, StochasticCci, StochasticOutput, SuperSmoother, @@ -143,12 +144,12 @@ pub use indicators::{ ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya, VolatilityCone, VolatilityConeOutput, VolatilityOfVolatility, VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend, VolumeProfile, - VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput, Vortex, - VortexOutput, Vpin, Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, - WaveTrend, WaveTrendOutput, Wedge, WeightedClose, WickRatio, WilliamsFractals, - WilliamsFractalsOutput, WilliamsR, WinRate, Wma, WoodiePivots, WoodiePivotsOutput, - YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput, - Zlema, FAMILIES, T3, + VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput, VolumeWeightedSr, + VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands, + VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, WaveTrend, WaveTrendOutput, Wedge, + WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma, + WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd, + ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3, }; // `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own // line so the indicator-count tooling (which scans the braced block above and diff --git a/docs/README.md b/docs/README.md index 5b68898d..f60c2056 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 **462 indicators** across +- A per-indicator deep dive for every one of the **467 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 fe98b651..ab45cc97 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, Aroon, AroonOscillator, Atr, AtrBands, AtrRatchet, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BetterVolume, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, Cci, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, ElderRay, ElderSafeZone, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KaseDevStop, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, ModifiedMaStop, MorningDojiStar, MorningEveningStar, Natr, Nrtr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PlusDi, PlusDm, ProjectionBands, ProjectionOscillator, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeBasedStop, TimeOfDayReturnProfile, TpoProfile, TradeVolumeIndex, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VolatilityCone, VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeRsi, VolumeWeightedMacd, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, Wad, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag}; +use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, AdaptiveCci, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, AndrewsPitchfork, Aroon, AroonOscillator, Atr, AtrBands, AtrRatchet, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BetterVolume, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, Cci, CentralPivotRange, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, ElderRay, ElderSafeZone, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KaseDevStop, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, ModifiedMaStop, MorningDojiStar, MorningEveningStar, MurreyMathLines, Natr, Nrtr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PivotReversal, PlusDi, PlusDm, ProjectionBands, ProjectionOscillator, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeBasedStop, TimeOfDayReturnProfile, TpoProfile, TradeVolumeIndex, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VolatilityCone, VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeRsi, VolumeWeightedMacd, VolumeWeightedSr, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, Wad, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag}; /// Convert a flat `f64` stream into a `Vec` by chunking it into /// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV @@ -433,4 +433,11 @@ fuzz_target!(|data: Vec| { drive(FibExtension::new, &candles); drive(FibRetracement::new, &candles); + // --- Pivots & S/R --- + drive(CentralPivotRange::new, &candles); + drive(|| MurreyMathLines::new(4).unwrap(), &candles); + drive(|| AndrewsPitchfork::new(2).unwrap(), &candles); + drive(|| VolumeWeightedSr::new(3).unwrap(), &candles); + drive(|| PivotReversal::new(1, 1).unwrap(), &candles); + });