diff --git a/CHANGELOG.md b/CHANGELOG.md index 80a63f6d..b52fa6fe 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] +- **Open-Interest Momentum** — rate-of-change of open interest over a rolling window (`OpenInterestMomentum`). +- **Funding-Implied APR** — annualised funding rate (per-interval funding times intervals per year) (`FundingImpliedApr`). +- **Perpetual Premium Index** — relative premium of the mark price over the index price (`PerpetualPremiumIndex`). +- **OI-to-Volume Ratio** — open interest divided by taker volume (position turnover proxy) (`OiToVolumeRatio`). +- **Estimated Leverage Ratio** — open interest divided by aggregate long+short position size (leverage proxy) (`EstimatedLeverageRatio`). ## [0.7.0] - 2026-06-08 - **Hasbrouck Information Share** — variance-ratio proxy for each venue's share of price discovery (Hasbrouck information share) (`HasbrouckInformationShare`). diff --git a/README.md b/README.md index 940838df..571a9f67 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 488 indicators; start at the + every one of the 493 indicators; start at the [indicators overview](https://docs.wickra.org/Indicators-Overview). - **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods), [streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch), @@ -66,7 +66,7 @@ an afterthought — **live, tick-by-tick data** — without giving up the breadt a full batch library, and without making you reimplement your indicators four times to get there. -- **The biggest streaming-native catalogue, period.** 488 indicators across 24 +- **The biggest streaming-native catalogue, period.** 493 indicators across 24 families — candlesticks, harmonic & chart patterns, market profile, market breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and @@ -77,7 +77,7 @@ times to get there. - **Correct by construction, not by hope.** Every `update` validates its input, runs a real warmup, and returns an `Option` so a single bad tick can't silently poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered - for all 488 indicators**. + for all 493 indicators**. - **Orders of magnitude faster where it counts.** In streaming Wickra is **11–56×** faster than the only other incremental peer and **thousands of times** faster than recompute-on-every-tick libraries. On batch it wins several rows outright @@ -95,7 +95,7 @@ Every other library forces one of those compromises. Wickra doesn't: | Library | Install | Streaming | Languages | Indicators | Active | |------------------|-------------|-------------|-----------------------------|-----------:|--------| -| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **488** | **yes** | +| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **493** | **yes** | | kand | clean | yes | Python · WASM · Rust | ~60 | yes | | ta-rs | clean | yes | Rust only | ~30 | stale | | yata | clean | partial | Rust only | ~35 | yes | @@ -128,7 +128,7 @@ Full tables (Rust + Python, streaming + batch) and how to reproduce them live in ## Indicators -488 streaming-first indicators across twenty-four families. Every one passes the +493 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). @@ -154,7 +154,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview). | Harmonic Patterns | AB=CD, Gartley, Butterfly, Bat, Crab, Shark, Cypher, Three Drives | | Fibonacci | Fibonacci Retracement, Fibonacci Extension, Fibonacci Projection, Auto-Fibonacci, Golden Pocket, Fibonacci Confluence, Fibonacci Fan, Fibonacci Arcs, Fibonacci Channel, Fibonacci Time Zones | | Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint, Order Flow Imbalance, VPIN, Amihud Illiquidity, Roll Measure, Trade-Sign Autocorrelation, Hasbrouck Information Share | -| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread | +| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread, Estimated Leverage Ratio, OI-to-Volume Ratio, Perpetual Premium Index, Funding-Implied APR, Open-Interest Momentum | | Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range | | Market Breadth | Advance/Decline Line, Advance/Decline Ratio, Advance/Decline Volume Line, McClellan Oscillator, McClellan Summation Index, TRIN / Arms Index, Breadth Thrust, New Highs - New Lows, High-Low Index, Percent Above Moving Average, Up/Down Volume Ratio, Bullish Percent Index, Cumulative Volume Index, Absolute Breadth Index, TICK Index | | Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) | @@ -237,7 +237,7 @@ A Python live-trading example using the public `websockets` package lives at ``` wickra/ ├── crates/ -│ ├── wickra-core/ core engine + all 488 indicators +│ ├── wickra-core/ core engine + all 493 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 2994fa8e..c9075e10 100644 --- a/bindings/node/__tests__/indicators.test.js +++ b/bindings/node/__tests__/indicators.test.js @@ -1443,6 +1443,56 @@ test('derivatives reject bad input', () => { assert.throws(() => new wickra.FundingBasis().update(100, 0)); }); +test('B16 derivatives reference values', () => { + // Estimated leverage: oi / (long + short) = 200 / 100 = 2. + assert.ok(Math.abs(new wickra.EstimatedLeverageRatio().update(200, 60, 40) - 2.0) < 1e-12); + // OI-to-volume: oi / (buy + sell) = 100 / 50 = 2. + assert.ok(Math.abs(new wickra.OiToVolumeRatio().update(100, 30, 20) - 2.0) < 1e-12); + // Perpetual premium: (mark - index) / index = 0.5 / 100 = 0.005. + assert.ok(Math.abs(new wickra.PerpetualPremiumIndex().update(100.5, 100.0) - 0.005) < 1e-12); + // Funding-implied APR: rate * intervals = 0.0001 * 1095 = 0.1095. + assert.ok(Math.abs(new wickra.FundingImpliedApr(1095).update(0.0001) - 0.1095) < 1e-12); + // Open-interest momentum (period 2): warmup then ROC% = 100*(120 - 100)/100 = 20. + const oim = new wickra.OpenInterestMomentum(2); + assert.equal(oim.update(100), null); + assert.equal(oim.update(110), null); + assert.ok(Math.abs(oim.update(120) - 20.0) < 1e-12); +}); + +test('B16 derivatives streaming matches batch', () => { + const n = 30; + const oi = Array.from({ length: n }, (_, i) => 1000 + 50 * Math.sin(i * 0.3)); + const longSz = Array.from({ length: n }, (_, i) => 600 + 20 * Math.cos(i * 0.2)); + const shortSz = Array.from({ length: n }, (_, i) => 400 + 15 * Math.sin(i * 0.4)); + const buy = Array.from({ length: n }, (_, i) => 300 + 10 * Math.sin(i * 0.5)); + const sell = Array.from({ length: n }, (_, i) => 250 + 12 * Math.cos(i * 0.35)); + const index = Array.from({ length: n }, (_, i) => 100 + Math.sin(i * 0.2)); + const mark = Array.from({ length: n }, (_, i) => index[i] + 0.05 * Math.cos(i * 0.3)); + const rate = Array.from({ length: n }, (_, i) => 0.0001 * Math.sin(i * 0.3)); + const cmp = (batch, s, i) => + assert.ok((s === null && Number.isNaN(batch[i])) || Math.abs(s - batch[i]) < 1e-12, `mismatch at ${i}`); + + let b = new wickra.EstimatedLeverageRatio().batch(oi, longSz, shortSz); + let st = new wickra.EstimatedLeverageRatio(); + for (let i = 0; i < n; i++) cmp(b, st.update(oi[i], longSz[i], shortSz[i]), i); + + b = new wickra.OiToVolumeRatio().batch(oi, buy, sell); + st = new wickra.OiToVolumeRatio(); + for (let i = 0; i < n; i++) cmp(b, st.update(oi[i], buy[i], sell[i]), i); + + b = new wickra.PerpetualPremiumIndex().batch(mark, index); + st = new wickra.PerpetualPremiumIndex(); + for (let i = 0; i < n; i++) cmp(b, st.update(mark[i], index[i]), i); + + b = new wickra.FundingImpliedApr(1095).batch(rate); + st = new wickra.FundingImpliedApr(1095); + for (let i = 0; i < n; i++) cmp(b, st.update(rate[i]), i); + + b = new wickra.OpenInterestMomentum(10).batch(oi); + st = new wickra.OpenInterestMomentum(10); + for (let i = 0; i < n; i++) cmp(b, st.update(oi[i]), i); +}); + test('market breadth: AdvanceDecline reference values', () => { // A breadth tick is the universe as parallel arrays; the sign of `change` // classifies each symbol as advancing / declining / unchanged. diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index 99dee94f..e58b0423 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -4544,6 +4544,51 @@ export declare class CalendarSpread { isReady(): boolean warmupPeriod(): number } +export type EstimatedLeverageRatioNode = EstimatedLeverageRatio +export declare class EstimatedLeverageRatio { + constructor() + update(openInterest: number, longSize: number, shortSize: number): number | null + batch(openInterest: Array, longSize: Array, shortSize: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type OiToVolumeRatioNode = OiToVolumeRatio +export declare class OiToVolumeRatio { + constructor() + update(openInterest: number, takerBuyVolume: number, takerSellVolume: number): number | null + batch(openInterest: Array, takerBuyVolume: Array, takerSellVolume: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type PerpetualPremiumIndexNode = PerpetualPremiumIndex +export declare class PerpetualPremiumIndex { + constructor() + update(markPrice: number, indexPrice: number): number | null + batch(markPrice: Array, indexPrice: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type FundingImpliedAprNode = FundingImpliedApr +export declare class FundingImpliedApr { + constructor(intervalsPerYear: number) + update(fundingRate: number): number | null + batch(fundingRate: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} +export type OpenInterestMomentumNode = OpenInterestMomentum +export declare class OpenInterestMomentum { + constructor(period: number) + update(openInterest: number): number | null + batch(openInterest: Array): Array + reset(): void + isReady(): boolean + warmupPeriod(): number +} export type AdvanceDeclineNode = AdvanceDecline export declare class AdvanceDecline { constructor() diff --git a/bindings/node/index.js b/bindings/node/index.js index 740418ef..6cecb793 100644 --- a/bindings/node/index.js +++ b/bindings/node/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, LogReturn, RealizedVolatility, RollingIqr, RollingPercentileRank, TrendLabel, WinRate, Expectancy, SWMA, GMA, EHMA, MedianMA, AdaptiveLaguerre, DisparityIndex, FisherRSI, RSX, DynamicMomentumIndex, TREND_STRENGTH_INDEX, TsfOscillator, BipowerVariation, JARQUEBERA, ROLLINGMINMAX, HIGHPASS, REFLEX, TRENDFLEX, CTI, ADAPTIVERSI, UNIVERSALOSC, BANDPASS, EVENBETTERSINE, AUTOCORRPGRAM, SHANNONENT, SAMPLEENT, EwmaVolatility, Garch11, VolatilityOfVolatility, VolatilityCone, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, KendallTau, BetaNeutralSpread, HasbrouckInformationShare, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, VolatilityRatio, ProjectionOscillator, TimeBasedStop, ADAPTIVECCI, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, GD, HoltWinters, RMI, DerivativeOscillator, MacdHistogram, PpoHistogram, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, KaseDevStop, ElderSafeZone, AtrRatchet, Nrtr, ModifiedMaStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, QuartileBands, BomarBands, MedianChannel, ProjectionBands, CentralPivotRange, MurreyMathLines, AndrewsPitchfork, VolumeWeightedSr, PivotReversal, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDDWave, TDMovingAverage, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, HeikinAshiOscillator, ThreeLineBreak, SmoothedHeikinAshi, Equivolume, CandleVolume, FryPanBottom, DumplingTop, NewPriceLines, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, DoubleTopBottom, TripleTopBottom, HeadAndShoulders, Triangle, Wedge, FlagPennant, RectangleRange, CupAndHandle, Abcd, Gartley, Butterfly, Bat, Crab, Shark, Cypher, ThreeDrives, TDCamouflage, TDClop, TDClopwin, TDPropulsion, TDTrap, Tristar, HaramiCross, TowerTopBottom, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, TradeSignAutocorrelation, Pin, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn, FibRetracement, FibExtension, FibProjection, AutoFib, GoldenPocket, FibConfluence, FibFan, FibArcs, FibChannel, FibTimeZones, VolumeRsi, Wad, TwiggsMoneyFlow, TradeVolumeIndex, IntradayIntensity, BetterVolume, VolumeWeightedMacd } = nativeBinding +const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, LogReturn, RealizedVolatility, RollingIqr, RollingPercentileRank, TrendLabel, WinRate, Expectancy, SWMA, GMA, EHMA, MedianMA, AdaptiveLaguerre, DisparityIndex, FisherRSI, RSX, DynamicMomentumIndex, TREND_STRENGTH_INDEX, TsfOscillator, BipowerVariation, JARQUEBERA, ROLLINGMINMAX, HIGHPASS, REFLEX, TRENDFLEX, CTI, ADAPTIVERSI, UNIVERSALOSC, BANDPASS, EVENBETTERSINE, AUTOCORRPGRAM, SHANNONENT, SAMPLEENT, EwmaVolatility, Garch11, VolatilityOfVolatility, VolatilityCone, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, KendallTau, BetaNeutralSpread, HasbrouckInformationShare, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, VolatilityRatio, ProjectionOscillator, TimeBasedStop, ADAPTIVECCI, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, GD, HoltWinters, RMI, DerivativeOscillator, MacdHistogram, PpoHistogram, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, KaseDevStop, ElderSafeZone, AtrRatchet, Nrtr, ModifiedMaStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, QuartileBands, BomarBands, MedianChannel, ProjectionBands, CentralPivotRange, MurreyMathLines, AndrewsPitchfork, VolumeWeightedSr, PivotReversal, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDDWave, TDMovingAverage, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, HeikinAshiOscillator, ThreeLineBreak, SmoothedHeikinAshi, Equivolume, CandleVolume, FryPanBottom, DumplingTop, NewPriceLines, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, DoubleTopBottom, TripleTopBottom, HeadAndShoulders, Triangle, Wedge, FlagPennant, RectangleRange, CupAndHandle, Abcd, Gartley, Butterfly, Bat, Crab, Shark, Cypher, ThreeDrives, TDCamouflage, TDClop, TDClopwin, TDPropulsion, TDTrap, Tristar, HaramiCross, TowerTopBottom, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, TradeSignAutocorrelation, Pin, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, EstimatedLeverageRatio, OiToVolumeRatio, PerpetualPremiumIndex, FundingImpliedApr, OpenInterestMomentum, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn, FibRetracement, FibExtension, FibProjection, AutoFib, GoldenPocket, FibConfluence, FibFan, FibArcs, FibChannel, FibTimeZones, VolumeRsi, Wad, TwiggsMoneyFlow, TradeVolumeIndex, IntradayIntensity, BetterVolume, VolumeWeightedMacd } = nativeBinding module.exports.version = version module.exports.SMA = SMA @@ -737,6 +737,11 @@ module.exports.TakerBuySellRatio = TakerBuySellRatio module.exports.LiquidationFeatures = LiquidationFeatures module.exports.TermStructureBasis = TermStructureBasis module.exports.CalendarSpread = CalendarSpread +module.exports.EstimatedLeverageRatio = EstimatedLeverageRatio +module.exports.OiToVolumeRatio = OiToVolumeRatio +module.exports.PerpetualPremiumIndex = PerpetualPremiumIndex +module.exports.FundingImpliedApr = FundingImpliedApr +module.exports.OpenInterestMomentum = OpenInterestMomentum module.exports.AdvanceDecline = AdvanceDecline module.exports.AdvanceDeclineRatio = AdvanceDeclineRatio module.exports.AdVolumeLine = AdVolumeLine diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index 2bc34f3e..0567b72e 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -14450,6 +14450,50 @@ fn deriv_taker(taker_buy_volume: f64, taker_sell_volume: f64) -> napi::Result napi::Result { + wc::DerivativesTick::new( + 0.0, + 1.0, + 1.0, + 1.0, + open_interest, + long_size, + short_size, + 0.0, + 0.0, + 0.0, + 0.0, + 0, + ) + .map_err(map_err) +} + +fn deriv_oi_taker( + open_interest: f64, + taker_buy_volume: f64, + taker_sell_volume: f64, +) -> napi::Result { + wc::DerivativesTick::new( + 0.0, + 1.0, + 1.0, + 1.0, + open_interest, + 0.0, + 0.0, + taker_buy_volume, + taker_sell_volume, + 0.0, + 0.0, + 0, + ) + .map_err(map_err) +} + fn deriv_liquidation( long_liquidation: f64, short_liquidation: f64, @@ -15165,6 +15209,288 @@ impl CalendarSpreadNode { } } +// Estimated leverage ratio: open interest over aggregate long+short size. +#[napi(js_name = "EstimatedLeverageRatio")] +pub struct EstimatedLeverageRatioNode { + inner: wc::EstimatedLeverageRatio, +} + +impl Default for EstimatedLeverageRatioNode { + fn default() -> Self { + Self::new() + } +} + +#[napi] +impl EstimatedLeverageRatioNode { + #[napi(constructor)] + pub fn new() -> Self { + Self { + inner: wc::EstimatedLeverageRatio::new(), + } + } + #[napi] + pub fn update( + &mut self, + open_interest: f64, + long_size: f64, + short_size: f64, + ) -> napi::Result> { + Ok(self + .inner + .update(deriv_oi_long_short(open_interest, long_size, short_size)?)) + } + #[napi] + pub fn batch( + &mut self, + open_interest: Vec, + long_size: Vec, + short_size: Vec, + ) -> napi::Result> { + if open_interest.len() != long_size.len() || long_size.len() != short_size.len() { + return Err(NapiError::from_reason( + "open_interest, long_size, short_size must be equal length".to_string(), + )); + } + let mut out = Vec::with_capacity(open_interest.len()); + for i in 0..open_interest.len() { + out.push( + self.inner + .update(deriv_oi_long_short( + open_interest[i], + long_size[i], + short_size[i], + )?) + .unwrap_or(f64::NAN), + ); + } + Ok(out) + } + #[napi] + pub fn reset(&mut self) { + self.inner.reset(); + } + #[napi(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[napi(js_name = "warmupPeriod")] + pub fn warmup_period(&self) -> u32 { + self.inner.warmup_period() as u32 + } +} + +// OI-to-volume ratio: open interest over taker buy+sell volume. +#[napi(js_name = "OiToVolumeRatio")] +pub struct OiToVolumeRatioNode { + inner: wc::OiToVolumeRatio, +} + +impl Default for OiToVolumeRatioNode { + fn default() -> Self { + Self::new() + } +} + +#[napi] +impl OiToVolumeRatioNode { + #[napi(constructor)] + pub fn new() -> Self { + Self { + inner: wc::OiToVolumeRatio::new(), + } + } + #[napi] + pub fn update( + &mut self, + open_interest: f64, + taker_buy_volume: f64, + taker_sell_volume: f64, + ) -> napi::Result> { + Ok(self.inner.update(deriv_oi_taker( + open_interest, + taker_buy_volume, + taker_sell_volume, + )?)) + } + #[napi] + pub fn batch( + &mut self, + open_interest: Vec, + taker_buy_volume: Vec, + taker_sell_volume: Vec, + ) -> napi::Result> { + if open_interest.len() != taker_buy_volume.len() + || taker_buy_volume.len() != taker_sell_volume.len() + { + return Err(NapiError::from_reason( + "open_interest, taker_buy_volume, taker_sell_volume must be equal length" + .to_string(), + )); + } + let mut out = Vec::with_capacity(open_interest.len()); + for i in 0..open_interest.len() { + out.push( + self.inner + .update(deriv_oi_taker( + open_interest[i], + taker_buy_volume[i], + taker_sell_volume[i], + )?) + .unwrap_or(f64::NAN), + ); + } + Ok(out) + } + #[napi] + pub fn reset(&mut self) { + self.inner.reset(); + } + #[napi(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[napi(js_name = "warmupPeriod")] + pub fn warmup_period(&self) -> u32 { + self.inner.warmup_period() as u32 + } +} + +// Perpetual premium index: relative premium of mark over index price. +#[napi(js_name = "PerpetualPremiumIndex")] +pub struct PerpetualPremiumIndexNode { + inner: wc::PerpetualPremiumIndex, +} + +impl Default for PerpetualPremiumIndexNode { + fn default() -> Self { + Self::new() + } +} + +#[napi] +impl PerpetualPremiumIndexNode { + #[napi(constructor)] + pub fn new() -> Self { + Self { + inner: wc::PerpetualPremiumIndex::new(), + } + } + #[napi] + pub fn update(&mut self, mark_price: f64, index_price: f64) -> napi::Result> { + Ok(self.inner.update(deriv_basis(mark_price, index_price)?)) + } + #[napi] + pub fn batch(&mut self, mark_price: Vec, index_price: Vec) -> napi::Result> { + if mark_price.len() != index_price.len() { + return Err(NapiError::from_reason( + "mark_price and index_price must be equal length".to_string(), + )); + } + let mut out = Vec::with_capacity(mark_price.len()); + for i in 0..mark_price.len() { + out.push( + self.inner + .update(deriv_basis(mark_price[i], index_price[i])?) + .unwrap_or(f64::NAN), + ); + } + Ok(out) + } + #[napi] + pub fn reset(&mut self) { + self.inner.reset(); + } + #[napi(js_name = "isReady")] + pub fn is_ready(&self) -> bool { + self.inner.is_ready() + } + #[napi(js_name = "warmupPeriod")] + pub fn warmup_period(&self) -> u32 { + self.inner.warmup_period() as u32 + } +} + +// Funding-implied APR: per-interval funding annualised. +#[napi(js_name = "FundingImpliedApr")] +pub struct FundingImpliedAprNode { + inner: wc::FundingImpliedApr, +} + +#[napi] +impl FundingImpliedAprNode { + #[napi(constructor)] + pub fn new(intervals_per_year: f64) -> napi::Result { + Ok(Self { + inner: wc::FundingImpliedApr::new(intervals_per_year).map_err(map_err)?, + }) + } + #[napi] + pub fn update(&mut self, funding_rate: f64) -> napi::Result> { + Ok(self.inner.update(deriv_funding(funding_rate)?)) + } + #[napi] + pub fn batch(&mut self, funding_rate: Vec) -> napi::Result> { + let mut out = Vec::with_capacity(funding_rate.len()); + for r in funding_rate { + out.push(self.inner.update(deriv_funding(r)?).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 + } +} + +// Open-interest momentum: rate-of-change of open interest over a window. +#[napi(js_name = "OpenInterestMomentum")] +pub struct OpenInterestMomentumNode { + inner: wc::OpenInterestMomentum, +} + +#[napi] +impl OpenInterestMomentumNode { + #[napi(constructor)] + pub fn new(period: u32) -> napi::Result { + Ok(Self { + inner: wc::OpenInterestMomentum::new(period as usize).map_err(map_err)?, + }) + } + #[napi] + pub fn update(&mut self, open_interest: f64) -> napi::Result> { + Ok(self.inner.update(deriv_oi(open_interest)?)) + } + #[napi] + pub fn batch(&mut self, open_interest: Vec) -> napi::Result> { + let mut out = Vec::with_capacity(open_interest.len()); + for oi in open_interest { + out.push(self.inner.update(deriv_oi(oi)?).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 + } +} + // ---------- Market Breadth (CrossSection input) ---------- // // A breadth tick is the per-symbol state of the whole universe, passed as four diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py index f468d13f..418fbfb7 100644 --- a/bindings/python/python/wickra/__init__.py +++ b/bindings/python/python/wickra/__init__.py @@ -480,6 +480,11 @@ from ._wickra import ( # Microstructure: footprint Footprint, # Derivatives + OpenInterestMomentum, + FundingImpliedApr, + PerpetualPremiumIndex, + OiToVolumeRatio, + EstimatedLeverageRatio, FundingRate, FundingRateMean, FundingRateZScore, @@ -998,6 +1003,11 @@ __all__ = [ # Microstructure: footprint "Footprint", # Derivatives + "OpenInterestMomentum", + "FundingImpliedApr", + "PerpetualPremiumIndex", + "OiToVolumeRatio", + "EstimatedLeverageRatio", "FundingRate", "FundingRateMean", "FundingRateZScore", diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index b4080935..56c97179 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -19420,6 +19420,50 @@ fn deriv_taker(taker_buy_volume: f64, taker_sell_volume: f64) -> PyResult PyResult { + wc::DerivativesTick::new( + 0.0, + 1.0, + 1.0, + 1.0, + open_interest, + long_size, + short_size, + 0.0, + 0.0, + 0.0, + 0.0, + 0, + ) + .map_err(map_err) +} + +fn deriv_oi_taker( + open_interest: f64, + taker_buy_volume: f64, + taker_sell_volume: f64, +) -> PyResult { + wc::DerivativesTick::new( + 0.0, + 1.0, + 1.0, + 1.0, + open_interest, + 0.0, + 0.0, + taker_buy_volume, + taker_sell_volume, + 0.0, + 0.0, + 0, + ) + .map_err(map_err) +} + fn deriv_liquidation( long_liquidation: f64, short_liquidation: f64, @@ -20140,6 +20184,302 @@ impl PyCalendarSpread { } } +// Estimated leverage ratio: open interest over aggregate long+short size. +#[pyclass( + name = "EstimatedLeverageRatio", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyEstimatedLeverageRatio { + inner: wc::EstimatedLeverageRatio, +} + +#[pymethods] +impl PyEstimatedLeverageRatio { + #[new] + fn new() -> Self { + Self { + inner: wc::EstimatedLeverageRatio::new(), + } + } + fn update( + &mut self, + open_interest: f64, + long_size: f64, + short_size: f64, + ) -> PyResult> { + Ok(self + .inner + .update(deriv_oi_long_short(open_interest, long_size, short_size)?)) + } + fn batch<'py>( + &mut self, + py: Python<'py>, + open_interest: Vec, + long_size: Vec, + short_size: Vec, + ) -> PyResult>> { + if open_interest.len() != long_size.len() || long_size.len() != short_size.len() { + return Err(PyValueError::new_err( + "open_interest, long_size, short_size must be equal length", + )); + } + let mut out = Vec::with_capacity(open_interest.len()); + for i in 0..open_interest.len() { + out.push( + self.inner + .update(deriv_oi_long_short( + open_interest[i], + long_size[i], + short_size[i], + )?) + .unwrap_or(f64::NAN), + ); + } + Ok(out.into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + "EstimatedLeverageRatio()".to_string() + } +} + +// OI-to-volume ratio: open interest over taker buy+sell volume. +#[pyclass( + name = "OiToVolumeRatio", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyOiToVolumeRatio { + inner: wc::OiToVolumeRatio, +} + +#[pymethods] +impl PyOiToVolumeRatio { + #[new] + fn new() -> Self { + Self { + inner: wc::OiToVolumeRatio::new(), + } + } + fn update( + &mut self, + open_interest: f64, + taker_buy_volume: f64, + taker_sell_volume: f64, + ) -> PyResult> { + Ok(self.inner.update(deriv_oi_taker( + open_interest, + taker_buy_volume, + taker_sell_volume, + )?)) + } + fn batch<'py>( + &mut self, + py: Python<'py>, + open_interest: Vec, + taker_buy_volume: Vec, + taker_sell_volume: Vec, + ) -> PyResult>> { + if open_interest.len() != taker_buy_volume.len() + || taker_buy_volume.len() != taker_sell_volume.len() + { + return Err(PyValueError::new_err( + "open_interest, taker_buy_volume, taker_sell_volume must be equal length", + )); + } + let mut out = Vec::with_capacity(open_interest.len()); + for i in 0..open_interest.len() { + out.push( + self.inner + .update(deriv_oi_taker( + open_interest[i], + taker_buy_volume[i], + taker_sell_volume[i], + )?) + .unwrap_or(f64::NAN), + ); + } + Ok(out.into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + "OiToVolumeRatio()".to_string() + } +} + +// Perpetual premium index: relative premium of mark over index price. +#[pyclass( + name = "PerpetualPremiumIndex", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyPerpetualPremiumIndex { + inner: wc::PerpetualPremiumIndex, +} + +#[pymethods] +impl PyPerpetualPremiumIndex { + #[new] + fn new() -> Self { + Self { + inner: wc::PerpetualPremiumIndex::new(), + } + } + fn update(&mut self, mark_price: f64, index_price: f64) -> PyResult> { + Ok(self.inner.update(deriv_basis(mark_price, index_price)?)) + } + fn batch<'py>( + &mut self, + py: Python<'py>, + mark_price: Vec, + index_price: Vec, + ) -> PyResult>> { + if mark_price.len() != index_price.len() { + return Err(PyValueError::new_err( + "mark_price and index_price must be equal length", + )); + } + let mut out = Vec::with_capacity(mark_price.len()); + for i in 0..mark_price.len() { + out.push( + self.inner + .update(deriv_basis(mark_price[i], index_price[i])?) + .unwrap_or(f64::NAN), + ); + } + Ok(out.into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + "PerpetualPremiumIndex()".to_string() + } +} + +// Funding-implied APR: per-interval funding annualised. +#[pyclass( + name = "FundingImpliedApr", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyFundingImpliedApr { + inner: wc::FundingImpliedApr, +} + +#[pymethods] +impl PyFundingImpliedApr { + #[new] + fn new(intervals_per_year: f64) -> PyResult { + Ok(Self { + inner: wc::FundingImpliedApr::new(intervals_per_year).map_err(map_err)?, + }) + } + fn update(&mut self, funding_rate: f64) -> PyResult> { + Ok(self.inner.update(deriv_funding(funding_rate)?)) + } + fn batch<'py>( + &mut self, + py: Python<'py>, + funding_rate: Vec, + ) -> PyResult>> { + let mut out = Vec::with_capacity(funding_rate.len()); + for r in funding_rate { + out.push(self.inner.update(deriv_funding(r)?).unwrap_or(f64::NAN)); + } + Ok(out.into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + format!( + "FundingImpliedApr(intervals_per_year={})", + self.inner.intervals_per_year() + ) + } +} + +// Open-interest momentum: rate-of-change of open interest over a window. +#[pyclass( + name = "OpenInterestMomentum", + module = "wickra._wickra", + skip_from_py_object +)] +#[derive(Clone)] +struct PyOpenInterestMomentum { + inner: wc::OpenInterestMomentum, +} + +#[pymethods] +impl PyOpenInterestMomentum { + #[new] + fn new(period: usize) -> PyResult { + Ok(Self { + inner: wc::OpenInterestMomentum::new(period).map_err(map_err)?, + }) + } + fn update(&mut self, open_interest: f64) -> PyResult> { + Ok(self.inner.update(deriv_oi(open_interest)?)) + } + fn batch<'py>( + &mut self, + py: Python<'py>, + open_interest: Vec, + ) -> PyResult>> { + let mut out = Vec::with_capacity(open_interest.len()); + for oi in open_interest { + out.push(self.inner.update(deriv_oi(oi)?).unwrap_or(f64::NAN)); + } + Ok(out.into_pyarray(py)) + } + fn reset(&mut self) { + self.inner.reset(); + } + fn is_ready(&self) -> bool { + self.inner.is_ready() + } + fn warmup_period(&self) -> usize { + self.inner.warmup_period() + } + fn __repr__(&self) -> String { + format!("OpenInterestMomentum(period={})", self.inner.period()) + } +} + // ============================== Market Breadth ============================== // // Market-breadth indicators consume a `CrossSection`: one tick carrying the @@ -25029,6 +25369,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 64a5e22e..56f8709b 100644 --- a/bindings/python/tests/test_new_indicators.py +++ b/bindings/python/tests/test_new_indicators.py @@ -4082,6 +4082,71 @@ def test_basis_indicators_streaming_equals_batch(): assert _eq_nan(batch, streamed) +def test_b16_derivatives_reference(): + # Estimated leverage: oi / (long + short) = 200 / 100 = 2. + assert ta.EstimatedLeverageRatio().update(200.0, 60.0, 40.0) == pytest.approx(2.0) + # OI-to-volume: oi / (buy + sell) = 100 / 50 = 2. + assert ta.OiToVolumeRatio().update(100.0, 30.0, 20.0) == pytest.approx(2.0) + # Perpetual premium: (mark - index) / index = 0.5 / 100 = 0.005. + assert ta.PerpetualPremiumIndex().update(100.5, 100.0) == pytest.approx(0.005) + # Funding-implied APR: rate * intervals = 0.0001 * 1095 = 0.1095. + assert ta.FundingImpliedApr(1095.0).update(0.0001) == pytest.approx(0.1095) + # Open-interest momentum (period 2): warmup then ROC% = 100*(120-100)/100 = 20. + oim = ta.OpenInterestMomentum(2) + assert oim.update(100.0) is None + assert oim.update(110.0) is None + assert oim.update(120.0) == pytest.approx(20.0) + + +def test_b16_derivatives_streaming_equals_batch(): + n = 40 + oi = np.array([1000.0 + 50.0 * math.sin(i * 0.3) for i in range(n)], dtype=np.float64) + long_sz = np.array([600.0 + 20.0 * math.cos(i * 0.2) for i in range(n)], dtype=np.float64) + short_sz = np.array([400.0 + 15.0 * math.sin(i * 0.4) for i in range(n)], dtype=np.float64) + buy = np.array([300.0 + 10.0 * math.sin(i * 0.5) for i in range(n)], dtype=np.float64) + sell = np.array([250.0 + 12.0 * math.cos(i * 0.35) for i in range(n)], dtype=np.float64) + index = np.array([100.0 + math.sin(i * 0.2) for i in range(n)], dtype=np.float64) + mark = np.array([index[i] + 0.05 * math.cos(i * 0.3) for i in range(n)], dtype=np.float64) + rate = np.array([0.0001 * math.sin(i * 0.3) for i in range(n)], dtype=np.float64) + + # EstimatedLeverageRatio; update(open_interest, long_size, short_size). + batch = ta.EstimatedLeverageRatio().batch(oi, long_sz, short_sz) + streamer = ta.EstimatedLeverageRatio() + streamed = np.array( + [streamer.update(oi[i], long_sz[i], short_sz[i]) for i in range(n)], dtype=np.float64 + ) + assert batch.shape == (n,) + assert _eq_nan(batch, streamed) + + # OiToVolumeRatio; update(open_interest, taker_buy_volume, taker_sell_volume). + batch = ta.OiToVolumeRatio().batch(oi, buy, sell) + streamer = ta.OiToVolumeRatio() + streamed = np.array( + [streamer.update(oi[i], buy[i], sell[i]) for i in range(n)], dtype=np.float64 + ) + assert _eq_nan(batch, streamed) + + # PerpetualPremiumIndex; update(mark_price, index_price). + batch = ta.PerpetualPremiumIndex().batch(mark, index) + streamer = ta.PerpetualPremiumIndex() + streamed = np.array( + [streamer.update(mark[i], index[i]) for i in range(n)], dtype=np.float64 + ) + assert _eq_nan(batch, streamed) + + # FundingImpliedApr; update(funding_rate). + batch = ta.FundingImpliedApr(1095.0).batch(rate) + streamer = ta.FundingImpliedApr(1095.0) + streamed = np.array([streamer.update(rate[i]) for i in range(n)], dtype=np.float64) + assert _eq_nan(batch, streamed) + + # OpenInterestMomentum; update(open_interest). + batch = ta.OpenInterestMomentum(10).batch(oi) + streamer = ta.OpenInterestMomentum(10) + streamed = np.array([streamer.update(oi[i]) for i in range(n)], dtype=np.float64) + assert _eq_nan(batch, streamed) + + # --- Alt-Chart Bars ------------------------------------------------------ diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 3b043088..bdfab73a 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -9933,6 +9933,50 @@ fn deriv_taker( .map_err(map_err) } +fn deriv_oi_long_short( + open_interest: f64, + long_size: f64, + short_size: f64, +) -> Result { + wc::DerivativesTick::new( + 0.0, + 1.0, + 1.0, + 1.0, + open_interest, + long_size, + short_size, + 0.0, + 0.0, + 0.0, + 0.0, + 0, + ) + .map_err(map_err) +} + +fn deriv_oi_taker( + open_interest: f64, + taker_buy_volume: f64, + taker_sell_volume: f64, +) -> Result { + wc::DerivativesTick::new( + 0.0, + 1.0, + 1.0, + 1.0, + open_interest, + 0.0, + 0.0, + taker_buy_volume, + taker_sell_volume, + 0.0, + 0.0, + 0, + ) + .map_err(map_err) +} + fn deriv_liquidation( long_liquidation: f64, short_liquidation: f64, @@ -10256,6 +10300,195 @@ impl WasmCalendarSpread { } } +// ---------- Estimated Leverage Ratio ---------- + +#[wasm_bindgen(js_name = EstimatedLeverageRatio)] +pub struct WasmEstimatedLeverageRatio { + inner: wc::EstimatedLeverageRatio, +} + +impl Default for WasmEstimatedLeverageRatio { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen(js_class = EstimatedLeverageRatio)] +impl WasmEstimatedLeverageRatio { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmEstimatedLeverageRatio { + Self { + inner: wc::EstimatedLeverageRatio::new(), + } + } + pub fn update( + &mut self, + open_interest: f64, + long_size: f64, + short_size: f64, + ) -> Result, JsError> { + Ok(self + .inner + .update(deriv_oi_long_short(open_interest, long_size, short_size)?)) + } + 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() + } +} + +// ---------- OI-to-Volume Ratio ---------- + +#[wasm_bindgen(js_name = OiToVolumeRatio)] +pub struct WasmOiToVolumeRatio { + inner: wc::OiToVolumeRatio, +} + +impl Default for WasmOiToVolumeRatio { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen(js_class = OiToVolumeRatio)] +impl WasmOiToVolumeRatio { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmOiToVolumeRatio { + Self { + inner: wc::OiToVolumeRatio::new(), + } + } + pub fn update( + &mut self, + open_interest: f64, + taker_buy_volume: f64, + taker_sell_volume: f64, + ) -> Result, JsError> { + Ok(self.inner.update(deriv_oi_taker( + open_interest, + taker_buy_volume, + taker_sell_volume, + )?)) + } + 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() + } +} + +// ---------- Perpetual Premium Index ---------- + +#[wasm_bindgen(js_name = PerpetualPremiumIndex)] +pub struct WasmPerpetualPremiumIndex { + inner: wc::PerpetualPremiumIndex, +} + +impl Default for WasmPerpetualPremiumIndex { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen(js_class = PerpetualPremiumIndex)] +impl WasmPerpetualPremiumIndex { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmPerpetualPremiumIndex { + Self { + inner: wc::PerpetualPremiumIndex::new(), + } + } + pub fn update(&mut self, mark_price: f64, index_price: f64) -> Result, JsError> { + Ok(self.inner.update(deriv_basis(mark_price, index_price)?)) + } + 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() + } +} + +// ---------- Funding-Implied APR ---------- + +#[wasm_bindgen(js_name = FundingImpliedApr)] +pub struct WasmFundingImpliedApr { + inner: wc::FundingImpliedApr, +} + +#[wasm_bindgen(js_class = FundingImpliedApr)] +impl WasmFundingImpliedApr { + #[wasm_bindgen(constructor)] + pub fn new(intervals_per_year: f64) -> Result { + Ok(Self { + inner: wc::FundingImpliedApr::new(intervals_per_year).map_err(map_err)?, + }) + } + pub fn update(&mut self, funding_rate: f64) -> Result, JsError> { + Ok(self.inner.update(deriv_funding(funding_rate)?)) + } + 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() + } +} + +// ---------- Open-Interest Momentum ---------- + +#[wasm_bindgen(js_name = OpenInterestMomentum)] +pub struct WasmOpenInterestMomentum { + inner: wc::OpenInterestMomentum, +} + +#[wasm_bindgen(js_class = OpenInterestMomentum)] +impl WasmOpenInterestMomentum { + #[wasm_bindgen(constructor)] + pub fn new(period: usize) -> Result { + Ok(Self { + inner: wc::OpenInterestMomentum::new(period).map_err(map_err)?, + }) + } + pub fn update(&mut self, open_interest: f64) -> Result, JsError> { + Ok(self.inner.update(deriv_oi(open_interest)?)) + } + 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() + } +} + // ---------- Heikin-Ashi Oscillator ---------- #[wasm_bindgen(js_name = HeikinAshiOscillator)] diff --git a/crates/wickra-core/src/indicators/estimated_leverage_ratio.rs b/crates/wickra-core/src/indicators/estimated_leverage_ratio.rs new file mode 100644 index 00000000..8b756dff --- /dev/null +++ b/crates/wickra-core/src/indicators/estimated_leverage_ratio.rs @@ -0,0 +1,155 @@ +//! Estimated Leverage Ratio — open interest per unit of aggregate position size. + +use crate::derivatives::DerivativesTick; +use crate::traits::Indicator; + +/// Estimated Leverage Ratio (ELR) — open interest relative to the aggregate +/// long+short position size, a proxy for how leveraged outstanding positions are. +/// +/// ```text +/// ELR = open_interest / (long_size + short_size) +/// ``` +/// +/// The classic estimated leverage ratio compares open interest (the notional of +/// outstanding contracts) to the capital backing it. With the size fields of a +/// [`DerivativesTick`] standing in for the position base, the ratio rises when a +/// given pool of positions controls more open interest — i.e. when the market is +/// running hotter leverage. Spikes in ELR mark crowded, fragile conditions where a +/// move can cascade into liquidations; a falling ELR marks deleveraging. +/// +/// The ratio is non-negative; a tick with zero aggregate size reports `0` rather +/// than dividing by zero. It is stateless — each tick yields one value (no warmup). +/// Each `update` is O(1). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{DerivativesTick, Indicator, EstimatedLeverageRatio}; +/// +/// let mut indicator = EstimatedLeverageRatio::new(); +/// let tick = DerivativesTick::new(0.0001, 100.0, 100.0, 100.0, 1_000.0, 400.0, 600.0, 0.0, 0.0, 0.0, 0.0, 0).unwrap(); +/// let elr = indicator.update(tick).unwrap(); +/// assert!((elr - 1.0).abs() < 1e-12); // 1000 / (400 + 600) +/// ``` +#[derive(Debug, Clone, Default)] +pub struct EstimatedLeverageRatio { + ready: bool, +} + +impl EstimatedLeverageRatio { + /// Construct a new Estimated Leverage Ratio. The indicator is parameter-free. + #[must_use] + pub const fn new() -> Self { + Self { ready: false } + } +} + +impl Indicator for EstimatedLeverageRatio { + type Input = DerivativesTick; + type Output = f64; + + fn update(&mut self, tick: DerivativesTick) -> Option { + let base = tick.long_size + tick.short_size; + let elr = if base > 0.0 { + tick.open_interest / base + } else { + 0.0 + }; + self.ready = true; + Some(elr) + } + + 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 { + "EstimatedLeverageRatio" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + fn tick(oi: f64, long: f64, short: f64) -> DerivativesTick { + DerivativesTick::new_unchecked( + 0.0, 100.0, 100.0, 100.0, oi, long, short, 0.0, 0.0, 0.0, 0.0, 0, + ) + } + + #[test] + fn accessors_and_metadata() { + let e = EstimatedLeverageRatio::new(); + assert_eq!(e.warmup_period(), 1); + assert_eq!(e.name(), "EstimatedLeverageRatio"); + assert!(!e.is_ready()); + } + + #[test] + fn ratio_reference_value() { + let mut e = EstimatedLeverageRatio::new(); + // 1000 / (400 + 600) = 1.0. + assert_relative_eq!( + e.update(tick(1_000.0, 400.0, 600.0)).unwrap(), + 1.0, + epsilon = 1e-12 + ); + } + + #[test] + fn higher_oi_raises_ratio() { + let mut e = EstimatedLeverageRatio::new(); + let low = e.update(tick(1_000.0, 500.0, 500.0)).unwrap(); + let high = e.update(tick(3_000.0, 500.0, 500.0)).unwrap(); + assert!(high > low); + } + + #[test] + fn zero_base_is_zero() { + let mut e = EstimatedLeverageRatio::new(); + assert_relative_eq!( + e.update(tick(1_000.0, 0.0, 0.0)).unwrap(), + 0.0, + epsilon = 1e-12 + ); + } + + #[test] + fn ready_after_first_update() { + let mut e = EstimatedLeverageRatio::new(); + assert!(!e.is_ready()); + e.update(tick(1_000.0, 500.0, 500.0)); + assert!(e.is_ready()); + } + + #[test] + fn reset_clears_state() { + let mut e = EstimatedLeverageRatio::new(); + e.update(tick(1_000.0, 500.0, 500.0)); + assert!(e.is_ready()); + e.reset(); + assert!(!e.is_ready()); + } + + #[test] + fn batch_equals_streaming() { + let ticks: Vec = (0..40) + .map(|i| tick(1_000.0 + f64::from(i) * 10.0, 500.0, 500.0)) + .collect(); + let batch = EstimatedLeverageRatio::new().batch(&ticks); + let mut b = EstimatedLeverageRatio::new(); + let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/funding_implied_apr.rs b/crates/wickra-core/src/indicators/funding_implied_apr.rs new file mode 100644 index 00000000..dcdd9f8b --- /dev/null +++ b/crates/wickra-core/src/indicators/funding_implied_apr.rs @@ -0,0 +1,164 @@ +//! Funding-Implied APR — the per-interval funding rate annualised. + +use crate::derivatives::DerivativesTick; +use crate::error::{Error, Result}; +use crate::traits::Indicator; + +/// Funding-Implied APR — the perpetual's per-interval funding rate scaled to an +/// annualised rate. +/// +/// ```text +/// APR = funding_rate · intervals_per_year +/// ``` +/// +/// Funding is paid in small per-interval amounts (commonly every 8 hours, i.e. +/// `1095` intervals per year). Annualising it converts the headline funding number +/// into the carry cost (or yield) of holding the position for a year, which is far +/// easier to reason about and to compare against spot lending rates, basis trades, +/// and other yields. A large positive APR means longs pay a steep carry to shorts +/// (and vice versa) — the economic incentive behind cash-and-carry and +/// funding-arbitrage strategies. +/// +/// The output is a fraction (multiply by `100` for percent) and may be negative. +/// It is stateless — each tick yields one value (no warmup). Each `update` is O(1). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{DerivativesTick, Indicator, FundingImpliedApr}; +/// +/// // 0.01% per 8h funding -> 0.0001 * 1095 ≈ 10.95% APR. +/// let mut indicator = FundingImpliedApr::new(1095.0).unwrap(); +/// let tick = DerivativesTick::new(0.0001, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0).unwrap(); +/// let apr = indicator.update(tick).unwrap(); +/// assert!((apr - 0.1095).abs() < 1e-9); +/// ``` +#[derive(Debug, Clone)] +pub struct FundingImpliedApr { + intervals_per_year: f64, + ready: bool, +} + +impl FundingImpliedApr { + /// Construct a Funding-Implied APR with the number of funding intervals per + /// year (e.g. `1095` for 8-hour funding, `365` for daily). + /// + /// # Errors + /// + /// Returns [`Error::InvalidParameter`] if `intervals_per_year` is not finite + /// and positive. + pub fn new(intervals_per_year: f64) -> Result { + if !intervals_per_year.is_finite() || intervals_per_year <= 0.0 { + return Err(Error::InvalidParameter { + message: "intervals_per_year must be finite and positive", + }); + } + Ok(Self { + intervals_per_year, + ready: false, + }) + } + + /// Configured intervals per year. + pub const fn intervals_per_year(&self) -> f64 { + self.intervals_per_year + } +} + +impl Indicator for FundingImpliedApr { + type Input = DerivativesTick; + type Output = f64; + + fn update(&mut self, tick: DerivativesTick) -> Option { + self.ready = true; + Some(tick.funding_rate * self.intervals_per_year) + } + + 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 { + "FundingImpliedApr" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + fn tick(funding: f64) -> DerivativesTick { + DerivativesTick::new_unchecked( + funding, 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, + ) + } + + #[test] + fn rejects_invalid_intervals() { + assert!(matches!( + FundingImpliedApr::new(0.0), + Err(Error::InvalidParameter { .. }) + )); + assert!(matches!( + FundingImpliedApr::new(-1.0), + Err(Error::InvalidParameter { .. }) + )); + } + + #[test] + fn accessors_and_metadata() { + let f = FundingImpliedApr::new(1095.0).unwrap(); + assert_relative_eq!(f.intervals_per_year(), 1095.0, epsilon = 1e-12); + assert_eq!(f.warmup_period(), 1); + assert_eq!(f.name(), "FundingImpliedApr"); + assert!(!f.is_ready()); + } + + #[test] + fn apr_reference_value() { + let mut f = FundingImpliedApr::new(1095.0).unwrap(); + assert_relative_eq!(f.update(tick(0.0001)).unwrap(), 0.1095, epsilon = 1e-9); + } + + #[test] + fn negative_funding_is_negative_apr() { + let mut f = FundingImpliedApr::new(1095.0).unwrap(); + assert!(f.update(tick(-0.0001)).unwrap() < 0.0); + } + + #[test] + fn zero_funding_is_zero() { + let mut f = FundingImpliedApr::new(365.0).unwrap(); + assert_relative_eq!(f.update(tick(0.0)).unwrap(), 0.0, epsilon = 1e-12); + } + + #[test] + fn reset_clears_state() { + let mut f = FundingImpliedApr::new(1095.0).unwrap(); + f.update(tick(0.0001)); + assert!(f.is_ready()); + f.reset(); + assert!(!f.is_ready()); + } + + #[test] + fn batch_equals_streaming() { + let ticks: Vec = (0..40) + .map(|i| tick(0.0001 * (f64::from(i) * 0.3).sin())) + .collect(); + let batch = FundingImpliedApr::new(1095.0).unwrap().batch(&ticks); + let mut b = FundingImpliedApr::new(1095.0).unwrap(); + let streamed: Vec<_> = ticks.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 4472a1d9..0b5af8dd 100644 --- a/crates/wickra-core/src/indicators/mod.rs +++ b/crates/wickra-core/src/indicators/mod.rs @@ -131,6 +131,7 @@ mod ema; mod empirical_mode_decomposition; mod engulfing; mod equivolume; +mod estimated_leverage_ratio; mod even_better_sinewave; mod evening_doji_star; mod evwma; @@ -156,6 +157,7 @@ mod fractal_chaos_bands; mod frama; mod fry_pan_bottom; mod funding_basis; +mod funding_implied_apr; mod funding_rate; mod funding_rate_mean; mod funding_rate_zscore; @@ -278,9 +280,11 @@ mod ob_imbalance_topn; mod obv; mod oi_delta; mod oi_price_divergence; +mod oi_to_volume_ratio; mod oi_weighted; mod omega_ratio; mod on_neck; +mod open_interest_momentum; mod opening_marubozu; mod opening_range; mod order_flow_imbalance; @@ -295,6 +299,7 @@ mod pearson_correlation; mod percent_above_ma; mod percent_b; mod percentage_trailing_stop; +mod perpetual_premium_index; mod pgo; mod piercing_dark_cloud; mod pin; @@ -619,6 +624,7 @@ pub use ema::Ema; pub use empirical_mode_decomposition::EmpiricalModeDecomposition; pub use engulfing::Engulfing; pub use equivolume::{Equivolume, EquivolumeOutput}; +pub use estimated_leverage_ratio::EstimatedLeverageRatio; pub use even_better_sinewave::EvenBetterSinewave; pub use evening_doji_star::EveningDojiStar; pub use evwma::Evwma; @@ -644,6 +650,7 @@ pub use fractal_chaos_bands::{FractalChaosBands, FractalChaosBandsOutput}; pub use frama::Frama; pub use fry_pan_bottom::FryPanBottom; pub use funding_basis::FundingBasis; +pub use funding_implied_apr::FundingImpliedApr; pub use funding_rate::FundingRate; pub use funding_rate_mean::FundingRateMean; pub use funding_rate_zscore::FundingRateZScore; @@ -766,9 +773,11 @@ pub use ob_imbalance_topn::OrderBookImbalanceTopN; pub use obv::Obv; pub use oi_delta::OpenInterestDelta; pub use oi_price_divergence::OIPriceDivergence; +pub use oi_to_volume_ratio::OiToVolumeRatio; pub use oi_weighted::OIWeighted; pub use omega_ratio::OmegaRatio; pub use on_neck::OnNeck; +pub use open_interest_momentum::OpenInterestMomentum; pub use opening_marubozu::OpeningMarubozu; pub use opening_range::{OpeningRange, OpeningRangeOutput}; pub use order_flow_imbalance::OrderFlowImbalance; @@ -783,6 +792,7 @@ pub use pearson_correlation::PearsonCorrelation; pub use percent_above_ma::PercentAboveMa; pub use percent_b::PercentB; pub use percentage_trailing_stop::PercentageTrailingStop; +pub use perpetual_premium_index::PerpetualPremiumIndex; pub use pgo::Pgo; pub use piercing_dark_cloud::PiercingDarkCloud; pub use pin::Pin; @@ -1478,6 +1488,11 @@ pub const FAMILIES: &[(&str, &[&str])] = &[ "LiquidationFeatures", "TermStructureBasis", "CalendarSpread", + "EstimatedLeverageRatio", + "OiToVolumeRatio", + "PerpetualPremiumIndex", + "FundingImpliedApr", + "OpenInterestMomentum", ], ), ( @@ -1624,6 +1639,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, 488, "FAMILIES total drifted from indicator count"); + assert_eq!(total, 493, "FAMILIES total drifted from indicator count"); } } diff --git a/crates/wickra-core/src/indicators/oi_to_volume_ratio.rs b/crates/wickra-core/src/indicators/oi_to_volume_ratio.rs new file mode 100644 index 00000000..510a84fc --- /dev/null +++ b/crates/wickra-core/src/indicators/oi_to_volume_ratio.rs @@ -0,0 +1,154 @@ +//! OI-to-Volume Ratio — open interest relative to traded volume. + +use crate::derivatives::DerivativesTick; +use crate::traits::Indicator; + +/// OI-to-Volume Ratio — open interest divided by the tick's total taker volume, a +/// measure of how much position is *held* versus *turned over*. +/// +/// ```text +/// OIVR = open_interest / (taker_buy_volume + taker_sell_volume) +/// ``` +/// +/// A high ratio means open interest dwarfs the volume trading it — positions are +/// being held, not churned (low participation, potential complacency or a coiling +/// market). A low ratio means heavy volume relative to outstanding interest — +/// active churn, often around breakouts or capitulation. Watching the ratio change +/// distinguishes new-money trends (OI and volume both rising) from short-covering +/// or position rolls. +/// +/// The ratio is non-negative; a tick with zero taker volume reports `0` rather than +/// dividing by zero. It is stateless — each tick yields one value (no warmup). Each +/// `update` is O(1). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{DerivativesTick, Indicator, OiToVolumeRatio}; +/// +/// let mut indicator = OiToVolumeRatio::new(); +/// let tick = DerivativesTick::new(0.0, 100.0, 100.0, 100.0, 5_000.0, 0.0, 0.0, 400.0, 600.0, 0.0, 0.0, 0).unwrap(); +/// let oivr = indicator.update(tick).unwrap(); +/// assert!((oivr - 5.0).abs() < 1e-12); // 5000 / (400 + 600) +/// ``` +#[derive(Debug, Clone, Default)] +pub struct OiToVolumeRatio { + ready: bool, +} + +impl OiToVolumeRatio { + /// Construct a new OI-to-Volume Ratio. The indicator is parameter-free. + #[must_use] + pub const fn new() -> Self { + Self { ready: false } + } +} + +impl Indicator for OiToVolumeRatio { + type Input = DerivativesTick; + type Output = f64; + + fn update(&mut self, tick: DerivativesTick) -> Option { + let volume = tick.taker_buy_volume + tick.taker_sell_volume; + let ratio = if volume > 0.0 { + tick.open_interest / volume + } else { + 0.0 + }; + self.ready = true; + Some(ratio) + } + + 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 { + "OiToVolumeRatio" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + fn tick(oi: f64, buy: f64, sell: f64) -> DerivativesTick { + DerivativesTick::new_unchecked( + 0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, buy, sell, 0.0, 0.0, 0, + ) + } + + #[test] + fn accessors_and_metadata() { + let o = OiToVolumeRatio::new(); + assert_eq!(o.warmup_period(), 1); + assert_eq!(o.name(), "OiToVolumeRatio"); + assert!(!o.is_ready()); + } + + #[test] + fn ratio_reference_value() { + let mut o = OiToVolumeRatio::new(); + assert_relative_eq!( + o.update(tick(5_000.0, 400.0, 600.0)).unwrap(), + 5.0, + epsilon = 1e-12 + ); + } + + #[test] + fn more_volume_lowers_ratio() { + let mut o = OiToVolumeRatio::new(); + let held = o.update(tick(5_000.0, 100.0, 100.0)).unwrap(); + let churned = o.update(tick(5_000.0, 1_000.0, 1_000.0)).unwrap(); + assert!(churned < held); + } + + #[test] + fn zero_volume_is_zero() { + let mut o = OiToVolumeRatio::new(); + assert_relative_eq!( + o.update(tick(5_000.0, 0.0, 0.0)).unwrap(), + 0.0, + epsilon = 1e-12 + ); + } + + #[test] + fn ready_after_first_update() { + let mut o = OiToVolumeRatio::new(); + assert!(!o.is_ready()); + o.update(tick(5_000.0, 100.0, 100.0)); + assert!(o.is_ready()); + } + + #[test] + fn reset_clears_state() { + let mut o = OiToVolumeRatio::new(); + o.update(tick(5_000.0, 100.0, 100.0)); + assert!(o.is_ready()); + o.reset(); + assert!(!o.is_ready()); + } + + #[test] + fn batch_equals_streaming() { + let ticks: Vec = (0..40) + .map(|i| tick(5_000.0, 100.0 + f64::from(i), 100.0)) + .collect(); + let batch = OiToVolumeRatio::new().batch(&ticks); + let mut b = OiToVolumeRatio::new(); + let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/open_interest_momentum.rs b/crates/wickra-core/src/indicators/open_interest_momentum.rs new file mode 100644 index 00000000..f43a9c65 --- /dev/null +++ b/crates/wickra-core/src/indicators/open_interest_momentum.rs @@ -0,0 +1,221 @@ +//! Open-Interest Momentum — the rate of change of open interest over a lookback. + +use std::collections::VecDeque; + +use crate::derivatives::DerivativesTick; +use crate::error::{Error, Result}; +use crate::traits::Indicator; + +/// Open-Interest Momentum — the percentage rate of change of open interest over a +/// `period`-tick lookback. +/// +/// ```text +/// OIM = 100 · (OI_t − OI_{t−period}) / OI_{t−period} +/// ``` +/// +/// Where [`OIDelta`](crate::OIDelta) reports the single-tick change in open +/// interest, OI Momentum measures the trend in positioning over a window: positive +/// values mean open interest is expanding (new money entering — a position build +/// that fuels the prevailing move), negative values mean it is contracting +/// (positions being closed — deleveraging or short-covering). Read alongside price: +/// rising OI with rising price is a strong new-long trend, while rising price with +/// falling OI is a short-covering rally on borrowed time. +/// +/// The output is a percentage and may be negative. A zero base open interest +/// `period` ticks ago reports `0` rather than dividing by zero. The first value +/// lands after `period + 1` inputs. Each `update` is O(1). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{DerivativesTick, Indicator, OpenInterestMomentum}; +/// +/// let mut indicator = OpenInterestMomentum::new(5).unwrap(); +/// let mut last = None; +/// for i in 0..20 { +/// let oi = 1_000.0 + f64::from(i) * 100.0; +/// let tick = DerivativesTick::new(0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0).unwrap(); +/// last = indicator.update(tick); +/// } +/// assert!(last.unwrap() > 0.0); // expanding OI +/// ``` +#[derive(Debug, Clone)] +pub struct OpenInterestMomentum { + period: usize, + window: VecDeque, + last: Option, +} + +impl OpenInterestMomentum { + /// Construct an OI Momentum over a `period`-tick lookback. + /// + /// # Errors + /// + /// Returns [`Error::PeriodZero`] if `period == 0`. + pub fn new(period: usize) -> Result { + if period == 0 { + return Err(Error::PeriodZero); + } + Ok(Self { + period, + window: VecDeque::with_capacity(period + 1), + 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 OpenInterestMomentum { + type Input = DerivativesTick; + type Output = f64; + + fn update(&mut self, tick: DerivativesTick) -> Option { + if self.window.len() == self.period + 1 { + self.window.pop_front(); + } + self.window.push_back(tick.open_interest); + if self.window.len() < self.period + 1 { + return None; + } + let base = *self.window.front().expect("non-empty"); + let current = tick.open_interest; + let oim = if base > 0.0 { + 100.0 * (current - base) / base + } else { + 0.0 + }; + self.last = Some(oim); + Some(oim) + } + + fn reset(&mut self) { + self.window.clear(); + self.last = None; + } + + fn warmup_period(&self) -> usize { + self.period + 1 + } + + fn is_ready(&self) -> bool { + self.last.is_some() + } + + fn name(&self) -> &'static str { + "OpenInterestMomentum" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + fn tick(oi: f64) -> DerivativesTick { + DerivativesTick::new_unchecked( + 0.0, 100.0, 100.0, 100.0, oi, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, + ) + } + + #[test] + fn rejects_zero_period() { + assert!(matches!( + OpenInterestMomentum::new(0), + Err(Error::PeriodZero) + )); + } + + #[test] + fn accessors_and_metadata() { + let o = OpenInterestMomentum::new(5).unwrap(); + assert_eq!(o.period(), 5); + assert_eq!(o.warmup_period(), 6); + assert_eq!(o.name(), "OpenInterestMomentum"); + assert!(!o.is_ready()); + assert_eq!(o.value(), None); + } + + #[test] + fn first_emission_at_warmup_period() { + let mut o = OpenInterestMomentum::new(3).unwrap(); + let ticks: Vec = (0..6) + .map(|i| tick(1_000.0 + f64::from(i) * 100.0)) + .collect(); + let out = o.batch(&ticks); + for v in out.iter().take(3) { + assert!(v.is_none()); + } + assert!(out[3].is_some()); + } + + #[test] + fn reference_value() { + // period 2: OI 1000 -> 1200 over the window -> +20%. + let mut o = OpenInterestMomentum::new(2).unwrap(); + let out = o.batch(&[tick(1_000.0), tick(1_100.0), tick(1_200.0)]); + assert_relative_eq!(out[2].unwrap(), 20.0, epsilon = 1e-9); + } + + #[test] + fn expanding_oi_is_positive() { + let mut o = OpenInterestMomentum::new(5).unwrap(); + let ticks: Vec = (0..20) + .map(|i| tick(1_000.0 + f64::from(i) * 100.0)) + .collect(); + let last = o.batch(&ticks).into_iter().flatten().last().unwrap(); + assert!(last > 0.0); + } + + #[test] + fn contracting_oi_is_negative() { + let mut o = OpenInterestMomentum::new(5).unwrap(); + let ticks: Vec = (0..20) + .map(|i| tick(3_000.0 - f64::from(i) * 100.0)) + .collect(); + let last = o.batch(&ticks).into_iter().flatten().last().unwrap(); + assert!(last < 0.0); + } + + #[test] + fn zero_base_is_zero() { + let mut o = OpenInterestMomentum::new(2).unwrap(); + let out = o.batch(&[tick(0.0), tick(100.0), tick(200.0)]); + assert_relative_eq!(out[2].unwrap(), 0.0, epsilon = 1e-12); + } + + #[test] + fn reset_clears_state() { + let mut o = OpenInterestMomentum::new(3).unwrap(); + o.batch( + &(0..10) + .map(|i| tick(1_000.0 + f64::from(i) * 50.0)) + .collect::>(), + ); + assert!(o.is_ready()); + o.reset(); + assert!(!o.is_ready()); + assert_eq!(o.value(), None); + assert_eq!(o.update(tick(1_000.0)), None); + } + + #[test] + fn batch_equals_streaming() { + let ticks: Vec = (0..80) + .map(|i| tick(1_000.0 + (f64::from(i) * 0.25).sin() * 300.0)) + .collect(); + let batch = OpenInterestMomentum::new(10).unwrap().batch(&ticks); + let mut b = OpenInterestMomentum::new(10).unwrap(); + let streamed: Vec<_> = ticks.iter().map(|x| b.update(*x)).collect(); + assert_eq!(batch, streamed); + } +} diff --git a/crates/wickra-core/src/indicators/perpetual_premium_index.rs b/crates/wickra-core/src/indicators/perpetual_premium_index.rs new file mode 100644 index 00000000..fd384350 --- /dev/null +++ b/crates/wickra-core/src/indicators/perpetual_premium_index.rs @@ -0,0 +1,139 @@ +//! Perpetual Premium Index — the perp mark price relative to spot. + +use crate::derivatives::DerivativesTick; +use crate::traits::Indicator; + +/// Perpetual Premium Index — the perpetual's mark price relative to the spot index +/// it tracks, as a fraction. +/// +/// ```text +/// premium = (mark_price − index_price) / index_price +/// ``` +/// +/// A perpetual swap is pegged to spot by the funding mechanism, but it can still +/// trade at a premium (above spot) or discount (below). A positive premium signals +/// net long demand willing to pay up to hold the perp — bullish positioning, and +/// the proximate driver of positive funding; a negative premium signals the +/// reverse. Sustained extremes flag crowded positioning ripe for a funding-driven +/// mean reversion. +/// +/// The output is centred on zero and dimensionless (a fraction; multiply by `100` +/// for percent). `index_price` is validated strictly positive on the tick, so the +/// division is always defined. It is stateless — each tick yields one value (no +/// warmup). Each `update` is O(1). +/// +/// # Example +/// +/// ``` +/// use wickra_core::{DerivativesTick, Indicator, PerpetualPremiumIndex}; +/// +/// let mut indicator = PerpetualPremiumIndex::new(); +/// // Mark 101 vs index 100 -> +1% premium. +/// let tick = DerivativesTick::new(0.0, 101.0, 100.0, 101.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0).unwrap(); +/// let premium = indicator.update(tick).unwrap(); +/// assert!((premium - 0.01).abs() < 1e-12); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct PerpetualPremiumIndex { + ready: bool, +} + +impl PerpetualPremiumIndex { + /// Construct a new Perpetual Premium Index. The indicator is parameter-free. + #[must_use] + pub const fn new() -> Self { + Self { ready: false } + } +} + +impl Indicator for PerpetualPremiumIndex { + type Input = DerivativesTick; + type Output = f64; + + fn update(&mut self, tick: DerivativesTick) -> Option { + let premium = (tick.mark_price - tick.index_price) / tick.index_price; + self.ready = true; + Some(premium) + } + + 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 { + "PerpetualPremiumIndex" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::BatchExt; + use approx::assert_relative_eq; + + fn tick(mark: f64, index: f64) -> DerivativesTick { + DerivativesTick::new_unchecked(0.0, mark, index, mark, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0) + } + + #[test] + fn accessors_and_metadata() { + let p = PerpetualPremiumIndex::new(); + assert_eq!(p.warmup_period(), 1); + assert_eq!(p.name(), "PerpetualPremiumIndex"); + assert!(!p.is_ready()); + } + + #[test] + fn premium_reference_value() { + let mut p = PerpetualPremiumIndex::new(); + assert_relative_eq!(p.update(tick(101.0, 100.0)).unwrap(), 0.01, epsilon = 1e-12); + } + + #[test] + fn discount_is_negative() { + let mut p = PerpetualPremiumIndex::new(); + assert!(p.update(tick(99.0, 100.0)).unwrap() < 0.0); + } + + #[test] + fn at_par_is_zero() { + let mut p = PerpetualPremiumIndex::new(); + assert_relative_eq!(p.update(tick(100.0, 100.0)).unwrap(), 0.0, epsilon = 1e-12); + } + + #[test] + fn ready_after_first_update() { + let mut p = PerpetualPremiumIndex::new(); + assert!(!p.is_ready()); + p.update(tick(100.0, 100.0)); + assert!(p.is_ready()); + } + + #[test] + fn reset_clears_state() { + let mut p = PerpetualPremiumIndex::new(); + p.update(tick(101.0, 100.0)); + assert!(p.is_ready()); + p.reset(); + assert!(!p.is_ready()); + } + + #[test] + fn batch_equals_streaming() { + let ticks: Vec = (0..40) + .map(|i| tick(100.0 + (f64::from(i) * 0.3).sin(), 100.0)) + .collect(); + let batch = PerpetualPremiumIndex::new().batch(&ticks); + let mut b = PerpetualPremiumIndex::new(); + let streamed: Vec<_> = ticks.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 f3683885..9f21e2e5 100644 --- a/crates/wickra-core/src/lib.rs +++ b/crates/wickra-core/src/lib.rs @@ -81,22 +81,23 @@ pub use indicators::{ DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, DumplingTop, Dx, DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma, ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone, ElderSafeZoneOutput, Ema, - EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput, EvenBetterSinewave, - EveningDojiStar, Evwma, EwmaVolatility, Expectancy, FallingThreeMethods, Fama, FibArcs, - FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput, FibExtension, - FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput, FibRetracement, - FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots, FibonacciPivotsOutput, - FisherRsi, FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex, - FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom, FundingBasis, FundingRate, - FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, Garch11, - GarmanKlassVolatility, Gartley, GatorOscillator, GatorOscillatorOutput, GeneralizedDema, - GeometricMa, GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer, - HangingMan, Harami, HaramiCross, HasbrouckInformationShare, HeadAndShoulders, HeikinAshi, - HeikinAshiOscillator, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, - HighpassFilter, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, - HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, - HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, - Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline, + EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput, EstimatedLeverageRatio, + EvenBetterSinewave, EveningDojiStar, Evwma, EwmaVolatility, Expectancy, FallingThreeMethods, + Fama, FibArcs, FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput, + FibExtension, FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput, + FibRetracement, FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots, + FibonacciPivotsOutput, FisherRsi, FisherTransform, FlagPennant, Footprint, FootprintOutput, + ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom, FundingBasis, + FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore, GainLossRatio, + GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley, GatorOscillator, + GatorOscillatorOutput, GeneralizedDema, GeometricMa, GoldenPocket, GoldenPocketOutput, + GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross, + HasbrouckInformationShare, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator, + HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, HighpassFilter, Hikkake, + HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon, + HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput, + HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia, + InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline, IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile, IntradayVolatilityProfileOutput, InverseFisherTransform, InvertedHammer, JarqueBera, Jma, JumpIndicator, KagiBars, KalmanHedgeRatio, KalmanHedgeRatioOutput, Kama, KaseDevStop, @@ -112,12 +113,13 @@ pub use indicators::{ MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop, ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines, MurreyMathLinesOutput, Natr, NewHighsNewLows, NewPriceLines, Nrtr, NrtrOutput, Nvi, - OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu, - OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1, - OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn, - OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, - PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud, - Pin, PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo, + OIPriceDivergence, OIWeighted, Obv, OiToVolumeRatio, OmegaRatio, OnNeck, OpenInterestDelta, + OpenInterestMomentum, OpeningMarubozu, OpeningRange, OpeningRangeOutput, + OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, OrderFlowImbalance, + OuHalfLife, OvernightGap, OvernightIntradayReturn, OvernightIntradayReturnOutput, PainIndex, + PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentAboveMa, + PercentB, PercentageTrailingStop, PerpetualPremiumIndex, Pgo, PiercingDarkCloud, Pin, + PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo, PpoHistogram, ProfitFactor, ProjectionBands, ProjectionBandsOutput, ProjectionOscillator, Psar, Pvi, Qqe, QqeOutput, Qstick, QuartileBands, QuartileBandsOutput, QuotedSpread, RSquared, RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange, Reflex, RegimeLabel, diff --git a/docs/README.md b/docs/README.md index a659c5e9..b31d700f 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 **488 indicators** across +- A per-indicator deep dive for every one of the **493 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_derivatives.rs b/fuzz/fuzz_targets/indicator_update_derivatives.rs index a7b6ccbf..7459ce96 100644 --- a/fuzz/fuzz_targets/indicator_update_derivatives.rs +++ b/fuzz/fuzz_targets/indicator_update_derivatives.rs @@ -10,11 +10,7 @@ //! never panic, streaming or batched. use libfuzzer_sys::fuzz_target; -use wickra_core::{ - BatchExt, CalendarSpread, DerivativesTick, FundingBasis, FundingRate, FundingRateMean, - FundingRateZScore, Indicator, LiquidationFeatures, LongShortRatio, OIPriceDivergence, - OIWeighted, OpenInterestDelta, TakerBuySellRatio, TermStructureBasis, -}; +use wickra_core::{BatchExt, CalendarSpread, DerivativesTick, EstimatedLeverageRatio, FundingBasis, FundingImpliedApr, FundingRate, FundingRateMean, FundingRateZScore, Indicator, LiquidationFeatures, LongShortRatio, OIPriceDivergence, OIWeighted, OiToVolumeRatio, OpenInterestDelta, OpenInterestMomentum, PerpetualPremiumIndex, TakerBuySellRatio, TermStructureBasis}; #[inline(never)] fn drive(make: impl Fn() -> I, ticks: &[DerivativesTick]) @@ -53,6 +49,11 @@ fuzz_target!(|data: &[u8]| { drive(TakerBuySellRatio::new, &ticks); drive(TermStructureBasis::new, &ticks); drive(CalendarSpread::new, &ticks); + drive(EstimatedLeverageRatio::new, &ticks); + drive(OiToVolumeRatio::new, &ticks); + drive(PerpetualPremiumIndex::new, &ticks); + drive(|| FundingImpliedApr::new(1095.0).unwrap(), &ticks); + drive(|| OpenInterestMomentum::new(14).unwrap(), &ticks); // LiquidationFeatures emits a struct, not an f64, so drive it directly. let mut liq = LiquidationFeatures::new();