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 @@
-
+
[](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();