diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07533aec..f615d13c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@ 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]
+- **Hasbrouck Information Share** — variance-ratio proxy for each venue's share of price discovery (Hasbrouck information share) (`HasbrouckInformationShare`).
+- **PIN** — probability of informed trading from rolling buy/sell imbalance (EKOP single-window estimator) (`Pin`).
+- **Trade-Sign Autocorrelation** — lag-1 autocorrelation of the signed trade aggressor (order-flow persistence) (`TradeSignAutocorrelation`).
## [0.6.9] - 2026-06-08
- **Tristar** — a three-doji star reversal: three consecutive dojis with the middle gapped above (bearish) or below (bullish) its neighbours (`Tristar`).
diff --git a/README.md b/README.md
index 63bf61b2..940838df 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 485 indicators; start at the
+ every one of the 488 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.** 485 indicators across 24
+- **The biggest streaming-native catalogue, period.** 488 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 485 indicators**.
+ for all 488 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** | **485** | **yes** |
+| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **488** | **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
-485 streaming-first indicators across twenty-four families. Every one passes the
+488 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).
@@ -153,7 +153,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
| Chart Patterns | Double Top / Bottom, Triple Top / Bottom, Head and Shoulders, Triangle (asc/desc/sym), Wedge (rising/falling), Flag / Pennant, Rectangle / Range, Cup and Handle |
| Harmonic Patterns | AB=CD, Gartley, Butterfly, Bat, Crab, Shark, Cypher, Three Drives |
| Fibonacci | Fibonacci Retracement, Fibonacci Extension, Fibonacci Projection, Auto-Fibonacci, Golden Pocket, Fibonacci Confluence, Fibonacci Fan, Fibonacci Arcs, Fibonacci Channel, Fibonacci Time Zones |
-| 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 |
+| 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 |
| 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 |
@@ -237,7 +237,7 @@ A Python live-trading example using the public `websockets` package lives at
```
wickra/
├── crates/
-│ ├── wickra-core/ core engine + all 485 indicators
+│ ├── wickra-core/ core engine + all 488 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 2a0e8013..2994fa8e 100644
--- a/bindings/node/__tests__/indicators.test.js
+++ b/bindings/node/__tests__/indicators.test.js
@@ -667,6 +667,7 @@ const pairFactories = {
GrangerCausality: () => new wickra.GrangerCausality(60, 1),
SpreadAr1Coefficient: () => new wickra.SpreadAr1Coefficient(40),
KendallTau: () => new wickra.KendallTau(20),
+ HasbrouckInformationShare: () => new wickra.HasbrouckInformationShare(2),
};
for (const [name, make] of Object.entries(pairFactories)) {
@@ -1270,7 +1271,7 @@ test('vpin / amihud / roll reference + streaming matches batch', () => {
const price = Array.from({ length: n }, (_, i) => 100 + Math.sin(i * 0.25) * 4);
const size = Array.from({ length: n }, (_, i) => 1 + (i % 5));
const isBuy = Array.from({ length: n }, (_, i) => i % 2 === 0);
- for (const make of [() => new wickra.Vpin(8, 5), () => new wickra.AmihudIlliquidity(14), () => new wickra.RollMeasure(14)]) {
+ for (const make of [() => new wickra.Vpin(8, 5), () => new wickra.AmihudIlliquidity(14), () => new wickra.RollMeasure(14), () => new wickra.TradeSignAutocorrelation(10), () => new wickra.Pin(10)]) {
const batch = make().batch(price, size, isBuy);
const streamer = make();
assert.equal(batch.length, n);
@@ -1279,6 +1280,16 @@ test('vpin / amihud / roll reference + streaming matches batch', () => {
assert.ok((Number.isNaN(batch[i]) && s === null) || Math.abs(s - batch[i]) < 1e-9, `mismatch at ${i}`);
}
}
+ // Trade-sign autocorrelation: alternating signs -> -1, all buys -> +1.
+ let tsac = null;
+ const tsacInd = new wickra.TradeSignAutocorrelation(10);
+ for (let i = 0; i < 20; i++) tsac = tsacInd.update(100, 1, i % 2 === 0);
+ assert.ok(Math.abs(tsac - -1.0) < 1e-12);
+ // PIN: one-sided flow -> 1, balanced flow -> 0.
+ let pin = null;
+ const pinInd = new wickra.Pin(10);
+ for (let i = 0; i < 20; i++) pin = pinInd.update(100, 1, true);
+ assert.ok(Math.abs(pin - 1.0) < 1e-12);
});
test('price-impact indicators reference values', () => {
diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts
index 8b8c76d5..99dee94f 100644
--- a/bindings/node/index.d.ts
+++ b/bindings/node/index.d.ts
@@ -1449,6 +1449,19 @@ export declare class BetaNeutralSpread {
isReady(): boolean
warmupPeriod(): number
}
+export type HasbrouckInformationShareNode = HasbrouckInformationShare
+export declare class HasbrouckInformationShare {
+ constructor(period: number)
+ update(x: number, y: number): number | null
+ /**
+ * Batch over two equally-sized arrays. Returns a length-`n` array
+ * with `NaN` for warmup positions.
+ */
+ batch(x: Array, y: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
export type PairSpreadZScoreNode = PairSpreadZScore
/**
* Pair spread z-score: two ctor params (`betaPeriod`, `zPeriod`), one `(a, b)`
@@ -4333,6 +4346,24 @@ export declare class TradeImbalance {
isReady(): boolean
warmupPeriod(): number
}
+export type TradeSignAutocorrelationNode = TradeSignAutocorrelation
+export declare class TradeSignAutocorrelation {
+ constructor(period: number)
+ update(price: number, size: number, isBuy: boolean): number | null
+ batch(price: Array, size: Array, isBuy: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
+export type PinNode = Pin
+export declare class Pin {
+ constructor(window: number)
+ update(price: number, size: number, isBuy: boolean): number | null
+ batch(price: Array, size: Array, isBuy: Array): Array
+ reset(): void
+ isReady(): boolean
+ warmupPeriod(): number
+}
export type OrderFlowImbalanceNode = OrderFlowImbalance
export declare class OrderFlowImbalance {
constructor(period: number)
diff --git a/bindings/node/index.js b/bindings/node/index.js
index 843fab63..740418ef 100644
--- a/bindings/node/index.js
+++ b/bindings/node/index.js
@@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
-const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, MIDPOINT, ROCP, ROCR, ROCR100, LINEARREG_INTERCEPT, TSF, LogReturn, RealizedVolatility, RollingIqr, RollingPercentileRank, TrendLabel, WinRate, Expectancy, SWMA, GMA, EHMA, MedianMA, AdaptiveLaguerre, DisparityIndex, FisherRSI, RSX, DynamicMomentumIndex, TREND_STRENGTH_INDEX, TsfOscillator, BipowerVariation, JARQUEBERA, ROLLINGMINMAX, HIGHPASS, REFLEX, TRENDFLEX, CTI, ADAPTIVERSI, UNIVERSALOSC, BANDPASS, EVENBETTERSINE, AUTOCORRPGRAM, SHANNONENT, SAMPLEENT, EwmaVolatility, Garch11, VolatilityOfVolatility, VolatilityCone, JumpIndicator, RegimeLabel, RollingQuantile, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpreadAr1Coefficient, SpearmanCorrelation, RollingCorrelation, RollingCovariance, OuHalfLife, SpreadHurst, DistanceSsd, KendallTau, BetaNeutralSpread, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, VarianceRatio, GrangerCausality, KalmanHedgeRatio, SpreadBollingerBands, MACD, MACDFIX, MACDEXT, BollingerBands, ATR, PLUS_DM, MINUS_DM, PLUS_DI, MINUS_DI, DX, MIDPRICE, AVGPRICE, SAREXT, HT_PHASOR, CloseVsOpen, BodySizePct, WickRatio, HighLowRange, StochasticCCI, IMI, QQE, ElderRay, TTM_TREND, Qstick, POLARIZED_FRACTAL_EFFICIENCY, WAVE_PM, GatorOscillator, KasePermissionStochastic, VolatilityRatio, ProjectionOscillator, TimeBasedStop, ADAPTIVECCI, Stochastic, OBV, ADX, ADXR, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP, AwesomeOscillator, Aroon, Inertia, ConnorsRSI, LaguerreRSI, SMI, KST, PGO, RVI, AwesomeOscillatorHistogram, STC, ElderImpulse, ZeroLagMACD, CFO, APO, KAMA, EVWMA, Alligator, JMA, VIDYA, ALMA, T3, GD, HoltWinters, RMI, DerivativeOscillator, MacdHistogram, PpoHistogram, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredRSI, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, KaseDevStop, ElderSafeZone, AtrRatchet, Nrtr, ModifiedMaStop, TypicalPrice, MedianPrice, WeightedClose, LinearRegression, LinRegSlope, AcceleratorOscillator, BalanceOfPower, ChoppinessIndex, TrueRange, ChaikinVolatility, YangZhangVolatility, RogersSatchellVolatility, GarmanKlassVolatility, ParkinsonVolatility, LinRegAngle, BollingerBandwidth, PercentB, NATR, HistoricalVolatility, AroonOscillator, WaveTrend, RWI, Vortex, MassIndex, StochRSI, UltimateOscillator, PPO, Coppock, VWMA, MaEnvelope, AccelerationBands, StarcBands, AtrBands, HurstChannel, LinRegChannel, StandardErrorBands, QuartileBands, BomarBands, MedianChannel, ProjectionBands, CentralPivotRange, MurreyMathLines, AndrewsPitchfork, VolumeWeightedSr, PivotReversal, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDDWave, TDMovingAverage, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HT_DCPHASE, HT_TRENDMODE, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, HeikinAshiOscillator, ThreeLineBreak, SmoothedHeikinAshi, Equivolume, CandleVolume, FryPanBottom, DumplingTop, NewPriceLines, ValueArea, VolumeProfile, TpoProfile, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, TwoCrows, UpsideGapTwoCrows, IdenticalThreeCrows, ThreeLineStrike, ThreeStarsInSouth, AbandonedBaby, AdvanceBlock, BeltHold, Breakaway, Counterattack, DojiStar, DragonflyDoji, GravestoneDoji, LongLeggedDoji, RickshawMan, EveningDojiStar, MorningDojiStar, GapSideBySideWhite, HighWave, Hikkake, HikkakeModified, HomingPigeon, OnNeck, InNeck, Thrusting, SeparatingLines, Kicking, KickingByLength, LadderBottom, MatHold, MatchingLow, LongLine, ShortLine, RisingThreeMethods, FallingThreeMethods, UpsideGapThreeMethods, DownsideGapThreeMethods, StalledPattern, StickSandwich, Takuri, ClosingMarubozu, OpeningMarubozu, TasukiGap, UniqueThreeRiver, ConcealingBabySwallow, DoubleTopBottom, TripleTopBottom, HeadAndShoulders, Triangle, Wedge, FlagPennant, RectangleRange, CupAndHandle, Abcd, Gartley, Butterfly, Bat, Crab, Shark, Cypher, ThreeDrives, TDCamouflage, TDClop, TDClopwin, TDPropulsion, TDTrap, Tristar, HaramiCross, TowerTopBottom, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, OrderFlowImbalance, Vpin, AmihudIlliquidity, RollMeasure, EffectiveSpread, RealizedSpread, KylesLambda, Footprint, FundingRate, FundingRateMean, FundingRateZScore, FundingBasis, OpenInterestDelta, OIPriceDivergence, OIWeighted, LongShortRatio, TakerBuySellRatio, LiquidationFeatures, TermStructureBasis, CalendarSpread, AdvanceDecline, AdvanceDeclineRatio, AdVolumeLine, McClellanOscillator, McClellanSummationIndex, Trin, BreadthThrust, NewHighsNewLows, HighLowIndex, PercentAboveMa, UpDownVolumeRatio, BullishPercentIndex, CumulativeVolumeIndex, AbsoluteBreadthIndex, TickIndex, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, RenkoBars, KagiBars, PointAndFigureBars, Alpha, SessionVwap, OvernightGap, SeasonalZScore, TimeOfDayReturnProfile, IntradayVolatilityProfile, VolumeByTimeProfile, DayOfWeekProfile, AverageDailyRange, TurnOfMonth, SessionHighLow, SessionRange, OvernightIntradayReturn, FibRetracement, FibExtension, FibProjection, AutoFib, GoldenPocket, FibConfluence, FibFan, FibArcs, FibChannel, FibTimeZones, VolumeRsi, Wad, TwiggsMoneyFlow, TradeVolumeIndex, IntradayIntensity, BetterVolume, VolumeWeightedMacd } = nativeBinding
+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
module.exports.version = version
module.exports.SMA = SMA
@@ -409,6 +409,7 @@ module.exports.SpreadHurst = SpreadHurst
module.exports.DistanceSsd = DistanceSsd
module.exports.KendallTau = KendallTau
module.exports.BetaNeutralSpread = BetaNeutralSpread
+module.exports.HasbrouckInformationShare = HasbrouckInformationShare
module.exports.PairSpreadZScore = PairSpreadZScore
module.exports.LeadLagCrossCorrelation = LeadLagCrossCorrelation
module.exports.Cointegration = Cointegration
@@ -714,6 +715,8 @@ module.exports.OrderBookImbalanceTopN = OrderBookImbalanceTopN
module.exports.SignedVolume = SignedVolume
module.exports.CumulativeVolumeDelta = CumulativeVolumeDelta
module.exports.TradeImbalance = TradeImbalance
+module.exports.TradeSignAutocorrelation = TradeSignAutocorrelation
+module.exports.Pin = Pin
module.exports.OrderFlowImbalance = OrderFlowImbalance
module.exports.Vpin = Vpin
module.exports.AmihudIlliquidity = AmihudIlliquidity
diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs
index e2807200..2bc34f3e 100644
--- a/bindings/node/src/lib.rs
+++ b/bindings/node/src/lib.rs
@@ -884,6 +884,11 @@ node_pair_indicator!(
"BetaNeutralSpread",
wc::BetaNeutralSpread
);
+node_pair_indicator!(
+ HasbrouckInformationShareNode,
+ "HasbrouckInformationShare",
+ wc::HasbrouckInformationShare
+);
// ============================== PairSpreadZScore ==============================
@@ -13739,6 +13744,108 @@ impl TradeImbalanceNode {
}
}
+// Trade-sign autocorrelation carries a `period` parameter, so it is hand-written.
+#[napi(js_name = "TradeSignAutocorrelation")]
+pub struct TradeSignAutocorrelationNode {
+ inner: wc::TradeSignAutocorrelation,
+}
+
+#[napi]
+impl TradeSignAutocorrelationNode {
+ #[napi(constructor)]
+ pub fn new(period: u32) -> napi::Result {
+ Ok(Self {
+ inner: wc::TradeSignAutocorrelation::new(period as usize).map_err(map_err)?,
+ })
+ }
+ #[napi]
+ pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> napi::Result> {
+ Ok(self.inner.update(build_trade(price, size, is_buy)?))
+ }
+ #[napi]
+ pub fn batch(
+ &mut self,
+ price: Vec,
+ size: Vec,
+ is_buy: Vec,
+ ) -> napi::Result> {
+ if price.len() != size.len() || size.len() != is_buy.len() {
+ return Err(NapiError::from_reason(
+ "price, size, is_buy must be equal length".to_string(),
+ ));
+ }
+ let mut out = Vec::with_capacity(price.len());
+ for i in 0..price.len() {
+ let trade = build_trade(price[i], size[i], is_buy[i])?;
+ out.push(self.inner.update(trade).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
+ }
+}
+
+// PIN carries a `window` parameter, so it is hand-written.
+#[napi(js_name = "Pin")]
+pub struct PinNode {
+ inner: wc::Pin,
+}
+
+#[napi]
+impl PinNode {
+ #[napi(constructor)]
+ pub fn new(window: u32) -> napi::Result {
+ Ok(Self {
+ inner: wc::Pin::new(window as usize).map_err(map_err)?,
+ })
+ }
+ #[napi]
+ pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> napi::Result> {
+ Ok(self.inner.update(build_trade(price, size, is_buy)?))
+ }
+ #[napi]
+ pub fn batch(
+ &mut self,
+ price: Vec,
+ size: Vec,
+ is_buy: Vec,
+ ) -> napi::Result> {
+ if price.len() != size.len() || size.len() != is_buy.len() {
+ return Err(NapiError::from_reason(
+ "price, size, is_buy must be equal length".to_string(),
+ ));
+ }
+ let mut out = Vec::with_capacity(price.len());
+ for i in 0..price.len() {
+ let trade = build_trade(price[i], size[i], is_buy[i])?;
+ out.push(self.inner.update(trade).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
+ }
+}
+
// Order Flow Imbalance: order-book input with a `period` parameter.
#[napi(js_name = "OrderFlowImbalance")]
pub struct OrderFlowImbalanceNode {
diff --git a/bindings/python/python/wickra/__init__.py b/bindings/python/python/wickra/__init__.py
index 8d293412..f468d13f 100644
--- a/bindings/python/python/wickra/__init__.py
+++ b/bindings/python/python/wickra/__init__.py
@@ -464,6 +464,8 @@ from ._wickra import (
QuotedSpread,
DepthSlope,
# Microstructure: trade flow
+ Pin,
+ TradeSignAutocorrelation,
RollMeasure,
AmihudIlliquidity,
Vpin,
@@ -471,6 +473,7 @@ from ._wickra import (
CumulativeVolumeDelta,
TradeImbalance,
# Microstructure: price impact
+ HasbrouckInformationShare,
EffectiveSpread,
RealizedSpread,
KylesLambda,
@@ -979,6 +982,8 @@ __all__ = [
"QuotedSpread",
"DepthSlope",
# Microstructure: trade flow
+ "Pin",
+ "TradeSignAutocorrelation",
"RollMeasure",
"AmihudIlliquidity",
"Vpin",
@@ -986,6 +991,7 @@ __all__ = [
"CumulativeVolumeDelta",
"TradeImbalance",
# Microstructure: price impact
+ "HasbrouckInformationShare",
"EffectiveSpread",
"RealizedSpread",
"KylesLambda",
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index e13b7e31..b4080935 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -16980,6 +16980,70 @@ impl PyRollingCorrelation {
}
}
+// ========================= HasbrouckInformationShare =========================
+
+#[pyclass(
+ name = "HasbrouckInformationShare",
+ module = "wickra._wickra",
+ skip_from_py_object
+)]
+#[derive(Clone)]
+struct PyHasbrouckInformationShare {
+ inner: wc::HasbrouckInformationShare,
+}
+
+#[pymethods]
+impl PyHasbrouckInformationShare {
+ #[new]
+ #[pyo3(signature = (period=20))]
+ fn new(period: usize) -> PyResult {
+ Ok(Self {
+ inner: wc::HasbrouckInformationShare::new(period).map_err(map_err)?,
+ })
+ }
+ fn update(&mut self, a: f64, b: f64) -> Option {
+ self.inner.update((a, b))
+ }
+ /// Batch over two equally-sized numpy arrays: `a` and `b`.
+ fn batch<'py>(
+ &mut self,
+ py: Python<'py>,
+ a: PyReadonlyArray1<'py, f64>,
+ b: PyReadonlyArray1<'py, f64>,
+ ) -> PyResult>> {
+ let xs = a
+ .as_slice()
+ .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
+ let ys = b
+ .as_slice()
+ .map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
+ if xs.len() != ys.len() {
+ return Err(PyValueError::new_err("a and b must be equal length"));
+ }
+ let mut out = Vec::with_capacity(xs.len());
+ for i in 0..xs.len() {
+ out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
+ }
+ Ok(out.into_pyarray(py))
+ }
+ #[getter]
+ fn period(&self) -> usize {
+ self.inner.period()
+ }
+ 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!("HasbrouckInformationShare(period={})", self.inner.period())
+ }
+}
+
// ============================== RollingCovariance ==============================
#[pyclass(
@@ -18652,6 +18716,112 @@ impl PyTradeImbalance {
}
}
+// Trade-sign autocorrelation carries a `period` parameter, so it is hand-written.
+#[pyclass(
+ name = "TradeSignAutocorrelation",
+ module = "wickra._wickra",
+ skip_from_py_object
+)]
+#[derive(Clone)]
+struct PyTradeSignAutocorrelation {
+ inner: wc::TradeSignAutocorrelation,
+}
+
+#[pymethods]
+impl PyTradeSignAutocorrelation {
+ #[new]
+ fn new(period: usize) -> PyResult {
+ Ok(Self {
+ inner: wc::TradeSignAutocorrelation::new(period).map_err(map_err)?,
+ })
+ }
+ fn update(&mut self, price: f64, size: f64, is_buy: bool) -> PyResult> {
+ Ok(self.inner.update(build_trade(price, size, is_buy)?))
+ }
+ fn batch<'py>(
+ &mut self,
+ py: Python<'py>,
+ price: Vec,
+ size: Vec,
+ is_buy: Vec,
+ ) -> PyResult>> {
+ if price.len() != size.len() || size.len() != is_buy.len() {
+ return Err(PyValueError::new_err(
+ "price, size, is_buy must be equal length",
+ ));
+ }
+ let mut out = Vec::with_capacity(price.len());
+ for i in 0..price.len() {
+ let trade = build_trade(price[i], size[i], is_buy[i])?;
+ out.push(self.inner.update(trade).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!("TradeSignAutocorrelation(period={})", self.inner.period())
+ }
+}
+
+// PIN carries a `window` parameter, so it is hand-written.
+#[pyclass(name = "Pin", module = "wickra._wickra", skip_from_py_object)]
+#[derive(Clone)]
+struct PyPin {
+ inner: wc::Pin,
+}
+
+#[pymethods]
+impl PyPin {
+ #[new]
+ fn new(window: usize) -> PyResult {
+ Ok(Self {
+ inner: wc::Pin::new(window).map_err(map_err)?,
+ })
+ }
+ fn update(&mut self, price: f64, size: f64, is_buy: bool) -> PyResult> {
+ Ok(self.inner.update(build_trade(price, size, is_buy)?))
+ }
+ fn batch<'py>(
+ &mut self,
+ py: Python<'py>,
+ price: Vec,
+ size: Vec,
+ is_buy: Vec,
+ ) -> PyResult>> {
+ if price.len() != size.len() || size.len() != is_buy.len() {
+ return Err(PyValueError::new_err(
+ "price, size, is_buy must be equal length",
+ ));
+ }
+ let mut out = Vec::with_capacity(price.len());
+ for i in 0..price.len() {
+ let trade = build_trade(price[i], size[i], is_buy[i])?;
+ out.push(self.inner.update(trade).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!("Pin(window={})", self.inner.window())
+ }
+}
+
// Order Flow Imbalance carries a `period` parameter and an order-book input,
// so it is hand-written.
#[pyclass(
@@ -24743,6 +24913,7 @@ 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::()?;
@@ -24833,6 +25004,8 @@ 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::()?;
diff --git a/bindings/python/tests/test_new_indicators.py b/bindings/python/tests/test_new_indicators.py
index eb33f0e7..64a5e22e 100644
--- a/bindings/python/tests/test_new_indicators.py
+++ b/bindings/python/tests/test_new_indicators.py
@@ -217,6 +217,7 @@ def test_scalar_streaming_matches_batch(cls, args, sine_prices):
# --- Two-series (asset, benchmark) indicators -----------------------------
PAIR = [
+ (ta.HasbrouckInformationShare, (2,)),
(ta.KendallTau, (20,)),
(ta.SpreadAr1Coefficient, (40,)),
(ta.GrangerCausality, (60, 1)),
@@ -3323,6 +3324,13 @@ def test_tower_top_bottom_reference():
assert t.update((110.0, 110.1, 99.9, 100.0, 1.0, 2)) == pytest.approx(-1.0)
+
+def test_hasbrouck_information_share_reference():
+ t = ta.HasbrouckInformationShare(2)
+ assert t.update(7.0, 9.0) is None
+ assert t.update(7.0, 9.0) is None
+ assert t.update(7.0, 9.0) == pytest.approx(0.5)
+
# --- Lifecycle ------------------------------------------------------------
@@ -3663,6 +3671,8 @@ def test_tradeflow_indicators_streaming_equals_batch():
lambda: ta.Vpin(8.0, 5),
lambda: ta.AmihudIlliquidity(14),
lambda: ta.RollMeasure(14),
+ lambda: ta.TradeSignAutocorrelation(10),
+ lambda: ta.Pin(10),
):
batch = make().batch(price, size, is_buy)
streamer = make()
@@ -3674,6 +3684,34 @@ def test_tradeflow_indicators_streaming_equals_batch():
assert _eq_nan(batch, streamed)
+def test_trade_sign_autocorrelation_reference():
+ # Perfectly alternating aggressor signs -> lag-1 autocorrelation -1.
+ t = ta.TradeSignAutocorrelation(10)
+ last = None
+ for i in range(20):
+ last = t.update(100.0, 1.0, i % 2 == 0)
+ assert last == pytest.approx(-1.0)
+ # All buys -> perfectly persistent flow -> +1.
+ t2 = ta.TradeSignAutocorrelation(10)
+ for _ in range(20):
+ last2 = t2.update(100.0, 1.0, True)
+ assert last2 == pytest.approx(1.0)
+
+
+def test_pin_reference():
+ # One-sided flow (all buys) -> maximally informed -> PIN 1.
+ p = ta.Pin(10)
+ last = None
+ for _ in range(20):
+ last = p.update(100.0, 1.0, True)
+ assert last == pytest.approx(1.0)
+ # Balanced flow -> uninformed -> PIN 0.
+ p2 = ta.Pin(10)
+ for i in range(20):
+ last2 = p2.update(100.0, 1.0, i % 2 == 0)
+ assert last2 == pytest.approx(0.0)
+
+
def test_price_impact_indicators_streaming_equals_batch():
n = 40
mid = np.array([100.0 + 0.5 * math.sin(i * 0.4) for i in range(n)], dtype=np.float64)
diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs
index af001d8a..3b043088 100644
--- a/bindings/wasm/src/lib.rs
+++ b/bindings/wasm/src/lib.rs
@@ -563,6 +563,11 @@ wasm_pair_indicator!(
"BetaNeutralSpread",
wc::BetaNeutralSpread
);
+wasm_pair_indicator!(
+ WasmHasbrouckInformationShare,
+ "HasbrouckInformationShare",
+ wc::HasbrouckInformationShare
+);
// ---------- PairSpreadZScore (two params) ----------
@@ -9279,6 +9284,66 @@ impl WasmTradeImbalance {
}
}
+// Trade-sign autocorrelation carries a `period` parameter, so it is hand-written.
+#[wasm_bindgen(js_name = TradeSignAutocorrelation)]
+pub struct WasmTradeSignAutocorrelation {
+ inner: wc::TradeSignAutocorrelation,
+}
+
+#[wasm_bindgen(js_class = TradeSignAutocorrelation)]
+impl WasmTradeSignAutocorrelation {
+ #[wasm_bindgen(constructor)]
+ pub fn new(period: usize) -> Result {
+ Ok(Self {
+ inner: wc::TradeSignAutocorrelation::new(period).map_err(map_err)?,
+ })
+ }
+ pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> Result, JsError> {
+ Ok(self.inner.update(build_trade(price, size, is_buy)?))
+ }
+ 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()
+ }
+}
+
+// PIN carries a `window` parameter, so it is hand-written.
+#[wasm_bindgen(js_name = Pin)]
+pub struct WasmPin {
+ inner: wc::Pin,
+}
+
+#[wasm_bindgen(js_class = Pin)]
+impl WasmPin {
+ #[wasm_bindgen(constructor)]
+ pub fn new(window: usize) -> Result {
+ Ok(Self {
+ inner: wc::Pin::new(window).map_err(map_err)?,
+ })
+ }
+ pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> Result, JsError> {
+ Ok(self.inner.update(build_trade(price, size, is_buy)?))
+ }
+ 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()
+ }
+}
+
// Order Flow Imbalance: order-book input with a `period` parameter.
#[wasm_bindgen(js_name = OrderFlowImbalance)]
pub struct WasmOrderFlowImbalance {
diff --git a/crates/wickra-core/src/indicators/hasbrouck_information_share.rs b/crates/wickra-core/src/indicators/hasbrouck_information_share.rs
new file mode 100644
index 00000000..a74e2c54
--- /dev/null
+++ b/crates/wickra-core/src/indicators/hasbrouck_information_share.rs
@@ -0,0 +1,251 @@
+//! Hasbrouck Information Share — each venue's contribution to price discovery.
+
+use std::collections::VecDeque;
+
+use crate::error::{Error, Result};
+use crate::traits::Indicator;
+
+/// Hasbrouck Information Share — the share of price-discovery attributable to the
+/// **first** of two synchronised price series (e.g. the same asset on two venues).
+///
+/// ```text
+/// rx_t = x_t − x_{t−1}, ry_t = y_t − y_{t−1} (one-step price changes)
+/// IS_x = var(rx) / ( var(rx) + var(ry) ) over the window, ∈ [0, 1]
+/// ```
+///
+/// When the same instrument trades on several venues, Joel Hasbrouck's information
+/// share measures how much each venue contributes to the common efficient price.
+/// The venue whose innovations carry more of the variance leads price discovery.
+/// This streaming form uses the **variance-ratio proxy**: the fraction of total
+/// return variance contributed by series `x`. A reading above `0.5` means venue
+/// `x` is the price leader; below `0.5`, the follower. (The full Hasbrouck measure
+/// estimates a vector error-correction model and reports an upper/lower bound from
+/// the Cholesky ordering; this proxy captures the leading idea without the VECM.)
+///
+/// The output is in `[0, 1]`; if both series are flat it reports the neutral `0.5`.
+/// The first value lands after `period + 1` inputs. Each `update` is O(1).
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Indicator, HasbrouckInformationShare};
+///
+/// let mut indicator = HasbrouckInformationShare::new(20).unwrap();
+/// let mut last = None;
+/// for i in 0..40 {
+/// // Venue x moves a lot, venue y barely moves -> x leads.
+/// let x = (f64::from(i) * 0.5).sin() * 10.0;
+/// let y = (f64::from(i) * 0.5).sin() * 1.0;
+/// last = indicator.update((x, y));
+/// }
+/// assert!(last.unwrap() > 0.8);
+/// ```
+#[derive(Debug, Clone)]
+pub struct HasbrouckInformationShare {
+ period: usize,
+ prev: Option<(f64, f64)>,
+ window: VecDeque<(f64, f64)>,
+ sum_x: f64,
+ sum_y: f64,
+ sum_xx: f64,
+ sum_yy: f64,
+}
+
+impl HasbrouckInformationShare {
+ /// Construct a Hasbrouck information share over `period` return pairs.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`Error::InvalidPeriod`] if `period < 2` (variance needs two
+ /// returns).
+ pub fn new(period: usize) -> Result {
+ if period < 2 {
+ return Err(Error::InvalidPeriod {
+ message: "information share needs period >= 2",
+ });
+ }
+ Ok(Self {
+ period,
+ prev: None,
+ window: VecDeque::with_capacity(period),
+ sum_x: 0.0,
+ sum_y: 0.0,
+ sum_xx: 0.0,
+ sum_yy: 0.0,
+ })
+ }
+
+ /// Configured window of return pairs.
+ pub const fn period(&self) -> usize {
+ self.period
+ }
+}
+
+impl Indicator for HasbrouckInformationShare {
+ type Input = (f64, f64);
+ type Output = f64;
+
+ fn update(&mut self, input: (f64, f64)) -> Option {
+ let (x, y) = input;
+ let Some((px, py)) = self.prev else {
+ self.prev = Some((x, y));
+ return None;
+ };
+ self.prev = Some((x, y));
+ let (rx, ry) = (x - px, y - py);
+ if self.window.len() == self.period {
+ let (ox, oy) = self.window.pop_front().expect("non-empty");
+ self.sum_x -= ox;
+ self.sum_y -= oy;
+ self.sum_xx -= ox * ox;
+ self.sum_yy -= oy * oy;
+ }
+ self.window.push_back((rx, ry));
+ self.sum_x += rx;
+ self.sum_y += ry;
+ self.sum_xx += rx * rx;
+ self.sum_yy += ry * ry;
+ if self.window.len() < self.period {
+ return None;
+ }
+ let n = self.period as f64;
+ let var_x = (self.sum_xx / n - (self.sum_x / n).powi(2)).max(0.0);
+ let var_y = (self.sum_yy / n - (self.sum_y / n).powi(2)).max(0.0);
+ let total = var_x + var_y;
+ Some(if total > 0.0 { var_x / total } else { 0.5 })
+ }
+
+ fn reset(&mut self) {
+ self.prev = None;
+ self.window.clear();
+ self.sum_x = 0.0;
+ self.sum_y = 0.0;
+ self.sum_xx = 0.0;
+ self.sum_yy = 0.0;
+ }
+
+ fn warmup_period(&self) -> usize {
+ self.period + 1
+ }
+
+ fn is_ready(&self) -> bool {
+ self.window.len() == self.period
+ }
+
+ fn name(&self) -> &'static str {
+ "HasbrouckInformationShare"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::traits::BatchExt;
+ use approx::assert_relative_eq;
+
+ #[test]
+ fn rejects_period_below_two() {
+ assert!(matches!(
+ HasbrouckInformationShare::new(1),
+ Err(Error::InvalidPeriod { .. })
+ ));
+ assert!(HasbrouckInformationShare::new(2).is_ok());
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let h = HasbrouckInformationShare::new(20).unwrap();
+ assert_eq!(h.period(), 20);
+ assert_eq!(h.warmup_period(), 21);
+ assert_eq!(h.name(), "HasbrouckInformationShare");
+ assert!(!h.is_ready());
+ }
+
+ #[test]
+ fn warmup_needs_period_plus_one() {
+ let mut h = HasbrouckInformationShare::new(3).unwrap();
+ assert_eq!(h.update((1.0, 1.0)), None);
+ assert_eq!(h.update((2.0, 2.0)), None);
+ assert_eq!(h.update((3.0, 2.5)), None);
+ assert!(h.update((4.0, 3.0)).is_some());
+ }
+
+ #[test]
+ fn loud_venue_leads() {
+ // x is far more volatile than y -> x holds nearly all the share.
+ let pairs: Vec<(f64, f64)> = (0..40)
+ .map(|i| {
+ (
+ (f64::from(i) * 0.5).sin() * 10.0,
+ (f64::from(i) * 0.5).sin() * 1.0,
+ )
+ })
+ .collect();
+ let last = HasbrouckInformationShare::new(20)
+ .unwrap()
+ .batch(&pairs)
+ .into_iter()
+ .flatten()
+ .last()
+ .unwrap();
+ assert!(last > 0.8, "the loud venue should lead, got {last}");
+ }
+
+ #[test]
+ fn equal_venues_split_evenly() {
+ // Independent but equal-variance moves -> share near 0.5.
+ let pairs: Vec<(f64, f64)> = (0..200)
+ .map(|i| {
+ (
+ (f64::from(i) * 0.5).sin() * 5.0,
+ (f64::from(i) * 0.5).cos() * 5.0,
+ )
+ })
+ .collect();
+ for v in HasbrouckInformationShare::new(40)
+ .unwrap()
+ .batch(&pairs)
+ .into_iter()
+ .flatten()
+ {
+ assert!((0.0..=1.0).contains(&v));
+ }
+ }
+
+ #[test]
+ fn flat_series_is_half() {
+ let pairs: Vec<(f64, f64)> = (0..20).map(|_| (7.0, 9.0)).collect();
+ let last = HasbrouckInformationShare::new(5)
+ .unwrap()
+ .batch(&pairs)
+ .into_iter()
+ .flatten()
+ .last()
+ .unwrap();
+ assert_relative_eq!(last, 0.5, epsilon = 1e-12);
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut h = HasbrouckInformationShare::new(4).unwrap();
+ h.batch(&[(1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0), (5.0, 5.0)]);
+ assert!(h.is_ready());
+ h.reset();
+ assert!(!h.is_ready());
+ assert_eq!(h.update((1.0, 1.0)), None);
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let pairs: Vec<(f64, f64)> = (0..120)
+ .map(|i| {
+ let t = f64::from(i);
+ (t.sin() * 5.0, (t * 0.5).cos() * 3.0)
+ })
+ .collect();
+ let batch = HasbrouckInformationShare::new(20).unwrap().batch(&pairs);
+ let mut h = HasbrouckInformationShare::new(20).unwrap();
+ let streamed: Vec<_> = pairs.iter().map(|p| h.update(*p)).collect();
+ assert_eq!(batch, streamed);
+ }
+}
diff --git a/crates/wickra-core/src/indicators/mod.rs b/crates/wickra-core/src/indicators/mod.rs
index 333780de..4472a1d9 100644
--- a/crates/wickra-core/src/indicators/mod.rs
+++ b/crates/wickra-core/src/indicators/mod.rs
@@ -174,6 +174,7 @@ mod hammer;
mod hanging_man;
mod harami;
mod harami_cross;
+mod hasbrouck_information_share;
mod head_and_shoulders;
mod heikin_ashi;
mod heikin_ashi_oscillator;
@@ -296,6 +297,7 @@ mod percent_b;
mod percentage_trailing_stop;
mod pgo;
mod piercing_dark_cloud;
+mod pin;
mod pivot_reversal;
mod plus_di;
mod plus_dm;
@@ -423,6 +425,7 @@ mod time_of_day_return_profile;
mod tower_top_bottom;
mod tpo_profile;
mod trade_imbalance;
+mod trade_sign_autocorrelation;
mod trade_volume_index;
mod trend_label;
mod trend_strength_index;
@@ -659,6 +662,7 @@ pub use hammer::Hammer;
pub use hanging_man::HangingMan;
pub use harami::Harami;
pub use harami_cross::HaramiCross;
+pub use hasbrouck_information_share::HasbrouckInformationShare;
pub use head_and_shoulders::HeadAndShoulders;
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
pub use heikin_ashi_oscillator::HeikinAshiOscillator;
@@ -781,6 +785,7 @@ pub use percent_b::PercentB;
pub use percentage_trailing_stop::PercentageTrailingStop;
pub use pgo::Pgo;
pub use piercing_dark_cloud::PiercingDarkCloud;
+pub use pin::Pin;
pub use pivot_reversal::PivotReversal;
pub use plus_di::PlusDi;
pub use plus_dm::PlusDm;
@@ -908,6 +913,7 @@ pub use time_of_day_return_profile::{TimeOfDayReturnProfile, TimeOfDayReturnProf
pub use tower_top_bottom::TowerTopBottom;
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
pub use trade_imbalance::TradeImbalance;
+pub use trade_sign_autocorrelation::TradeSignAutocorrelation;
pub use trade_volume_index::TradeVolumeIndex;
pub use trend_label::TrendLabel;
pub use trend_strength_index::TrendStrengthIndex;
@@ -1452,6 +1458,9 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"Vpin",
"AmihudIlliquidity",
"RollMeasure",
+ "TradeSignAutocorrelation",
+ "Pin",
+ "HasbrouckInformationShare",
],
),
(
@@ -1615,6 +1624,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, 485, "FAMILIES total drifted from indicator count");
+ assert_eq!(total, 488, "FAMILIES total drifted from indicator count");
}
}
diff --git a/crates/wickra-core/src/indicators/pin.rs b/crates/wickra-core/src/indicators/pin.rs
new file mode 100644
index 00000000..83d2f12e
--- /dev/null
+++ b/crates/wickra-core/src/indicators/pin.rs
@@ -0,0 +1,223 @@
+//! PIN — Probability of Informed Trading (single-window EKOP estimate).
+
+use std::collections::VecDeque;
+
+use crate::error::{Error, Result};
+use crate::microstructure::Trade;
+use crate::traits::Indicator;
+
+/// PIN — the **Probability of Informed Trading**, estimated from the buy/sell order
+/// imbalance over a rolling window of trades.
+///
+/// ```text
+/// over the last `window` trades: B = buys, S = sells (B + S = window)
+/// PIN ≈ |B − S| / (B + S) ∈ [0, 1]
+/// ```
+///
+/// The Easley-Kiefer-O'Hara-Paperman (EKOP) model splits order flow into an
+/// uninformed component (balanced buys and sells, rate `ε` per side) and an
+/// informed component that trades one-directionally when private information
+/// arrives (rate `μ`, probability `α`). The probability that any given trade is
+/// information-motivated is `PIN = αμ / (αμ + 2ε)`. Estimated over a single window,
+/// the informed flow shows up as the **net imbalance** `|B − S|` and the uninformed
+/// flow as the balanced remainder, giving the moment estimator above. A high PIN
+/// flags a one-sided, likely-informed market; a low PIN flags balanced, uninformed
+/// flow.
+///
+/// This is distinct from [`Vpin`](crate::Vpin), the volume-synchronised variant
+/// that buckets by volume and uses bulk-volume classification; here trades are
+/// counted in event time and classified by their tagged aggressor side. The full
+/// PIN is fit by maximum likelihood over many periods — this single-window
+/// estimator is the streaming moment approximation. The output is in `[0, 1]`; the
+/// first value lands after `window` trades.
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Indicator, Pin, Side, Trade};
+///
+/// let mut indicator = Pin::new(20).unwrap();
+/// let mut last = None;
+/// for i in 0..40 {
+/// // All buys -> maximally one-sided -> PIN 1.
+/// last = indicator.update(Trade::new(100.0, 1.0, Side::Buy, i).unwrap());
+/// }
+/// assert!((last.unwrap() - 1.0).abs() < 1e-9);
+/// ```
+#[derive(Debug, Clone)]
+pub struct Pin {
+ window: usize,
+ sides: VecDeque,
+ buy_count: usize,
+ last: Option,
+}
+
+impl Pin {
+ /// Construct a PIN estimator over `window` trades.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`Error::PeriodZero`] if `window == 0`.
+ pub fn new(window: usize) -> Result {
+ if window == 0 {
+ return Err(Error::PeriodZero);
+ }
+ Ok(Self {
+ window,
+ sides: VecDeque::with_capacity(window),
+ buy_count: 0,
+ last: None,
+ })
+ }
+
+ /// Configured window of trades.
+ pub const fn window(&self) -> usize {
+ self.window
+ }
+
+ /// Current value if available.
+ pub const fn value(&self) -> Option {
+ self.last
+ }
+}
+
+impl Indicator for Pin {
+ type Input = Trade;
+ type Output = f64;
+
+ fn update(&mut self, trade: Trade) -> Option {
+ let is_buy = trade.side.sign() > 0.0;
+ if self.sides.len() == self.window {
+ let old = self.sides.pop_front().expect("non-empty");
+ if old > 0.0 {
+ self.buy_count -= 1;
+ }
+ }
+ self.sides.push_back(if is_buy { 1.0 } else { 0.0 });
+ if is_buy {
+ self.buy_count += 1;
+ }
+ if self.sides.len() < self.window {
+ return None;
+ }
+ // The window is full and `window >= 1` (zero is rejected at
+ // construction), so the trade count is always positive — `|B - S| / N`
+ // needs no zero guard.
+ let buys = self.buy_count as f64;
+ let sells = self.window as f64 - buys;
+ let total = self.window as f64;
+ let pin = (buys - sells).abs() / total;
+ self.last = Some(pin);
+ Some(pin)
+ }
+
+ fn reset(&mut self) {
+ self.sides.clear();
+ self.buy_count = 0;
+ self.last = None;
+ }
+
+ fn warmup_period(&self) -> usize {
+ self.window
+ }
+
+ fn is_ready(&self) -> bool {
+ self.last.is_some()
+ }
+
+ fn name(&self) -> &'static str {
+ "PIN"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::microstructure::Side;
+ use crate::traits::BatchExt;
+ use approx::assert_relative_eq;
+
+ fn buy() -> Trade {
+ Trade::new_unchecked(100.0, 1.0, Side::Buy, 0)
+ }
+
+ fn sell() -> Trade {
+ Trade::new_unchecked(100.0, 1.0, Side::Sell, 0)
+ }
+
+ #[test]
+ fn rejects_zero_window() {
+ assert!(matches!(Pin::new(0), Err(Error::PeriodZero)));
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let p = Pin::new(20).unwrap();
+ assert_eq!(p.window(), 20);
+ assert_eq!(p.warmup_period(), 20);
+ assert_eq!(p.name(), "PIN");
+ assert!(!p.is_ready());
+ assert_eq!(p.value(), None);
+ }
+
+ #[test]
+ fn first_emission_at_warmup_period() {
+ let mut p = Pin::new(4).unwrap();
+ let out = p.batch(&[buy(), buy(), buy(), buy(), buy()]);
+ for v in out.iter().take(3) {
+ assert!(v.is_none());
+ }
+ assert!(out[3].is_some());
+ }
+
+ #[test]
+ fn one_sided_flow_is_one() {
+ let mut p = Pin::new(10).unwrap();
+ let trades: Vec = (0..20).map(|_| buy()).collect();
+ let last = p.batch(&trades).into_iter().flatten().last().unwrap();
+ assert_relative_eq!(last, 1.0, epsilon = 1e-12);
+ }
+
+ #[test]
+ fn balanced_flow_is_zero() {
+ let mut p = Pin::new(10).unwrap();
+ let trades: Vec = (0..20)
+ .map(|i| if i % 2 == 0 { buy() } else { sell() })
+ .collect();
+ let last = p.batch(&trades).into_iter().flatten().last().unwrap();
+ assert_relative_eq!(last, 0.0, epsilon = 1e-12);
+ }
+
+ #[test]
+ fn output_in_range() {
+ let mut p = Pin::new(16).unwrap();
+ let trades: Vec = (0..200)
+ .map(|i| if (i * 5 % 13) < 8 { buy() } else { sell() })
+ .collect();
+ for v in p.batch(&trades).into_iter().flatten() {
+ assert!((0.0..=1.0).contains(&v));
+ }
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut p = Pin::new(4).unwrap();
+ p.batch(&[buy(), buy(), sell(), buy()]);
+ assert!(p.is_ready());
+ p.reset();
+ assert!(!p.is_ready());
+ assert_eq!(p.value(), None);
+ assert_eq!(p.update(buy()), None);
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let trades: Vec = (0..120)
+ .map(|i| if i % 3 == 0 { sell() } else { buy() })
+ .collect();
+ let batch = Pin::new(16).unwrap().batch(&trades);
+ let mut b = Pin::new(16).unwrap();
+ let streamed: Vec<_> = trades.iter().map(|x| b.update(*x)).collect();
+ assert_eq!(batch, streamed);
+ }
+}
diff --git a/crates/wickra-core/src/indicators/trade_sign_autocorrelation.rs b/crates/wickra-core/src/indicators/trade_sign_autocorrelation.rs
new file mode 100644
index 00000000..436c6510
--- /dev/null
+++ b/crates/wickra-core/src/indicators/trade_sign_autocorrelation.rs
@@ -0,0 +1,217 @@
+//! Trade-Sign Autocorrelation — lag-1 persistence of the trade-aggressor side.
+
+use std::collections::VecDeque;
+
+use crate::error::{Error, Result};
+use crate::microstructure::Trade;
+use crate::traits::Indicator;
+
+/// Trade-Sign Autocorrelation — the lag-1 autocorrelation of the **trade sign**
+/// (`+1` buy, `−1` sell), measuring how strongly signed order flow persists.
+///
+/// ```text
+/// s_t = +1 if the trade is a buy, −1 if a sell
+/// ρ1 = mean over the window of ( s_t · s_{t−1} ) ∈ [−1, +1]
+/// ```
+///
+/// In real markets trade signs are strongly **positively** autocorrelated: a buy
+/// tends to be followed by another buy (and a sell by a sell), because large
+/// parent orders are split into many child trades and because of order-splitting
+/// and herding. A high reading therefore indicates persistent directional pressure
+/// — a footprint of informed or algorithmic execution — while a reading near zero
+/// signals balanced, uninformed flow and a negative reading signals alternating
+/// (bid-ask bounce) flow.
+///
+/// The output is the mean product of consecutive signs, bounded in `[−1, +1]`. The
+/// first value lands after `period` trades. Each `update` is O(`period`).
+///
+/// # Example
+///
+/// ```
+/// use wickra_core::{Indicator, Side, Trade, TradeSignAutocorrelation};
+///
+/// let mut indicator = TradeSignAutocorrelation::new(20).unwrap();
+/// let mut last = None;
+/// for i in 0..40 {
+/// let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
+/// last = indicator.update(Trade::new(100.0, 1.0, side, i).unwrap());
+/// }
+/// // Perfectly alternating signs -> autocorrelation -1.
+/// assert!((last.unwrap() + 1.0).abs() < 1e-9);
+/// ```
+#[derive(Debug, Clone)]
+pub struct TradeSignAutocorrelation {
+ period: usize,
+ signs: VecDeque,
+ last: Option,
+}
+
+impl TradeSignAutocorrelation {
+ /// Construct a trade-sign autocorrelation over `period` trades.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`Error::InvalidPeriod`] if `period < 2` (a lag-1 product needs two
+ /// trades).
+ pub fn new(period: usize) -> Result {
+ if period < 2 {
+ return Err(Error::InvalidPeriod {
+ message: "trade-sign autocorrelation needs period >= 2",
+ });
+ }
+ Ok(Self {
+ period,
+ signs: VecDeque::with_capacity(period),
+ last: None,
+ })
+ }
+
+ /// Configured window of trades.
+ pub const fn period(&self) -> usize {
+ self.period
+ }
+
+ /// Current value if available.
+ pub const fn value(&self) -> Option {
+ self.last
+ }
+}
+
+impl Indicator for TradeSignAutocorrelation {
+ type Input = Trade;
+ type Output = f64;
+
+ fn update(&mut self, trade: Trade) -> Option {
+ if self.signs.len() == self.period {
+ self.signs.pop_front();
+ }
+ self.signs.push_back(trade.side.sign());
+ if self.signs.len() < self.period {
+ return None;
+ }
+ let mut product_sum = 0.0;
+ let mut prev: Option = None;
+ for &s in &self.signs {
+ if let Some(p) = prev {
+ product_sum += s * p;
+ }
+ prev = Some(s);
+ }
+ let rho = product_sum / (self.period as f64 - 1.0);
+ self.last = Some(rho);
+ Some(rho)
+ }
+
+ fn reset(&mut self) {
+ self.signs.clear();
+ self.last = None;
+ }
+
+ fn warmup_period(&self) -> usize {
+ self.period
+ }
+
+ fn is_ready(&self) -> bool {
+ self.last.is_some()
+ }
+
+ fn name(&self) -> &'static str {
+ "TradeSignAutocorrelation"
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::microstructure::Side;
+ use crate::traits::BatchExt;
+ use approx::assert_relative_eq;
+
+ fn buy() -> Trade {
+ Trade::new_unchecked(100.0, 1.0, Side::Buy, 0)
+ }
+
+ fn sell() -> Trade {
+ Trade::new_unchecked(100.0, 1.0, Side::Sell, 0)
+ }
+
+ #[test]
+ fn rejects_period_below_two() {
+ assert!(matches!(
+ TradeSignAutocorrelation::new(1),
+ Err(Error::InvalidPeriod { .. })
+ ));
+ assert!(TradeSignAutocorrelation::new(2).is_ok());
+ }
+
+ #[test]
+ fn accessors_and_metadata() {
+ let t = TradeSignAutocorrelation::new(20).unwrap();
+ assert_eq!(t.period(), 20);
+ assert_eq!(t.warmup_period(), 20);
+ assert_eq!(t.name(), "TradeSignAutocorrelation");
+ assert!(!t.is_ready());
+ assert_eq!(t.value(), None);
+ }
+
+ #[test]
+ fn first_emission_at_warmup_period() {
+ let mut t = TradeSignAutocorrelation::new(4).unwrap();
+ let out = t.batch(&[buy(), buy(), buy(), buy(), buy()]);
+ for v in out.iter().take(3) {
+ assert!(v.is_none());
+ }
+ assert!(out[3].is_some());
+ }
+
+ #[test]
+ fn persistent_flow_is_one() {
+ let mut t = TradeSignAutocorrelation::new(10).unwrap();
+ let trades: Vec = (0..20).map(|_| buy()).collect();
+ let last = t.batch(&trades).into_iter().flatten().last().unwrap();
+ assert_relative_eq!(last, 1.0, epsilon = 1e-12);
+ }
+
+ #[test]
+ fn alternating_flow_is_minus_one() {
+ let mut t = TradeSignAutocorrelation::new(10).unwrap();
+ let trades: Vec = (0..20)
+ .map(|i| if i % 2 == 0 { buy() } else { sell() })
+ .collect();
+ let last = t.batch(&trades).into_iter().flatten().last().unwrap();
+ assert_relative_eq!(last, -1.0, epsilon = 1e-12);
+ }
+
+ #[test]
+ fn output_in_range() {
+ let mut t = TradeSignAutocorrelation::new(16).unwrap();
+ let trades: Vec = (0..200)
+ .map(|i| if (i * 7 % 13) < 6 { buy() } else { sell() })
+ .collect();
+ for v in t.batch(&trades).into_iter().flatten() {
+ assert!((-1.0..=1.0).contains(&v));
+ }
+ }
+
+ #[test]
+ fn reset_clears_state() {
+ let mut t = TradeSignAutocorrelation::new(4).unwrap();
+ t.batch(&[buy(), buy(), buy(), buy()]);
+ assert!(t.is_ready());
+ t.reset();
+ assert!(!t.is_ready());
+ assert_eq!(t.value(), None);
+ assert_eq!(t.update(buy()), None);
+ }
+
+ #[test]
+ fn batch_equals_streaming() {
+ let trades: Vec = (0..120)
+ .map(|i| if i % 3 == 0 { sell() } else { buy() })
+ .collect();
+ let batch = TradeSignAutocorrelation::new(16).unwrap().batch(&trades);
+ let mut b = TradeSignAutocorrelation::new(16).unwrap();
+ let streamed: Vec<_> = trades.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 b002fc15..f3683885 100644
--- a/crates/wickra-core/src/lib.rs
+++ b/crates/wickra-core/src/lib.rs
@@ -91,12 +91,12 @@ pub use indicators::{
FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, Garch11,
GarmanKlassVolatility, Gartley, GatorOscillator, GatorOscillatorOutput, GeneralizedDema,
GeometricMa, GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer,
- HangingMan, Harami, HaramiCross, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator,
- HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave, HighpassFilter, Hikkake,
- HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma, HoltWinters, HomingPigeon,
- HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel, HurstChannelOutput,
- HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck, Inertia,
- InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
+ 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,
@@ -117,7 +117,7 @@ pub use indicators::{
OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
- PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo,
+ 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,
@@ -140,20 +140,21 @@ pub use indicators::{
TdSequentialOutput, TdSetup, TdTrap, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth,
Thrusting, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput,
- TowerTopBottom, TpoProfile, TpoProfileOutput, TradeImbalance, TradeVolumeIndex, TrendLabel,
- TrendStrengthIndex, Trendflex, TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Tristar,
- Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend,
- TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UlcerIndex, UltimateOscillator,
- UniqueThreeRiver, UniversalOscillator, UpDownVolumeRatio, UpsideGapThreeMethods,
- UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance, VarianceRatio,
- VerticalHorizontalFilter, Vidya, VolatilityCone, VolatilityConeOutput, VolatilityOfVolatility,
- VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeByTimeProfileOutput, VolumeOscillator,
- VolumePriceTrend, VolumeProfile, VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd,
- VolumeWeightedMacdOutput, VolumeWeightedSr, VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin,
- Vwap, VwapStdDevBands, VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, WaveTrend,
- WaveTrendOutput, Wedge, WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput,
- WilliamsR, WinRate, Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit,
- ZScore, ZeroLagMacd, ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
+ TowerTopBottom, TpoProfile, TpoProfileOutput, TradeImbalance, TradeSignAutocorrelation,
+ TradeVolumeIndex, TrendLabel, TrendStrengthIndex, Trendflex, TreynorRatio, Triangle, Trima,
+ Trin, TripleTopBottom, Tristar, Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze,
+ TtmSqueezeOutput, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice,
+ UlcerIndex, UltimateOscillator, UniqueThreeRiver, UniversalOscillator, UpDownVolumeRatio,
+ UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance,
+ VarianceRatio, VerticalHorizontalFilter, Vidya, VolatilityCone, VolatilityConeOutput,
+ VolatilityOfVolatility, VolatilityRatio, VoltyStop, VolumeByTimeProfile,
+ VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend, VolumeProfile,
+ VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput, VolumeWeightedSr,
+ VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
+ VwapStdDevBandsOutput, Vwma, Vzo, Wad, WavePm, WaveTrend, WaveTrendOutput, Wedge,
+ WeightedClose, WickRatio, WilliamsFractals, WilliamsFractalsOutput, WilliamsR, WinRate, Wma,
+ WoodiePivots, WoodiePivotsOutput, YangZhangVolatility, YoyoExit, ZScore, ZeroLagMacd,
+ ZeroLagMacdOutput, ZigZag, ZigZagOutput, Zlema, FAMILIES, T3,
};
// `FootprintLevel` is a row element of `FootprintOutput`, re-exported on its own
// line so the indicator-count tooling (which scans the braced block above and
diff --git a/docs/README.md b/docs/README.md
index 963f6331..a659c5e9 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 **485 indicators** across
+- A per-indicator deep dive for every one of the **488 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_pair.rs b/fuzz/fuzz_targets/indicator_update_pair.rs
index 2e259e55..db467972 100644
--- a/fuzz/fuzz_targets/indicator_update_pair.rs
+++ b/fuzz/fuzz_targets/indicator_update_pair.rs
@@ -8,7 +8,7 @@
//! panic.
use libfuzzer_sys::fuzz_target;
-use wickra_core::{Alpha, BatchExt, BetaNeutralSpread, Cointegration, DistanceSsd, GrangerCausality, Indicator, InformationRatio, KalmanHedgeRatio, KendallTau, LeadLagCrossCorrelation, OuHalfLife, PairSpreadZScore, PairwiseBeta, RelativeStrengthAB, RollingCorrelation, RollingCovariance, SpreadAr1Coefficient, SpreadBollingerBands, SpreadHurst, TreynorRatio, VarianceRatio};
+use wickra_core::{Alpha, BatchExt, BetaNeutralSpread, Cointegration, DistanceSsd, GrangerCausality, HasbrouckInformationShare, Indicator, InformationRatio, KalmanHedgeRatio, KendallTau, LeadLagCrossCorrelation, OuHalfLife, PairSpreadZScore, PairwiseBeta, RelativeStrengthAB, RollingCorrelation, RollingCovariance, SpreadAr1Coefficient, SpreadBollingerBands, SpreadHurst, TreynorRatio, VarianceRatio};
#[inline(never)]
fn drive(make: impl Fn() -> I, data: &[(f64, f64)])
@@ -48,6 +48,7 @@ fuzz_target!(|data: &[u8]| {
drive(|| GrangerCausality::new(60, 1).unwrap(), &pairs);
drive(|| SpreadAr1Coefficient::new(40).unwrap(), &pairs);
drive(|| KendallTau::new(20).unwrap(), &pairs);
+ drive(|| HasbrouckInformationShare::new(2).unwrap(), &pairs);
// Struct-output pair indicator: drive update + batch directly (the generic
// `drive` above only covers `Output = f64`).
diff --git a/fuzz/fuzz_targets/indicator_update_trade.rs b/fuzz/fuzz_targets/indicator_update_trade.rs
index 7428c02d..348c021f 100644
--- a/fuzz/fuzz_targets/indicator_update_trade.rs
+++ b/fuzz/fuzz_targets/indicator_update_trade.rs
@@ -10,7 +10,7 @@
//! would reject — the indicators must never panic, streaming or batched.
use libfuzzer_sys::fuzz_target;
-use wickra_core::{AmihudIlliquidity, BatchExt, CumulativeVolumeDelta, Footprint, Indicator, RollMeasure, Side, SignedVolume, Trade, TradeImbalance, Vpin};
+use wickra_core::{AmihudIlliquidity, BatchExt, CumulativeVolumeDelta, Footprint, Indicator, Pin, RollMeasure, Side, SignedVolume, Trade, TradeImbalance, TradeSignAutocorrelation, Vpin};
#[inline(never)]
fn drive(make: impl Fn() -> I, trades: &[Trade])
@@ -43,6 +43,8 @@ fuzz_target!(|data: &[u8]| {
drive(|| Vpin::new(8.0, 5).unwrap(), &trades);
drive(|| AmihudIlliquidity::new(20).unwrap(), &trades);
drive(|| RollMeasure::new(20).unwrap(), &trades);
+ drive(|| TradeSignAutocorrelation::new(20).unwrap(), &trades);
+ drive(|| Pin::new(20).unwrap(), &trades);
// Footprint emits a variable-length `FootprintOutput` rather than an `f64`,
// so it is driven directly rather than through the scalar-output helper.