feat: microstructure price-impact & depth indicators (part 3 of 4) (#122)
* feat: effective spread microstructure indicator (part 3 of 4) * feat: realized spread microstructure indicator (part 3 of 4) * feat: kyle's lambda microstructure indicator (part 3 of 4) * feat: depth slope microstructure indicator (part 3 of 4)
This commit is contained in:
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Microstructure family — price impact & depth (part 3).** Indicators over a
|
||||||
|
trade paired with the prevailing mid (`TradeQuote`) and over the order-book
|
||||||
|
depth profile, exposed in Rust, Python, Node and WASM:
|
||||||
|
- **Effective Spread** — `2 · D · (tradePrice − mid) / mid · 10_000` bps, the
|
||||||
|
realised round-trip cost of a single trade against the mid.
|
||||||
|
- **Realized Spread** — `2 · D · (tradePrice − mid_{t+horizon}) / mid_t ·
|
||||||
|
10_000` bps, the share of the effective spread a liquidity provider keeps
|
||||||
|
once the mid has moved over a configurable horizon.
|
||||||
|
- **Kyle's Lambda** — the rolling OLS slope of mid changes on signed volume
|
||||||
|
(`cov(Δmid, q) / var(q)`), the canonical price-impact / market-depth proxy.
|
||||||
|
- **Depth Slope** — the mean per-side OLS slope of cumulative resting size
|
||||||
|
against distance from the mid, measuring how fast the book thickens away
|
||||||
|
from the touch.
|
||||||
|
|
||||||
## [0.4.2] - 2026-06-01
|
## [0.4.2] - 2026-06-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=227" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=231" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||||
@@ -47,7 +47,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
|||||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||||
every one of the 227 indicators; start at the
|
every one of the 231 indicators; start at the
|
||||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||||
@@ -135,7 +135,7 @@ python -m benchmarks.compare_libraries
|
|||||||
|
|
||||||
## Indicators
|
## Indicators
|
||||||
|
|
||||||
227 streaming-first indicators across seventeen families. Every one passes the
|
231 streaming-first indicators across seventeen families. Every one passes the
|
||||||
`batch == streaming` equivalence test, reference-value tests, and reset
|
`batch == streaming` equivalence test, reference-value tests, and reset
|
||||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||||
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
||||||
@@ -156,7 +156,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
|
|||||||
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level |
|
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level |
|
||||||
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi |
|
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi |
|
||||||
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down |
|
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down |
|
||||||
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Signed Volume, Cumulative Volume Delta, Trade Imbalance |
|
| 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 |
|
||||||
| Market Profile | Value Area (POC / VAH / VAL), Initial Balance, Opening Range |
|
| Market Profile | Value Area (POC / VAH / VAL), Initial Balance, Opening Range |
|
||||||
| 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) |
|
| 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/
|
wickra/
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── wickra-core/ core engine + all 227 indicators
|
│ ├── wickra-core/ core engine + all 231 indicators
|
||||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||||
├── bindings/
|
├── bindings/
|
||||||
|
|||||||
@@ -926,6 +926,10 @@ test('order-book indicators reference values', () => {
|
|||||||
assert.equal(new wickra.Microprice().update([100], [1], [101], [3]), 100.25);
|
assert.equal(new wickra.Microprice().update([100], [1], [101], [3]), 100.25);
|
||||||
// Quoted spread: 1 / 100.5 * 10000 ≈ 99.5025 bps.
|
// Quoted spread: 1 / 100.5 * 10000 ≈ 99.5025 bps.
|
||||||
assert.ok(Math.abs(new wickra.QuotedSpread().update([100], [1], [101], [1]) - 99.50248756) < 1e-6);
|
assert.ok(Math.abs(new wickra.QuotedSpread().update([100], [1], [101], [1]) - 99.50248756) < 1e-6);
|
||||||
|
// Depth slope: each side distances 1,2 -> cumulative 1,3 -> OLS slope 2.
|
||||||
|
assert.ok(Math.abs(new wickra.DepthSlope().update([99, 98], [1, 2], [101, 102], [1, 2]) - 2.0) < 1e-9);
|
||||||
|
// Single level per side -> no slope -> 0.
|
||||||
|
assert.equal(new wickra.DepthSlope().update([100], [1], [101], [1]), 0.0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('order-book streaming update matches batch', () => {
|
test('order-book streaming update matches batch', () => {
|
||||||
@@ -981,3 +985,81 @@ test('trade-flow rejects bad input', () => {
|
|||||||
assert.throws(() => new wickra.TradeImbalance(0));
|
assert.throws(() => new wickra.TradeImbalance(0));
|
||||||
assert.throws(() => new wickra.SignedVolume().update(100, -1, true));
|
assert.throws(() => new wickra.SignedVolume().update(100, -1, true));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('price-impact indicators reference values', () => {
|
||||||
|
// Buy at 100.05 vs mid 100.0: 2 * (100.05 - 100) / 100 * 10000 = 10 bps.
|
||||||
|
assert.ok(Math.abs(new wickra.EffectiveSpread().update(100.05, 1, true, 100.0) - 10.0) < 1e-9);
|
||||||
|
// Sell at 99.95 vs mid 100.0: 2 * -1 * (99.95 - 100) / 100 * 10000 = 10 bps.
|
||||||
|
assert.ok(Math.abs(new wickra.EffectiveSpread().update(99.95, 1, false, 100.0) - 10.0) < 1e-9);
|
||||||
|
// A buy filled below the mid is price improvement -> negative.
|
||||||
|
assert.ok(new wickra.EffectiveSpread().update(99.95, 1, true, 100.0) < 0.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('price-impact streaming update matches batch', () => {
|
||||||
|
const n = 30;
|
||||||
|
const mid = Array.from({ length: n }, (_, i) => 100 + 0.25 * Math.sin(i * 0.5));
|
||||||
|
const isBuy = Array.from({ length: n }, (_, i) => i % 3 !== 0);
|
||||||
|
const price = Array.from({ length: n }, (_, i) => mid[i] + (isBuy[i] ? 0.03 : -0.03));
|
||||||
|
const size = Array.from({ length: n }, (_, i) => 1 + (i % 4));
|
||||||
|
const batch = new wickra.EffectiveSpread().batch(price, size, isBuy, mid);
|
||||||
|
const streamer = new wickra.EffectiveSpread();
|
||||||
|
assert.equal(batch.length, n);
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const s = streamer.update(price[i], size[i], isBuy[i], mid[i]);
|
||||||
|
assert.ok(Math.abs(s - batch[i]) < 1e-9, `mismatch at ${i}: ${s} vs ${batch[i]}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('realized spread resolves against the future mid', () => {
|
||||||
|
const rs = new wickra.RealizedSpread(1);
|
||||||
|
assert.equal(rs.update(100.10, 1, true, 100.0), null); // buffered
|
||||||
|
// 2 * (+1) * (100.10 - 100.20) / 100.0 * 10000 = -20 bps.
|
||||||
|
assert.ok(Math.abs(rs.update(99.90, 1, false, 100.20) - -20.0) < 1e-9);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('realized spread streaming update matches batch', () => {
|
||||||
|
const n = 30;
|
||||||
|
const mid = Array.from({ length: n }, (_, i) => 100 + 0.25 * Math.sin(i * 0.5));
|
||||||
|
const isBuy = Array.from({ length: n }, (_, i) => i % 3 !== 0);
|
||||||
|
const price = Array.from({ length: n }, (_, i) => mid[i] + (isBuy[i] ? 0.03 : -0.03));
|
||||||
|
const size = Array.from({ length: n }, (_, i) => 1 + (i % 4));
|
||||||
|
const batch = new wickra.RealizedSpread(4).batch(price, size, isBuy, mid);
|
||||||
|
const streamer = new wickra.RealizedSpread(4);
|
||||||
|
assert.equal(batch.length, n);
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const s = streamer.update(price[i], size[i], isBuy[i], mid[i]);
|
||||||
|
const got = s === null ? NaN : s;
|
||||||
|
assert.ok(
|
||||||
|
(Number.isNaN(got) && Number.isNaN(batch[i])) || Math.abs(got - batch[i]) < 1e-9,
|
||||||
|
`mismatch at ${i}: ${got} vs ${batch[i]}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("kyle's lambda recovers a constant price-impact slope", () => {
|
||||||
|
// Each trade moves the mid by exactly 0.5 per unit of signed volume.
|
||||||
|
const impact = 0.5;
|
||||||
|
let mid = 100;
|
||||||
|
const price = [];
|
||||||
|
const size = [];
|
||||||
|
const isBuy = [];
|
||||||
|
const mids = [];
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const buy = i % 2 === 0;
|
||||||
|
const sz = 1 + (i % 3);
|
||||||
|
const signed = buy ? sz : -sz;
|
||||||
|
mid += impact * signed;
|
||||||
|
price.push(mid);
|
||||||
|
size.push(sz);
|
||||||
|
isBuy.push(buy);
|
||||||
|
mids.push(mid);
|
||||||
|
}
|
||||||
|
const out = new wickra.KylesLambda(6).batch(price, size, isBuy, mids);
|
||||||
|
assert.ok(Math.abs(out[out.length - 1] - 0.5) < 1e-9);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('price-impact rejects bad input', () => {
|
||||||
|
assert.throws(() => new wickra.EffectiveSpread().update(100, 1, true, 0));
|
||||||
|
assert.throws(() => new wickra.RealizedSpread(0));
|
||||||
|
assert.throws(() => new wickra.KylesLambda(1));
|
||||||
|
});
|
||||||
|
|||||||
Vendored
+36
@@ -2232,6 +2232,15 @@ export declare class QuotedSpread {
|
|||||||
isReady(): boolean
|
isReady(): boolean
|
||||||
warmupPeriod(): number
|
warmupPeriod(): number
|
||||||
}
|
}
|
||||||
|
export type DepthSlopeNode = DepthSlope
|
||||||
|
export declare class DepthSlope {
|
||||||
|
constructor()
|
||||||
|
update(bidPx: Array<number>, bidSz: Array<number>, askPx: Array<number>, askSz: Array<number>): number | null
|
||||||
|
batch(snapshots: Array<ObSnapshot>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
export type OrderBookImbalanceTopNNode = OrderBookImbalanceTopN
|
export type OrderBookImbalanceTopNNode = OrderBookImbalanceTopN
|
||||||
export declare class OrderBookImbalanceTopN {
|
export declare class OrderBookImbalanceTopN {
|
||||||
constructor(levels: number)
|
constructor(levels: number)
|
||||||
@@ -2268,6 +2277,33 @@ export declare class TradeImbalance {
|
|||||||
isReady(): boolean
|
isReady(): boolean
|
||||||
warmupPeriod(): number
|
warmupPeriod(): number
|
||||||
}
|
}
|
||||||
|
export type EffectiveSpreadNode = EffectiveSpread
|
||||||
|
export declare class EffectiveSpread {
|
||||||
|
constructor()
|
||||||
|
update(price: number, size: number, isBuy: boolean, mid: number): number | null
|
||||||
|
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>, mid: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type RealizedSpreadNode = RealizedSpread
|
||||||
|
export declare class RealizedSpread {
|
||||||
|
constructor(horizon: number)
|
||||||
|
update(price: number, size: number, isBuy: boolean, mid: number): number | null
|
||||||
|
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>, mid: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
|
export type KylesLambdaNode = KylesLambda
|
||||||
|
export declare class KylesLambda {
|
||||||
|
constructor(window: number)
|
||||||
|
update(price: number, size: number, isBuy: boolean, mid: number): number | null
|
||||||
|
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>, mid: Array<number>): Array<number>
|
||||||
|
reset(): void
|
||||||
|
isReady(): boolean
|
||||||
|
warmupPeriod(): number
|
||||||
|
}
|
||||||
export type SharpeRatioNode = SharpeRatio
|
export type SharpeRatioNode = SharpeRatio
|
||||||
export declare class SharpeRatio {
|
export declare class SharpeRatio {
|
||||||
constructor(period: number, riskFree: number)
|
constructor(period: number, riskFree: number)
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
|||||||
throw new Error(`Failed to load native binding`)
|
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, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, 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, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, 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, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = 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, Autocorrelation, HurstExponent, PearsonCorrelation, Beta, PairwiseBeta, SpearmanCorrelation, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration, RelativeStrengthAB, MACD, BollingerBands, ATR, 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, TSI, PMO, TII, ADL, VolumePriceTrend, ChaikinMoneyFlow, ChaikinOscillator, ForceIndex, NVI, PVI, VolumeOscillator, KVO, WilliamsAD, AnchoredVWAP, DemandIndex, TSV, VZO, MarketFacilitationIndex, EaseOfMovement, SuperTrend, ChandelierExit, ChandeKrollStop, AtrTrailingStop, HiLoActivator, VoltyStop, YoyoExit, DonchianStop, PercentageTrailingStop, StepTrailingStop, RenkoTrailingStop, 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, DoubleBollinger, TtmSqueeze, FractalChaosBands, VwapStdDevBands, ClassicPivots, FibonacciPivots, Camarilla, WoodiePivots, DemarkPivots, WilliamsFractals, ZigZag, TDSetup, TDSequential, TDDeMarker, TDREI, TDPressure, TDCombo, TDCountdown, TDLines, TDRangeProjection, TDDifferential, TDOpen, TDRiskLevel, InverseFisherTransform, DecyclerOscillator, RoofingFilter, EmpiricalModeDecomposition, HilbertDominantCycle, AdaptiveCycle, SineWave, MAMA, FAMA, Ichimoku, HeikinAshi, ValueArea, InitialBalance, OpeningRange, Doji, Hammer, InvertedHammer, HangingMan, ShootingStar, Engulfing, Harami, MorningEveningStar, ThreeSoldiersOrCrows, PiercingDarkCloud, Marubozu, Tweezer, SpinningTop, ThreeInside, ThreeOutside, OrderBookImbalanceTop1, OrderBookImbalanceFull, Microprice, QuotedSpread, DepthSlope, OrderBookImbalanceTopN, SignedVolume, CumulativeVolumeDelta, TradeImbalance, EffectiveSpread, RealizedSpread, KylesLambda, SharpeRatio, SortinoRatio, CalmarRatio, OmegaRatio, MaxDrawdown, AverageDrawdown, DrawdownDuration, PainIndex, ValueAtRisk, ConditionalValueAtRisk, ProfitFactor, GainLossRatio, RecoveryFactor, KellyCriterion, TreynorRatio, InformationRatio, Alpha } = nativeBinding
|
||||||
|
|
||||||
module.exports.version = version
|
module.exports.version = version
|
||||||
module.exports.SMA = SMA
|
module.exports.SMA = SMA
|
||||||
@@ -519,10 +519,14 @@ module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1
|
|||||||
module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
|
module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
|
||||||
module.exports.Microprice = Microprice
|
module.exports.Microprice = Microprice
|
||||||
module.exports.QuotedSpread = QuotedSpread
|
module.exports.QuotedSpread = QuotedSpread
|
||||||
|
module.exports.DepthSlope = DepthSlope
|
||||||
module.exports.OrderBookImbalanceTopN = OrderBookImbalanceTopN
|
module.exports.OrderBookImbalanceTopN = OrderBookImbalanceTopN
|
||||||
module.exports.SignedVolume = SignedVolume
|
module.exports.SignedVolume = SignedVolume
|
||||||
module.exports.CumulativeVolumeDelta = CumulativeVolumeDelta
|
module.exports.CumulativeVolumeDelta = CumulativeVolumeDelta
|
||||||
module.exports.TradeImbalance = TradeImbalance
|
module.exports.TradeImbalance = TradeImbalance
|
||||||
|
module.exports.EffectiveSpread = EffectiveSpread
|
||||||
|
module.exports.RealizedSpread = RealizedSpread
|
||||||
|
module.exports.KylesLambda = KylesLambda
|
||||||
module.exports.SharpeRatio = SharpeRatio
|
module.exports.SharpeRatio = SharpeRatio
|
||||||
module.exports.SortinoRatio = SortinoRatio
|
module.exports.SortinoRatio = SortinoRatio
|
||||||
module.exports.CalmarRatio = CalmarRatio
|
module.exports.CalmarRatio = CalmarRatio
|
||||||
|
|||||||
@@ -8865,6 +8865,7 @@ node_ob_indicator!(
|
|||||||
);
|
);
|
||||||
node_ob_indicator!(MicropriceNode, wc::Microprice, "Microprice");
|
node_ob_indicator!(MicropriceNode, wc::Microprice, "Microprice");
|
||||||
node_ob_indicator!(QuotedSpreadNode, wc::QuotedSpread, "QuotedSpread");
|
node_ob_indicator!(QuotedSpreadNode, wc::QuotedSpread, "QuotedSpread");
|
||||||
|
node_ob_indicator!(DepthSlopeNode, wc::DepthSlope, "DepthSlope");
|
||||||
|
|
||||||
// Top-N imbalance carries a `levels` parameter, so it is hand-written.
|
// Top-N imbalance carries a `levels` parameter, so it is hand-written.
|
||||||
#[napi(js_name = "OrderBookImbalanceTopN")]
|
#[napi(js_name = "OrderBookImbalanceTopN")]
|
||||||
@@ -9052,6 +9053,217 @@ impl TradeImbalanceNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================== Microstructure: Price Impact ==============================
|
||||||
|
//
|
||||||
|
// Price-impact indicators consume a trade paired with the mid prevailing at
|
||||||
|
// execution. Streaming `update(price, size, isBuy, mid)` takes one such
|
||||||
|
// trade-quote (`isBuy=true` for a buyer-initiated trade); `batch` takes four
|
||||||
|
// equal-length arrays.
|
||||||
|
|
||||||
|
fn build_trade_quote(
|
||||||
|
price: f64,
|
||||||
|
size: f64,
|
||||||
|
is_buy: bool,
|
||||||
|
mid: f64,
|
||||||
|
) -> napi::Result<wc::TradeQuote> {
|
||||||
|
let trade = build_trade(price, size, is_buy)?;
|
||||||
|
wc::TradeQuote::new(trade, mid).map_err(map_err)
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! node_trade_quote_indicator {
|
||||||
|
($node:ident, $inner:ty, $js:literal) => {
|
||||||
|
#[napi(js_name = $js)]
|
||||||
|
pub struct $node {
|
||||||
|
inner: $inner,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for $node {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl $node {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: <$inner>::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
price: f64,
|
||||||
|
size: f64,
|
||||||
|
is_buy: bool,
|
||||||
|
mid: f64,
|
||||||
|
) -> napi::Result<Option<f64>> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_trade_quote(price, size, is_buy, mid)?))
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn batch(
|
||||||
|
&mut self,
|
||||||
|
price: Vec<f64>,
|
||||||
|
size: Vec<f64>,
|
||||||
|
is_buy: Vec<bool>,
|
||||||
|
mid: Vec<f64>,
|
||||||
|
) -> napi::Result<Vec<f64>> {
|
||||||
|
if price.len() != size.len()
|
||||||
|
|| size.len() != is_buy.len()
|
||||||
|
|| is_buy.len() != mid.len()
|
||||||
|
{
|
||||||
|
return Err(NapiError::from_reason(
|
||||||
|
"price, size, is_buy, mid must be equal length".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(price.len());
|
||||||
|
for i in 0..price.len() {
|
||||||
|
let quote = build_trade_quote(price[i], size[i], is_buy[i], mid[i])?;
|
||||||
|
out.push(self.inner.update(quote).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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
node_trade_quote_indicator!(EffectiveSpreadNode, wc::EffectiveSpread, "EffectiveSpread");
|
||||||
|
|
||||||
|
// Realized spread carries a `horizon` parameter, so it is hand-written.
|
||||||
|
#[napi(js_name = "RealizedSpread")]
|
||||||
|
pub struct RealizedSpreadNode {
|
||||||
|
inner: wc::RealizedSpread,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl RealizedSpreadNode {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new(horizon: u32) -> napi::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::RealizedSpread::new(horizon as usize).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
price: f64,
|
||||||
|
size: f64,
|
||||||
|
is_buy: bool,
|
||||||
|
mid: f64,
|
||||||
|
) -> napi::Result<Option<f64>> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_trade_quote(price, size, is_buy, mid)?))
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn batch(
|
||||||
|
&mut self,
|
||||||
|
price: Vec<f64>,
|
||||||
|
size: Vec<f64>,
|
||||||
|
is_buy: Vec<bool>,
|
||||||
|
mid: Vec<f64>,
|
||||||
|
) -> napi::Result<Vec<f64>> {
|
||||||
|
if price.len() != size.len() || size.len() != is_buy.len() || is_buy.len() != mid.len() {
|
||||||
|
return Err(NapiError::from_reason(
|
||||||
|
"price, size, is_buy, mid must be equal length".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(price.len());
|
||||||
|
for i in 0..price.len() {
|
||||||
|
let quote = build_trade_quote(price[i], size[i], is_buy[i], mid[i])?;
|
||||||
|
out.push(self.inner.update(quote).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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kyle's lambda carries a `window` parameter, so it is hand-written.
|
||||||
|
#[napi(js_name = "KylesLambda")]
|
||||||
|
pub struct KylesLambdaNode {
|
||||||
|
inner: wc::KylesLambda,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl KylesLambdaNode {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new(window: u32) -> napi::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::KylesLambda::new(window as usize).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
price: f64,
|
||||||
|
size: f64,
|
||||||
|
is_buy: bool,
|
||||||
|
mid: f64,
|
||||||
|
) -> napi::Result<Option<f64>> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_trade_quote(price, size, is_buy, mid)?))
|
||||||
|
}
|
||||||
|
#[napi]
|
||||||
|
pub fn batch(
|
||||||
|
&mut self,
|
||||||
|
price: Vec<f64>,
|
||||||
|
size: Vec<f64>,
|
||||||
|
is_buy: Vec<bool>,
|
||||||
|
mid: Vec<f64>,
|
||||||
|
) -> napi::Result<Vec<f64>> {
|
||||||
|
if price.len() != size.len() || size.len() != is_buy.len() || is_buy.len() != mid.len() {
|
||||||
|
return Err(NapiError::from_reason(
|
||||||
|
"price, size, is_buy, mid must be equal length".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(price.len());
|
||||||
|
for i in 0..price.len() {
|
||||||
|
let quote = build_trade_quote(price[i], size[i], is_buy[i], mid[i])?;
|
||||||
|
out.push(self.inner.update(quote).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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================== Family 15: Risk / Performance ==============================
|
// ============================== Family 15: Risk / Performance ==============================
|
||||||
|
|
||||||
// Risk metrics with fallible `new` (most need `period >= 2`), so each wrapper
|
// Risk metrics with fallible `new` (most need `period >= 2`), so each wrapper
|
||||||
|
|||||||
@@ -246,10 +246,15 @@ from ._wickra import (
|
|||||||
OrderBookImbalanceFull,
|
OrderBookImbalanceFull,
|
||||||
Microprice,
|
Microprice,
|
||||||
QuotedSpread,
|
QuotedSpread,
|
||||||
|
DepthSlope,
|
||||||
# Microstructure: trade flow
|
# Microstructure: trade flow
|
||||||
SignedVolume,
|
SignedVolume,
|
||||||
CumulativeVolumeDelta,
|
CumulativeVolumeDelta,
|
||||||
TradeImbalance,
|
TradeImbalance,
|
||||||
|
# Microstructure: price impact
|
||||||
|
EffectiveSpread,
|
||||||
|
RealizedSpread,
|
||||||
|
KylesLambda,
|
||||||
# Risk / Performance
|
# Risk / Performance
|
||||||
SharpeRatio,
|
SharpeRatio,
|
||||||
SortinoRatio,
|
SortinoRatio,
|
||||||
@@ -493,10 +498,15 @@ __all__ = [
|
|||||||
"OrderBookImbalanceFull",
|
"OrderBookImbalanceFull",
|
||||||
"Microprice",
|
"Microprice",
|
||||||
"QuotedSpread",
|
"QuotedSpread",
|
||||||
|
"DepthSlope",
|
||||||
# Microstructure: trade flow
|
# Microstructure: trade flow
|
||||||
"SignedVolume",
|
"SignedVolume",
|
||||||
"CumulativeVolumeDelta",
|
"CumulativeVolumeDelta",
|
||||||
"TradeImbalance",
|
"TradeImbalance",
|
||||||
|
# Microstructure: price impact
|
||||||
|
"EffectiveSpread",
|
||||||
|
"RealizedSpread",
|
||||||
|
"KylesLambda",
|
||||||
# Risk / Performance
|
# Risk / Performance
|
||||||
"SharpeRatio",
|
"SharpeRatio",
|
||||||
"SortinoRatio",
|
"SortinoRatio",
|
||||||
|
|||||||
@@ -11714,6 +11714,7 @@ py_ob_indicator!(
|
|||||||
);
|
);
|
||||||
py_ob_indicator!(PyMicroprice, wc::Microprice, "Microprice");
|
py_ob_indicator!(PyMicroprice, wc::Microprice, "Microprice");
|
||||||
py_ob_indicator!(PyQuotedSpread, wc::QuotedSpread, "QuotedSpread");
|
py_ob_indicator!(PyQuotedSpread, wc::QuotedSpread, "QuotedSpread");
|
||||||
|
py_ob_indicator!(PyDepthSlope, wc::DepthSlope, "DepthSlope");
|
||||||
|
|
||||||
// Top-N imbalance carries a `levels` parameter, so it is hand-written.
|
// Top-N imbalance carries a `levels` parameter, so it is hand-written.
|
||||||
#[pyclass(
|
#[pyclass(
|
||||||
@@ -11902,6 +11903,198 @@ impl PyTradeImbalance {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================== Microstructure: Price Impact ==============================
|
||||||
|
//
|
||||||
|
// Price-impact indicators consume a trade paired with the mid prevailing at
|
||||||
|
// execution. Streaming `update(price, size, is_buy, mid)` takes one such
|
||||||
|
// trade-quote (`is_buy=True` for a buyer-initiated trade); `batch` takes four
|
||||||
|
// equal-length arrays.
|
||||||
|
|
||||||
|
fn build_trade_quote(price: f64, size: f64, is_buy: bool, mid: f64) -> PyResult<wc::TradeQuote> {
|
||||||
|
let trade = build_trade(price, size, is_buy)?;
|
||||||
|
wc::TradeQuote::new(trade, mid).map_err(map_err)
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! py_trade_quote_indicator {
|
||||||
|
($name:ident, $inner:ty, $repr:expr) => {
|
||||||
|
#[pyclass(name = $repr, module = "wickra._wickra", skip_from_py_object)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct $name {
|
||||||
|
inner: $inner,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl $name {
|
||||||
|
#[new]
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: <$inner>::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
price: f64,
|
||||||
|
size: f64,
|
||||||
|
is_buy: bool,
|
||||||
|
mid: f64,
|
||||||
|
) -> PyResult<Option<f64>> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_trade_quote(price, size, is_buy, mid)?))
|
||||||
|
}
|
||||||
|
fn batch<'py>(
|
||||||
|
&mut self,
|
||||||
|
py: Python<'py>,
|
||||||
|
price: Vec<f64>,
|
||||||
|
size: Vec<f64>,
|
||||||
|
is_buy: Vec<bool>,
|
||||||
|
mid: Vec<f64>,
|
||||||
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
|
if price.len() != size.len()
|
||||||
|
|| size.len() != is_buy.len()
|
||||||
|
|| is_buy.len() != mid.len()
|
||||||
|
{
|
||||||
|
return Err(PyValueError::new_err(
|
||||||
|
"price, size, is_buy, mid must be equal length",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(price.len());
|
||||||
|
for i in 0..price.len() {
|
||||||
|
let quote = build_trade_quote(price[i], size[i], is_buy[i], mid[i])?;
|
||||||
|
out.push(self.inner.update(quote).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!("{}()", $repr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
py_trade_quote_indicator!(PyEffectiveSpread, wc::EffectiveSpread, "EffectiveSpread");
|
||||||
|
|
||||||
|
// Realized spread carries a `horizon` parameter, so it is hand-written.
|
||||||
|
#[pyclass(
|
||||||
|
name = "RealizedSpread",
|
||||||
|
module = "wickra._wickra",
|
||||||
|
skip_from_py_object
|
||||||
|
)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PyRealizedSpread {
|
||||||
|
inner: wc::RealizedSpread,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PyRealizedSpread {
|
||||||
|
#[new]
|
||||||
|
fn new(horizon: usize) -> PyResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::RealizedSpread::new(horizon).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn update(&mut self, price: f64, size: f64, is_buy: bool, mid: f64) -> PyResult<Option<f64>> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_trade_quote(price, size, is_buy, mid)?))
|
||||||
|
}
|
||||||
|
fn batch<'py>(
|
||||||
|
&mut self,
|
||||||
|
py: Python<'py>,
|
||||||
|
price: Vec<f64>,
|
||||||
|
size: Vec<f64>,
|
||||||
|
is_buy: Vec<bool>,
|
||||||
|
mid: Vec<f64>,
|
||||||
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
|
if price.len() != size.len() || size.len() != is_buy.len() || is_buy.len() != mid.len() {
|
||||||
|
return Err(PyValueError::new_err(
|
||||||
|
"price, size, is_buy, mid must be equal length",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(price.len());
|
||||||
|
for i in 0..price.len() {
|
||||||
|
let quote = build_trade_quote(price[i], size[i], is_buy[i], mid[i])?;
|
||||||
|
out.push(self.inner.update(quote).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!("RealizedSpread(horizon={})", self.inner.horizon())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kyle's lambda carries a `window` parameter, so it is hand-written.
|
||||||
|
#[pyclass(name = "KylesLambda", module = "wickra._wickra", skip_from_py_object)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PyKylesLambda {
|
||||||
|
inner: wc::KylesLambda,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PyKylesLambda {
|
||||||
|
#[new]
|
||||||
|
fn new(window: usize) -> PyResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::KylesLambda::new(window).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn update(&mut self, price: f64, size: f64, is_buy: bool, mid: f64) -> PyResult<Option<f64>> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_trade_quote(price, size, is_buy, mid)?))
|
||||||
|
}
|
||||||
|
fn batch<'py>(
|
||||||
|
&mut self,
|
||||||
|
py: Python<'py>,
|
||||||
|
price: Vec<f64>,
|
||||||
|
size: Vec<f64>,
|
||||||
|
is_buy: Vec<bool>,
|
||||||
|
mid: Vec<f64>,
|
||||||
|
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||||
|
if price.len() != size.len() || size.len() != is_buy.len() || is_buy.len() != mid.len() {
|
||||||
|
return Err(PyValueError::new_err(
|
||||||
|
"price, size, is_buy, mid must be equal length",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(price.len());
|
||||||
|
for i in 0..price.len() {
|
||||||
|
let quote = build_trade_quote(price[i], size[i], is_buy[i], mid[i])?;
|
||||||
|
out.push(self.inner.update(quote).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!("KylesLambda(window={})", self.inner.window())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================== Family 15: Risk / Performance ==============================
|
// ============================== Family 15: Risk / Performance ==============================
|
||||||
|
|
||||||
#[pyclass(name = "SharpeRatio", module = "wickra._wickra", skip_from_py_object)]
|
#[pyclass(name = "SharpeRatio", module = "wickra._wickra", skip_from_py_object)]
|
||||||
@@ -13013,10 +13206,15 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||||||
m.add_class::<PyOrderBookImbalanceFull>()?;
|
m.add_class::<PyOrderBookImbalanceFull>()?;
|
||||||
m.add_class::<PyMicroprice>()?;
|
m.add_class::<PyMicroprice>()?;
|
||||||
m.add_class::<PyQuotedSpread>()?;
|
m.add_class::<PyQuotedSpread>()?;
|
||||||
|
m.add_class::<PyDepthSlope>()?;
|
||||||
// Microstructure: trade flow.
|
// Microstructure: trade flow.
|
||||||
m.add_class::<PySignedVolume>()?;
|
m.add_class::<PySignedVolume>()?;
|
||||||
m.add_class::<PyCumulativeVolumeDelta>()?;
|
m.add_class::<PyCumulativeVolumeDelta>()?;
|
||||||
m.add_class::<PyTradeImbalance>()?;
|
m.add_class::<PyTradeImbalance>()?;
|
||||||
|
// Microstructure: price impact.
|
||||||
|
m.add_class::<PyEffectiveSpread>()?;
|
||||||
|
m.add_class::<PyRealizedSpread>()?;
|
||||||
|
m.add_class::<PyKylesLambda>()?;
|
||||||
// Family 15: Risk / Performance metrics.
|
// Family 15: Risk / Performance metrics.
|
||||||
m.add_class::<PySharpeRatio>()?;
|
m.add_class::<PySharpeRatio>()?;
|
||||||
m.add_class::<PySortinoRatio>()?;
|
m.add_class::<PySortinoRatio>()?;
|
||||||
|
|||||||
@@ -211,3 +211,23 @@ def test_trade_non_positive_price_raises():
|
|||||||
def test_trade_batch_unequal_lengths_raise():
|
def test_trade_batch_unequal_lengths_raise():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ta.SignedVolume().batch([100.0, 100.0], [1.0], [True, False])
|
ta.SignedVolume().batch([100.0, 100.0], [1.0], [True, False])
|
||||||
|
|
||||||
|
|
||||||
|
def test_effective_spread_non_positive_mid_raises():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ta.EffectiveSpread().update(100.0, 1.0, True, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_effective_spread_batch_unequal_lengths_raise():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ta.EffectiveSpread().batch([100.0, 100.0], [1.0, 1.0], [True, False], [100.0])
|
||||||
|
|
||||||
|
|
||||||
|
def test_realized_spread_zero_horizon_raises():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ta.RealizedSpread(0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_kyles_lambda_window_below_two_raises():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ta.KylesLambda(1)
|
||||||
|
|||||||
@@ -872,6 +872,16 @@ def test_quoted_spread_reference_value():
|
|||||||
assert qs.update([100.0], [1.0], [101.0], [1.0]) == pytest.approx(99.50248756, abs=1e-6)
|
assert qs.update([100.0], [1.0], [101.0], [1.0]) == pytest.approx(99.50248756, abs=1e-6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_depth_slope_reference_value():
|
||||||
|
# Symmetric book, each side distances 1, 2 with cumulative sizes 1, 3.
|
||||||
|
# OLS slope of (1->1, 2->3) = 2; mean of two equal sides = 2.
|
||||||
|
ds = ta.DepthSlope()
|
||||||
|
out = ds.update([99.0, 98.0], [1.0, 2.0], [101.0, 102.0], [1.0, 2.0])
|
||||||
|
assert out == pytest.approx(2.0, abs=1e-9)
|
||||||
|
# A book with a single level per side has no slope -> 0.
|
||||||
|
assert ta.DepthSlope().update([100.0], [1.0], [101.0], [1.0]) == pytest.approx(0.0)
|
||||||
|
|
||||||
|
|
||||||
def test_signed_volume_reference_values():
|
def test_signed_volume_reference_values():
|
||||||
assert ta.SignedVolume().update(100.0, 2.0, True) == pytest.approx(2.0)
|
assert ta.SignedVolume().update(100.0, 2.0, True) == pytest.approx(2.0)
|
||||||
assert ta.SignedVolume().update(100.0, 3.0, False) == pytest.approx(-3.0)
|
assert ta.SignedVolume().update(100.0, 3.0, False) == pytest.approx(-3.0)
|
||||||
@@ -889,3 +899,39 @@ def test_trade_imbalance_reference_value():
|
|||||||
assert ti.update(100.0, 3.0, True) is None # warming up
|
assert ti.update(100.0, 3.0, True) is None # warming up
|
||||||
# Window full: buyVol 3, sellVol 1 -> (3 - 1) / 4 = 0.5.
|
# Window full: buyVol 3, sellVol 1 -> (3 - 1) / 4 = 0.5.
|
||||||
assert ti.update(100.0, 1.0, False) == pytest.approx(0.5)
|
assert ti.update(100.0, 1.0, False) == pytest.approx(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_effective_spread_reference_values():
|
||||||
|
# Buy at 100.05 vs mid 100.0: 2 * (100.05 - 100) / 100 * 10000 = 10 bps.
|
||||||
|
assert ta.EffectiveSpread().update(100.05, 1.0, True, 100.0) == pytest.approx(10.0)
|
||||||
|
# Sell at 99.95 vs mid 100.0: 2 * -1 * (99.95 - 100) / 100 * 10000 = 10 bps.
|
||||||
|
assert ta.EffectiveSpread().update(99.95, 1.0, False, 100.0) == pytest.approx(10.0)
|
||||||
|
# A buy filled below the mid is price improvement -> negative.
|
||||||
|
assert ta.EffectiveSpread().update(99.95, 1.0, True, 100.0) < 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_realized_spread_reference_value():
|
||||||
|
rs = ta.RealizedSpread(1)
|
||||||
|
assert rs.update(100.10, 1.0, True, 100.0) is None # buffered
|
||||||
|
# Resolved against mid 100.20 one trade later:
|
||||||
|
# 2 * (+1) * (100.10 - 100.20) / 100.0 * 10000 = -20 bps (adverse selection).
|
||||||
|
assert rs.update(99.90, 1.0, False, 100.20) == pytest.approx(-20.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_kyles_lambda_recovers_constant_impact():
|
||||||
|
# Build a tape where each trade moves the mid by exactly 0.5 per unit of
|
||||||
|
# signed volume -> the rolling OLS slope is 0.5.
|
||||||
|
impact = 0.5
|
||||||
|
mid = 100.0
|
||||||
|
price, size, is_buy, mids = [], [], [], []
|
||||||
|
for i in range(20):
|
||||||
|
buy = i % 2 == 0
|
||||||
|
sz = 1.0 + (i % 3)
|
||||||
|
signed = sz if buy else -sz
|
||||||
|
mid += impact * signed
|
||||||
|
price.append(mid)
|
||||||
|
size.append(sz)
|
||||||
|
is_buy.append(buy)
|
||||||
|
mids.append(mid)
|
||||||
|
out = ta.KylesLambda(6).batch(price, size, is_buy, mids)
|
||||||
|
assert out[-1] == pytest.approx(0.5, abs=1e-9)
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ def test_orderbook_lifecycle():
|
|||||||
ta.OrderBookImbalanceFull(),
|
ta.OrderBookImbalanceFull(),
|
||||||
ta.Microprice(),
|
ta.Microprice(),
|
||||||
ta.QuotedSpread(),
|
ta.QuotedSpread(),
|
||||||
|
ta.DepthSlope(),
|
||||||
]:
|
]:
|
||||||
assert ind.warmup_period() == 1
|
assert ind.warmup_period() == 1
|
||||||
assert not ind.is_ready()
|
assert not ind.is_ready()
|
||||||
@@ -172,3 +173,37 @@ def test_trade_imbalance_lifecycle_and_repr():
|
|||||||
ti.reset()
|
ti.reset()
|
||||||
assert not ti.is_ready()
|
assert not ti.is_ready()
|
||||||
assert repr(ta.TradeImbalance(4)) == "TradeImbalance(window=4)"
|
assert repr(ta.TradeImbalance(4)) == "TradeImbalance(window=4)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_effective_spread_lifecycle():
|
||||||
|
es = ta.EffectiveSpread()
|
||||||
|
assert es.warmup_period() == 1
|
||||||
|
assert not es.is_ready()
|
||||||
|
es.update(100.05, 1.0, True, 100.0)
|
||||||
|
assert es.is_ready()
|
||||||
|
es.reset()
|
||||||
|
assert not es.is_ready()
|
||||||
|
|
||||||
|
|
||||||
|
def test_realized_spread_lifecycle_and_repr():
|
||||||
|
rs = ta.RealizedSpread(3)
|
||||||
|
assert rs.warmup_period() == 4
|
||||||
|
assert not rs.is_ready()
|
||||||
|
for _ in range(4):
|
||||||
|
rs.update(100.0, 1.0, True, 100.0)
|
||||||
|
assert rs.is_ready()
|
||||||
|
rs.reset()
|
||||||
|
assert not rs.is_ready()
|
||||||
|
assert repr(ta.RealizedSpread(5)) == "RealizedSpread(horizon=5)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_kyles_lambda_lifecycle_and_repr():
|
||||||
|
kl = ta.KylesLambda(3)
|
||||||
|
assert kl.warmup_period() == 4
|
||||||
|
assert not kl.is_ready()
|
||||||
|
for i in range(4):
|
||||||
|
kl.update(100.0 + i, 1.0 + (i % 2), i % 2 == 0, 100.0 + i)
|
||||||
|
assert kl.is_ready()
|
||||||
|
kl.reset()
|
||||||
|
assert not kl.is_ready()
|
||||||
|
assert repr(ta.KylesLambda(7)) == "KylesLambda(window=7)"
|
||||||
|
|||||||
@@ -1890,6 +1890,7 @@ def test_orderbook_indicators_streaming_equals_batch():
|
|||||||
ta.OrderBookImbalanceFull,
|
ta.OrderBookImbalanceFull,
|
||||||
ta.Microprice,
|
ta.Microprice,
|
||||||
ta.QuotedSpread,
|
ta.QuotedSpread,
|
||||||
|
ta.DepthSlope,
|
||||||
):
|
):
|
||||||
batch = make().batch(snaps)
|
batch = make().batch(snaps)
|
||||||
streamer = make()
|
streamer = make()
|
||||||
@@ -1918,3 +1919,23 @@ def test_tradeflow_indicators_streaming_equals_batch():
|
|||||||
)
|
)
|
||||||
assert batch.shape == (n,)
|
assert batch.shape == (n,)
|
||||||
assert _eq_nan(batch, streamed)
|
assert _eq_nan(batch, streamed)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
is_buy = [i % 2 == 0 for i in range(n)]
|
||||||
|
# Aggressive trades print across the mid in the aggressor's direction.
|
||||||
|
price = np.array(
|
||||||
|
[mid[i] + (0.02 if is_buy[i] else -0.02) for i in range(n)], dtype=np.float64
|
||||||
|
)
|
||||||
|
size = np.array([1.0 + (i % 5) for i in range(n)], dtype=np.float64)
|
||||||
|
for make in (ta.EffectiveSpread, lambda: ta.RealizedSpread(4), lambda: ta.KylesLambda(5)):
|
||||||
|
batch = make().batch(price, size, is_buy, mid)
|
||||||
|
streamer = make()
|
||||||
|
streamed = np.array(
|
||||||
|
[streamer.update(price[i], size[i], is_buy[i], mid[i]) for i in range(n)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert batch.shape == (n,)
|
||||||
|
assert _eq_nan(batch, streamed)
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ def test_orderbook_indicators_construct_and_emit():
|
|||||||
ta.OrderBookImbalanceFull(),
|
ta.OrderBookImbalanceFull(),
|
||||||
ta.Microprice(),
|
ta.Microprice(),
|
||||||
ta.QuotedSpread(),
|
ta.QuotedSpread(),
|
||||||
|
ta.DepthSlope(),
|
||||||
]
|
]
|
||||||
for ind in indicators:
|
for ind in indicators:
|
||||||
out = ind.update(*snapshot)
|
out = ind.update(*snapshot)
|
||||||
@@ -135,3 +136,21 @@ def test_tradeflow_batch_returns_one_value_per_trade():
|
|||||||
out = ta.CumulativeVolumeDelta().batch(price, size, is_buy)
|
out = ta.CumulativeVolumeDelta().batch(price, size, is_buy)
|
||||||
assert out.shape == (6,)
|
assert out.shape == (6,)
|
||||||
assert out.dtype == np.float64
|
assert out.dtype == np.float64
|
||||||
|
|
||||||
|
|
||||||
|
def test_price_impact_indicators_construct_and_emit():
|
||||||
|
# Price-impact indicators take a trade paired with the prevailing mid.
|
||||||
|
assert isinstance(ta.EffectiveSpread().update(100.05, 1.0, True, 100.0), float)
|
||||||
|
# RealizedSpread buffers until its horizon elapses.
|
||||||
|
assert ta.RealizedSpread(1).update(100.05, 1.0, True, 100.0) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_price_impact_batch_returns_one_value_per_trade():
|
||||||
|
price = np.array([100.05, 99.95, 100.10, 99.90])
|
||||||
|
size = np.array([1.0, 2.0, 1.0, 2.0])
|
||||||
|
is_buy = [True, False, True, False]
|
||||||
|
mid = np.full(4, 100.0)
|
||||||
|
for ind in (ta.EffectiveSpread(), ta.RealizedSpread(2), ta.KylesLambda(2)):
|
||||||
|
out = ind.batch(price, size, is_buy, mid)
|
||||||
|
assert out.shape == (4,)
|
||||||
|
assert out.dtype == np.float64
|
||||||
|
|||||||
@@ -231,3 +231,20 @@ def test_tradeflow_streaming_matches_batch():
|
|||||||
dtype=np.float64,
|
dtype=np.float64,
|
||||||
)
|
)
|
||||||
assert _equal_with_nan(batch, streamed)
|
assert _equal_with_nan(batch, streamed)
|
||||||
|
|
||||||
|
|
||||||
|
def test_price_impact_streaming_matches_batch():
|
||||||
|
n = 30
|
||||||
|
mid = np.array([100.0 + 0.25 * math.sin(i * 0.5) for i in range(n)], dtype=np.float64)
|
||||||
|
is_buy = [i % 3 != 0 for i in range(n)]
|
||||||
|
price = np.array(
|
||||||
|
[mid[i] + (0.03 if is_buy[i] else -0.03) for i in range(n)], dtype=np.float64
|
||||||
|
)
|
||||||
|
size = np.array([1.0 + (i % 4) for i in range(n)], dtype=np.float64)
|
||||||
|
batch = ta.EffectiveSpread().batch(price, size, is_buy, mid)
|
||||||
|
streamer = ta.EffectiveSpread()
|
||||||
|
streamed = np.array(
|
||||||
|
[streamer.update(price[i], size[i], is_buy[i], mid[i]) for i in range(n)],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
assert _equal_with_nan(batch, streamed)
|
||||||
|
|||||||
@@ -6421,6 +6421,7 @@ wasm_ob_indicator!(
|
|||||||
);
|
);
|
||||||
wasm_ob_indicator!(WasmMicroprice, wc::Microprice, Microprice);
|
wasm_ob_indicator!(WasmMicroprice, wc::Microprice, Microprice);
|
||||||
wasm_ob_indicator!(WasmQuotedSpread, wc::QuotedSpread, QuotedSpread);
|
wasm_ob_indicator!(WasmQuotedSpread, wc::QuotedSpread, QuotedSpread);
|
||||||
|
wasm_ob_indicator!(WasmDepthSlope, wc::DepthSlope, DepthSlope);
|
||||||
|
|
||||||
// Top-N imbalance carries a `levels` parameter, so it is hand-written.
|
// Top-N imbalance carries a `levels` parameter, so it is hand-written.
|
||||||
#[wasm_bindgen(js_name = OrderBookImbalanceTopN)]
|
#[wasm_bindgen(js_name = OrderBookImbalanceTopN)]
|
||||||
@@ -6555,6 +6556,149 @@ impl WasmTradeImbalance {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================== Microstructure: Price Impact ==============================
|
||||||
|
//
|
||||||
|
// Price-impact indicators consume a trade paired with the mid prevailing at
|
||||||
|
// execution. Each `update(price, size, isBuy, mid)` takes one such trade-quote
|
||||||
|
// (`isBuy=true` for a buyer-initiated trade) — the streaming model for a live
|
||||||
|
// browser trade feed. Batch over a tape is provided by the Python and Node
|
||||||
|
// bindings.
|
||||||
|
|
||||||
|
fn build_trade_quote(
|
||||||
|
price: f64,
|
||||||
|
size: f64,
|
||||||
|
is_buy: bool,
|
||||||
|
mid: f64,
|
||||||
|
) -> Result<wc::TradeQuote, JsError> {
|
||||||
|
let trade = build_trade(price, size, is_buy)?;
|
||||||
|
wc::TradeQuote::new(trade, mid).map_err(map_err)
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! wasm_trade_quote_indicator {
|
||||||
|
($wasm:ident, $inner:ty, $js:ident) => {
|
||||||
|
#[wasm_bindgen(js_name = $js)]
|
||||||
|
pub struct $wasm {
|
||||||
|
inner: $inner,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for $wasm {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = $js)]
|
||||||
|
impl $wasm {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new() -> $wasm {
|
||||||
|
Self {
|
||||||
|
inner: <$inner>::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
price: f64,
|
||||||
|
size: f64,
|
||||||
|
is_buy: bool,
|
||||||
|
mid: f64,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_trade_quote(price, size, is_buy, mid)?))
|
||||||
|
}
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.inner.reset();
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = isReady)]
|
||||||
|
pub fn is_ready(&self) -> bool {
|
||||||
|
self.inner.is_ready()
|
||||||
|
}
|
||||||
|
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||||
|
pub fn warmup_period(&self) -> usize {
|
||||||
|
self.inner.warmup_period()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
wasm_trade_quote_indicator!(WasmEffectiveSpread, wc::EffectiveSpread, EffectiveSpread);
|
||||||
|
|
||||||
|
// Realized spread carries a `horizon` parameter, so it is hand-written.
|
||||||
|
#[wasm_bindgen(js_name = RealizedSpread)]
|
||||||
|
pub struct WasmRealizedSpread {
|
||||||
|
inner: wc::RealizedSpread,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = RealizedSpread)]
|
||||||
|
impl WasmRealizedSpread {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(horizon: usize) -> Result<WasmRealizedSpread, JsError> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::RealizedSpread::new(horizon).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
price: f64,
|
||||||
|
size: f64,
|
||||||
|
is_buy: bool,
|
||||||
|
mid: f64,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_trade_quote(price, size, is_buy, mid)?))
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kyle's lambda carries a `window` parameter, so it is hand-written.
|
||||||
|
#[wasm_bindgen(js_name = KylesLambda)]
|
||||||
|
pub struct WasmKylesLambda {
|
||||||
|
inner: wc::KylesLambda,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_class = KylesLambda)]
|
||||||
|
impl WasmKylesLambda {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(window: usize) -> Result<WasmKylesLambda, JsError> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: wc::KylesLambda::new(window).map_err(map_err)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
price: f64,
|
||||||
|
size: f64,
|
||||||
|
is_buy: bool,
|
||||||
|
mid: f64,
|
||||||
|
) -> Result<Option<f64>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.inner
|
||||||
|
.update(build_trade_quote(price, size, is_buy, mid)?))
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
//! Depth Slope — how fast resting liquidity accumulates away from the mid.
|
||||||
|
|
||||||
|
use crate::microstructure::{Level, OrderBook};
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Ordinary-least-squares slope of cumulative resting size against distance
|
||||||
|
/// from the mid, over the levels of one book side.
|
||||||
|
///
|
||||||
|
/// `signed_distance` is `+1.0` for the ask side (price above the mid) and
|
||||||
|
/// `−1.0` for the bid side (price below the mid), so the regressor `x` —
|
||||||
|
/// distance from the mid — is non-negative on both sides. The response `y` is
|
||||||
|
/// the cumulative size walking outward from the touch. Returns `0.0` for a
|
||||||
|
/// degenerate fit where every level sits at the same distance (zero variance in
|
||||||
|
/// `x`).
|
||||||
|
fn cumulative_slope(levels: &[Level], mid: f64, signed_distance: f64) -> f64 {
|
||||||
|
let count = levels.len() as f64;
|
||||||
|
let mut cumulative = 0.0;
|
||||||
|
let mut sum_x = 0.0;
|
||||||
|
let mut sum_y = 0.0;
|
||||||
|
let mut sum_xy = 0.0;
|
||||||
|
let mut sum_xx = 0.0;
|
||||||
|
for level in levels {
|
||||||
|
let x = signed_distance * (level.price - mid);
|
||||||
|
cumulative += level.size;
|
||||||
|
sum_x += x;
|
||||||
|
sum_y += cumulative;
|
||||||
|
sum_xy += x * cumulative;
|
||||||
|
sum_xx += x * x;
|
||||||
|
}
|
||||||
|
let denom = count * sum_xx - sum_x * sum_x;
|
||||||
|
if denom == 0.0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
(count * sum_xy - sum_x * sum_y) / denom
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Depth Slope — the average rate at which cumulative resting size grows with
|
||||||
|
/// distance from the mid, across the bid and ask sides of the book.
|
||||||
|
///
|
||||||
|
/// For each side the indicator runs an ordinary-least-squares regression of
|
||||||
|
/// cumulative size (walking outward from the touch) on the level's distance
|
||||||
|
/// from the mid, then reports the mean of the two slopes:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// slope_side = OLS slope of (|priceᵢ − mid|, Σ_{j≤i} sizeⱼ)
|
||||||
|
/// depthSlope = (slope_bid + slope_ask) / 2
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Because the response is *cumulative* size it never decreases with distance,
|
||||||
|
/// so the slope is non-negative: it is a magnitude, not a direction. A large
|
||||||
|
/// slope means cumulative liquidity builds quickly away from the touch — a deep
|
||||||
|
/// book that absorbs large orders with little walking; a small slope is a thin,
|
||||||
|
/// shallow book. A book whose size is concentrated at the touch and thins out
|
||||||
|
/// behind it (a fragile book) reads a *smaller* slope than one of equal total
|
||||||
|
/// depth that thickens with distance.
|
||||||
|
///
|
||||||
|
/// A side with fewer than two levels carries no slope, so the indicator returns
|
||||||
|
/// `0.0` whenever either side has fewer than two levels (including an empty
|
||||||
|
/// book).
|
||||||
|
///
|
||||||
|
/// `Input = OrderBook`, `Output = f64`. Stateless; ready after the first
|
||||||
|
/// snapshot.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{DepthSlope, Indicator, Level, OrderBook};
|
||||||
|
///
|
||||||
|
/// // Both sides thicken linearly away from the mid (sizes 1, 2, 3 …).
|
||||||
|
/// let book = OrderBook::new(
|
||||||
|
/// vec![Level::new(99.0, 1.0).unwrap(), Level::new(98.0, 2.0).unwrap()],
|
||||||
|
/// vec![Level::new(101.0, 1.0).unwrap(), Level::new(102.0, 2.0).unwrap()],
|
||||||
|
/// )
|
||||||
|
/// .unwrap();
|
||||||
|
/// let mut ds = DepthSlope::new();
|
||||||
|
/// assert!(ds.update(book).unwrap() > 0.0);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct DepthSlope {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DepthSlope {
|
||||||
|
/// Construct a new depth-slope indicator.
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for DepthSlope {
|
||||||
|
type Input = OrderBook;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, book: OrderBook) -> Option<f64> {
|
||||||
|
self.has_emitted = true;
|
||||||
|
let Some(mid) = book.mid() else {
|
||||||
|
return Some(0.0);
|
||||||
|
};
|
||||||
|
if book.bids.len() < 2 || book.asks.len() < 2 {
|
||||||
|
return Some(0.0);
|
||||||
|
}
|
||||||
|
let bid_slope = cumulative_slope(&book.bids, mid, -1.0);
|
||||||
|
let ask_slope = cumulative_slope(&book.asks, mid, 1.0);
|
||||||
|
Some(f64::midpoint(bid_slope, ask_slope))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"DepthSlope"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn book(bids: &[(f64, f64)], asks: &[(f64, f64)]) -> OrderBook {
|
||||||
|
let to_levels = |xs: &[(f64, f64)]| {
|
||||||
|
xs.iter()
|
||||||
|
.map(|&(p, s)| Level::new(p, s).unwrap())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
OrderBook::new(to_levels(bids), to_levels(asks)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let ds = DepthSlope::new();
|
||||||
|
assert_eq!(ds.name(), "DepthSlope");
|
||||||
|
assert_eq!(ds.warmup_period(), 1);
|
||||||
|
assert!(!ds.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn thickening_book_has_positive_slope() {
|
||||||
|
let mut ds = DepthSlope::new();
|
||||||
|
let out = ds
|
||||||
|
.update(book(
|
||||||
|
&[(99.0, 1.0), (98.0, 2.0), (97.0, 3.0)],
|
||||||
|
&[(101.0, 1.0), (102.0, 2.0), (103.0, 3.0)],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(out > 0.0);
|
||||||
|
assert!(ds.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn front_loaded_book_has_smaller_slope_than_back_loaded() {
|
||||||
|
// Same total depth (6 per side), but one book thickens away from the
|
||||||
|
// touch and the other thins. Cumulative slope is non-negative for both;
|
||||||
|
// the back-loaded book accumulates faster, so its slope is larger.
|
||||||
|
let mut back = DepthSlope::new();
|
||||||
|
let back_slope = back
|
||||||
|
.update(book(
|
||||||
|
&[(99.0, 1.0), (98.0, 2.0), (97.0, 3.0)],
|
||||||
|
&[(101.0, 1.0), (102.0, 2.0), (103.0, 3.0)],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
let mut front = DepthSlope::new();
|
||||||
|
let front_slope = front
|
||||||
|
.update(book(
|
||||||
|
&[(99.0, 3.0), (98.0, 2.0), (97.0, 1.0)],
|
||||||
|
&[(101.0, 3.0), (102.0, 2.0), (103.0, 1.0)],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(front_slope >= 0.0);
|
||||||
|
assert!(back_slope > front_slope);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn known_slope_value() {
|
||||||
|
// Symmetric book, each side: distances 1, 2; cumulative sizes 1, 3.
|
||||||
|
// OLS slope of (1->1, 2->3) = 2. Mean of two equal sides = 2.
|
||||||
|
let mut ds = DepthSlope::new();
|
||||||
|
let out = ds
|
||||||
|
.update(book(
|
||||||
|
&[(99.0, 1.0), (98.0, 2.0)],
|
||||||
|
&[(101.0, 1.0), (102.0, 2.0)],
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!((out - 2.0).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_level_side_is_zero() {
|
||||||
|
let mut ds = DepthSlope::new();
|
||||||
|
// Bid side has only one level -> no slope -> 0.
|
||||||
|
assert_eq!(
|
||||||
|
ds.update(book(&[(100.0, 1.0)], &[(101.0, 1.0), (102.0, 1.0)])),
|
||||||
|
Some(0.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_book_is_zero() {
|
||||||
|
let mut ds = DepthSlope::new();
|
||||||
|
assert_eq!(
|
||||||
|
ds.update(OrderBook::new_unchecked(vec![], vec![])),
|
||||||
|
Some(0.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn degenerate_distance_slope_is_zero() {
|
||||||
|
// Two levels at the same distance from mid carry zero x-variance.
|
||||||
|
let levels = [
|
||||||
|
Level::new_unchecked(100.0, 1.0),
|
||||||
|
Level::new_unchecked(100.0, 2.0),
|
||||||
|
];
|
||||||
|
assert_eq!(cumulative_slope(&levels, 100.0, 1.0), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let books: Vec<OrderBook> = (0..20)
|
||||||
|
.map(|i| {
|
||||||
|
let extra = f64::from(i % 4);
|
||||||
|
book(
|
||||||
|
&[(99.0, 1.0 + extra), (98.0, 2.0)],
|
||||||
|
&[(101.0, 1.0), (102.0, 2.0 + extra)],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut a = DepthSlope::new();
|
||||||
|
let mut b = DepthSlope::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch(&books),
|
||||||
|
books
|
||||||
|
.iter()
|
||||||
|
.map(|x| b.update(x.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut ds = DepthSlope::new();
|
||||||
|
ds.update(book(
|
||||||
|
&[(99.0, 1.0), (98.0, 2.0)],
|
||||||
|
&[(101.0, 1.0), (102.0, 2.0)],
|
||||||
|
));
|
||||||
|
assert!(ds.is_ready());
|
||||||
|
ds.reset();
|
||||||
|
assert!(!ds.is_ready());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
//! Effective Spread — the realised cost of a single trade in basis points.
|
||||||
|
|
||||||
|
use crate::microstructure::TradeQuote;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Effective Spread — twice the signed deviation of an executed trade price
|
||||||
|
/// from the prevailing mid, expressed in basis points of the mid.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// effectiveSpread = 2 · D · (tradePrice − mid) / mid · 10_000 (bps)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// where `D` is the aggressor sign (`+1` for a buy, `−1` for a sell). The
|
||||||
|
/// factor of two scales the one-sided deviation up to a full round-trip cost so
|
||||||
|
/// it is directly comparable to the [quoted spread]: a marketable order that
|
||||||
|
/// fills exactly at the touch of an otherwise quoted-spread book pays an
|
||||||
|
/// effective spread equal to the quoted spread. Trades that fill *inside* the
|
||||||
|
/// spread (price improvement) read below the quoted spread; trades that walk
|
||||||
|
/// the book read above it.
|
||||||
|
///
|
||||||
|
/// A buy printed above the mid (`tradePrice > mid`) and a sell printed below it
|
||||||
|
/// both yield a positive effective spread — the conventional sign, since the
|
||||||
|
/// aggressor pays in both cases. A trade printed on the wrong side of the mid
|
||||||
|
/// for its aggressor flag (a buy below the mid) reads negative, the signature of
|
||||||
|
/// price improvement or a stale/mislabelled quote.
|
||||||
|
///
|
||||||
|
/// `Input = TradeQuote`, `Output = f64`. Stateless; ready after the first
|
||||||
|
/// trade-quote.
|
||||||
|
///
|
||||||
|
/// [quoted spread]: crate::QuotedSpread
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{EffectiveSpread, Indicator, Side, Trade, TradeQuote};
|
||||||
|
///
|
||||||
|
/// let mut es = EffectiveSpread::new();
|
||||||
|
/// // Buy filled at 100.05 against a mid of 100.0:
|
||||||
|
/// // 2 · (+1) · (100.05 − 100.0) / 100.0 · 10_000 = 10 bps.
|
||||||
|
/// let trade = Trade::new(100.05, 1.0, Side::Buy, 0).unwrap();
|
||||||
|
/// let quote = TradeQuote::new(trade, 100.0).unwrap();
|
||||||
|
/// assert!((es.update(quote).unwrap() - 10.0).abs() < 1e-9);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct EffectiveSpread {
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EffectiveSpread {
|
||||||
|
/// Construct a new effective-spread indicator.
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { has_emitted: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for EffectiveSpread {
|
||||||
|
type Input = TradeQuote;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, quote: TradeQuote) -> Option<f64> {
|
||||||
|
self.has_emitted = true;
|
||||||
|
let sign = quote.trade.side.sign();
|
||||||
|
Some(2.0 * sign * (quote.trade.price - quote.mid) / quote.mid * 10_000.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"EffectiveSpread"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::microstructure::{Side, Trade};
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn quote(price: f64, side: Side, mid: f64) -> TradeQuote {
|
||||||
|
TradeQuote::new(Trade::new(price, 1.0, side, 0).unwrap(), mid).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let es = EffectiveSpread::new();
|
||||||
|
assert_eq!(es.name(), "EffectiveSpread");
|
||||||
|
assert_eq!(es.warmup_period(), 1);
|
||||||
|
assert!(!es.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn buy_above_mid_is_positive() {
|
||||||
|
let mut es = EffectiveSpread::new();
|
||||||
|
// 2 · (+1) · (100.05 − 100.0) / 100.0 · 10_000 = 10 bps.
|
||||||
|
let out = es.update(quote(100.05, Side::Buy, 100.0)).unwrap();
|
||||||
|
assert!((out - 10.0).abs() < 1e-9);
|
||||||
|
assert!(es.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sell_below_mid_is_positive() {
|
||||||
|
let mut es = EffectiveSpread::new();
|
||||||
|
// 2 · (−1) · (99.95 − 100.0) / 100.0 · 10_000 = 10 bps.
|
||||||
|
let out = es.update(quote(99.95, Side::Sell, 100.0)).unwrap();
|
||||||
|
assert!((out - 10.0).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn price_improvement_reads_negative() {
|
||||||
|
let mut es = EffectiveSpread::new();
|
||||||
|
// A buy filled below the mid: price improvement -> negative.
|
||||||
|
let out = es.update(quote(99.95, Side::Buy, 100.0)).unwrap();
|
||||||
|
assert!(out < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trade_at_mid_is_zero() {
|
||||||
|
let mut es = EffectiveSpread::new();
|
||||||
|
assert_eq!(es.update(quote(100.0, Side::Buy, 100.0)), Some(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let quotes: Vec<TradeQuote> = (0..20)
|
||||||
|
.map(|i| {
|
||||||
|
let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
|
||||||
|
let price = 100.0 + f64::from(i % 4) * 0.01;
|
||||||
|
quote(price, side, 100.0)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut a = EffectiveSpread::new();
|
||||||
|
let mut b = EffectiveSpread::new();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch("es),
|
||||||
|
quotes.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut es = EffectiveSpread::new();
|
||||||
|
es.update(quote(100.05, Side::Buy, 100.0));
|
||||||
|
assert!(es.is_ready());
|
||||||
|
es.reset();
|
||||||
|
assert!(!es.is_ready());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
//! Kyle's Lambda — rolling price impact per unit of signed order flow.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::microstructure::TradeQuote;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Kyle's Lambda — the rolling ordinary-least-squares slope of mid-price changes
|
||||||
|
/// on signed trade volume, the canonical measure of market depth / price
|
||||||
|
/// impact.
|
||||||
|
///
|
||||||
|
/// Each `update` receives a [`TradeQuote`] — a trade plus the mid prevailing at
|
||||||
|
/// execution. Internally the indicator forms, per trade, the mid change since
|
||||||
|
/// the previous trade (`Δmid = midₜ − midₜ₋₁`) and the signed volume
|
||||||
|
/// (`q = size · D`, with `D` the aggressor sign), then runs a rolling OLS
|
||||||
|
/// regression of `Δmid` on `q` over the trailing window of `window` trades:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// cov = (1/n) · Σ q·Δmid − q̄·Δ̄mid
|
||||||
|
/// var = (1/n) · Σ q² − q̄²
|
||||||
|
/// λ = cov / var
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// `λ` is the estimated price move per unit of signed volume: a deep, liquid
|
||||||
|
/// book absorbs flow with little movement and reads a small `λ`; a thin book
|
||||||
|
/// moves sharply per unit traded and reads a large `λ`. It is a direct,
|
||||||
|
/// model-light proxy for the slope of the demand curve in Kyle's microstructure
|
||||||
|
/// model.
|
||||||
|
///
|
||||||
|
/// Each `update` is O(1): four running sums (`Σq`, `ΣΔmid`, `Σq²`, `Σq·Δmid`)
|
||||||
|
/// are maintained as the window slides. A window of constant signed volume has
|
||||||
|
/// zero variance and `λ` is undefined; the indicator returns `0` in that case
|
||||||
|
/// rather than producing `NaN`.
|
||||||
|
///
|
||||||
|
/// `Input = TradeQuote`, `Output = f64`. It warms up for `window + 1`
|
||||||
|
/// trade-quotes: one to seed the previous mid, then `window` paired
|
||||||
|
/// observations.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{Indicator, KylesLambda, Side, Trade, TradeQuote};
|
||||||
|
///
|
||||||
|
/// // A book where each trade moves the mid by exactly 0.5 per unit of signed
|
||||||
|
/// // volume gives λ = 0.5.
|
||||||
|
/// let mut lambda = KylesLambda::new(8).unwrap();
|
||||||
|
/// let mut mid = 100.0;
|
||||||
|
/// let mut last = None;
|
||||||
|
/// for i in 0..20 {
|
||||||
|
/// let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
|
||||||
|
/// let size = 1.0 + f64::from(i % 3);
|
||||||
|
/// let signed = size * side.sign();
|
||||||
|
/// mid += 0.5 * signed;
|
||||||
|
/// let trade = Trade::new(mid, size, side, 0).unwrap();
|
||||||
|
/// last = lambda.update(TradeQuote::new(trade, mid).unwrap());
|
||||||
|
/// }
|
||||||
|
/// assert!((last.unwrap() - 0.5).abs() < 1e-9);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct KylesLambda {
|
||||||
|
window: usize,
|
||||||
|
prev_mid: Option<f64>,
|
||||||
|
pairs: VecDeque<(f64, f64)>,
|
||||||
|
sum_q: f64,
|
||||||
|
sum_dm: f64,
|
||||||
|
sum_qq: f64,
|
||||||
|
sum_qdm: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KylesLambda {
|
||||||
|
/// Construct a rolling Kyle's lambda over `window` paired observations.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::InvalidPeriod`] if `window < 2` (the regression
|
||||||
|
/// variance needs at least two observations).
|
||||||
|
pub fn new(window: usize) -> Result<Self> {
|
||||||
|
if window < 2 {
|
||||||
|
return Err(Error::InvalidPeriod {
|
||||||
|
message: "kyle's lambda needs window >= 2",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
window,
|
||||||
|
prev_mid: None,
|
||||||
|
pairs: VecDeque::with_capacity(window),
|
||||||
|
sum_q: 0.0,
|
||||||
|
sum_dm: 0.0,
|
||||||
|
sum_qq: 0.0,
|
||||||
|
sum_qdm: 0.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The configured window length, in paired observations.
|
||||||
|
pub const fn window(&self) -> usize {
|
||||||
|
self.window
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_pair(&mut self, signed_vol: f64, delta_mid: f64) -> Option<f64> {
|
||||||
|
if self.pairs.len() == self.window {
|
||||||
|
let (old_q, old_dm) = self.pairs.pop_front().expect("non-empty");
|
||||||
|
self.sum_q -= old_q;
|
||||||
|
self.sum_dm -= old_dm;
|
||||||
|
self.sum_qq -= old_q * old_q;
|
||||||
|
self.sum_qdm -= old_q * old_dm;
|
||||||
|
}
|
||||||
|
self.pairs.push_back((signed_vol, delta_mid));
|
||||||
|
self.sum_q += signed_vol;
|
||||||
|
self.sum_dm += delta_mid;
|
||||||
|
self.sum_qq += signed_vol * signed_vol;
|
||||||
|
self.sum_qdm += signed_vol * delta_mid;
|
||||||
|
if self.pairs.len() < self.window {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let n = self.window as f64;
|
||||||
|
let mean_q = self.sum_q / n;
|
||||||
|
let mean_dm = self.sum_dm / n;
|
||||||
|
let var_q = (self.sum_qq / n - mean_q * mean_q).max(0.0);
|
||||||
|
let cov = self.sum_qdm / n - mean_q * mean_dm;
|
||||||
|
if var_q == 0.0 {
|
||||||
|
// Constant signed-volume window has no defined slope.
|
||||||
|
return Some(0.0);
|
||||||
|
}
|
||||||
|
Some(cov / var_q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for KylesLambda {
|
||||||
|
type Input = TradeQuote;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, quote: TradeQuote) -> Option<f64> {
|
||||||
|
let mid = quote.mid;
|
||||||
|
let signed_vol = quote.trade.size * quote.trade.side.sign();
|
||||||
|
let Some(prev) = self.prev_mid else {
|
||||||
|
self.prev_mid = Some(mid);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
self.prev_mid = Some(mid);
|
||||||
|
self.push_pair(signed_vol, mid - prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.prev_mid = None;
|
||||||
|
self.pairs.clear();
|
||||||
|
self.sum_q = 0.0;
|
||||||
|
self.sum_dm = 0.0;
|
||||||
|
self.sum_qq = 0.0;
|
||||||
|
self.sum_qdm = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.window + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.pairs.len() == self.window
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"KylesLambda"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::microstructure::{Side, Trade};
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
use approx::assert_relative_eq;
|
||||||
|
|
||||||
|
fn quotes_with_impact(n: usize, impact: f64) -> Vec<TradeQuote> {
|
||||||
|
let mut mid = 100.0;
|
||||||
|
(0..n)
|
||||||
|
.map(|i| {
|
||||||
|
let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
|
||||||
|
let size = 1.0 + (i % 3) as f64;
|
||||||
|
let signed = size * side.sign();
|
||||||
|
mid += impact * signed;
|
||||||
|
let trade = Trade::new(mid, size, side, 0).unwrap();
|
||||||
|
TradeQuote::new(trade, mid).unwrap()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_window_below_two() {
|
||||||
|
assert!(KylesLambda::new(0).is_err());
|
||||||
|
assert!(KylesLambda::new(1).is_err());
|
||||||
|
assert!(KylesLambda::new(2).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let kl = KylesLambda::new(14).unwrap();
|
||||||
|
assert_eq!(kl.name(), "KylesLambda");
|
||||||
|
assert_eq!(kl.window(), 14);
|
||||||
|
assert_eq!(kl.warmup_period(), 15);
|
||||||
|
assert!(!kl.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recovers_constant_impact_slope() {
|
||||||
|
// mid moves exactly 0.5 per unit signed volume -> lambda = 0.5.
|
||||||
|
let last = KylesLambda::new(6)
|
||||||
|
.unwrap()
|
||||||
|
.batch("es_with_impact(20, 0.5))
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.last()
|
||||||
|
.unwrap();
|
||||||
|
assert_relative_eq!(last, 0.5, epsilon = 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negative_impact_reads_negative() {
|
||||||
|
let last = KylesLambda::new(6)
|
||||||
|
.unwrap()
|
||||||
|
.batch("es_with_impact(20, -0.3))
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.last()
|
||||||
|
.unwrap();
|
||||||
|
assert_relative_eq!(last, -0.3, epsilon = 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn constant_signed_volume_is_zero() {
|
||||||
|
// Every trade is a buy of size 1: signed volume is constant -> var 0 -> 0.
|
||||||
|
let mut mid = 100.0;
|
||||||
|
let quotes: Vec<TradeQuote> = (0..10)
|
||||||
|
.map(|_| {
|
||||||
|
mid += 0.01;
|
||||||
|
let trade = Trade::new(mid, 1.0, Side::Buy, 0).unwrap();
|
||||||
|
TradeQuote::new(trade, mid).unwrap()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let last = KylesLambda::new(5)
|
||||||
|
.unwrap()
|
||||||
|
.batch("es)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.last()
|
||||||
|
.unwrap();
|
||||||
|
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn warms_up_after_window_plus_one() {
|
||||||
|
let mut kl = KylesLambda::new(3).unwrap();
|
||||||
|
let quotes = quotes_with_impact(4, 0.2);
|
||||||
|
assert_eq!(kl.update(quotes[0]), None); // seeds prev mid
|
||||||
|
assert_eq!(kl.update(quotes[1]), None);
|
||||||
|
assert_eq!(kl.update(quotes[2]), None);
|
||||||
|
assert!(!kl.is_ready());
|
||||||
|
assert!(kl.update(quotes[3]).is_some());
|
||||||
|
assert!(kl.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let quotes = quotes_with_impact(40, 0.15);
|
||||||
|
let batch = KylesLambda::new(10).unwrap().batch("es);
|
||||||
|
let mut kl = KylesLambda::new(10).unwrap();
|
||||||
|
let streamed: Vec<_> = quotes.iter().map(|q| kl.update(*q)).collect();
|
||||||
|
assert_eq!(batch, streamed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut kl = KylesLambda::new(3).unwrap();
|
||||||
|
for q in quotes_with_impact(6, 0.2) {
|
||||||
|
kl.update(q);
|
||||||
|
}
|
||||||
|
assert!(kl.is_ready());
|
||||||
|
kl.reset();
|
||||||
|
assert!(!kl.is_ready());
|
||||||
|
assert_eq!(kl.update(quotes_with_impact(1, 0.2)[0]), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ mod decycler_oscillator;
|
|||||||
mod dema;
|
mod dema;
|
||||||
mod demand_index;
|
mod demand_index;
|
||||||
mod demark_pivots;
|
mod demark_pivots;
|
||||||
|
mod depth_slope;
|
||||||
mod detrended_std_dev;
|
mod detrended_std_dev;
|
||||||
mod doji;
|
mod doji;
|
||||||
mod donchian;
|
mod donchian;
|
||||||
@@ -62,6 +63,7 @@ mod double_bollinger;
|
|||||||
mod dpo;
|
mod dpo;
|
||||||
mod drawdown_duration;
|
mod drawdown_duration;
|
||||||
mod ease_of_movement;
|
mod ease_of_movement;
|
||||||
|
mod effective_spread;
|
||||||
mod ehlers_stochastic;
|
mod ehlers_stochastic;
|
||||||
mod elder_impulse;
|
mod elder_impulse;
|
||||||
mod ema;
|
mod ema;
|
||||||
@@ -100,6 +102,7 @@ mod keltner;
|
|||||||
mod kst;
|
mod kst;
|
||||||
mod kurtosis;
|
mod kurtosis;
|
||||||
mod kvo;
|
mod kvo;
|
||||||
|
mod kyles_lambda;
|
||||||
mod laguerre_rsi;
|
mod laguerre_rsi;
|
||||||
mod lead_lag_cross_correlation;
|
mod lead_lag_cross_correlation;
|
||||||
mod linreg;
|
mod linreg;
|
||||||
@@ -144,6 +147,7 @@ mod psar;
|
|||||||
mod pvi;
|
mod pvi;
|
||||||
mod quoted_spread;
|
mod quoted_spread;
|
||||||
mod r_squared;
|
mod r_squared;
|
||||||
|
mod realized_spread;
|
||||||
mod recovery_factor;
|
mod recovery_factor;
|
||||||
mod relative_strength_ab;
|
mod relative_strength_ab;
|
||||||
mod renko_trailing_stop;
|
mod renko_trailing_stop;
|
||||||
@@ -281,6 +285,7 @@ pub use decycler_oscillator::DecyclerOscillator;
|
|||||||
pub use dema::Dema;
|
pub use dema::Dema;
|
||||||
pub use demand_index::DemandIndex;
|
pub use demand_index::DemandIndex;
|
||||||
pub use demark_pivots::{DemarkPivots, DemarkPivotsOutput};
|
pub use demark_pivots::{DemarkPivots, DemarkPivotsOutput};
|
||||||
|
pub use depth_slope::DepthSlope;
|
||||||
pub use detrended_std_dev::DetrendedStdDev;
|
pub use detrended_std_dev::DetrendedStdDev;
|
||||||
pub use doji::Doji;
|
pub use doji::Doji;
|
||||||
pub use donchian::{Donchian, DonchianOutput};
|
pub use donchian::{Donchian, DonchianOutput};
|
||||||
@@ -289,6 +294,7 @@ pub use double_bollinger::{DoubleBollinger, DoubleBollingerOutput};
|
|||||||
pub use dpo::Dpo;
|
pub use dpo::Dpo;
|
||||||
pub use drawdown_duration::DrawdownDuration;
|
pub use drawdown_duration::DrawdownDuration;
|
||||||
pub use ease_of_movement::EaseOfMovement;
|
pub use ease_of_movement::EaseOfMovement;
|
||||||
|
pub use effective_spread::EffectiveSpread;
|
||||||
pub use ehlers_stochastic::EhlersStochastic;
|
pub use ehlers_stochastic::EhlersStochastic;
|
||||||
pub use elder_impulse::ElderImpulse;
|
pub use elder_impulse::ElderImpulse;
|
||||||
pub use ema::Ema;
|
pub use ema::Ema;
|
||||||
@@ -327,6 +333,7 @@ pub use keltner::{Keltner, KeltnerOutput};
|
|||||||
pub use kst::{Kst, KstOutput};
|
pub use kst::{Kst, KstOutput};
|
||||||
pub use kurtosis::Kurtosis;
|
pub use kurtosis::Kurtosis;
|
||||||
pub use kvo::Kvo;
|
pub use kvo::Kvo;
|
||||||
|
pub use kyles_lambda::KylesLambda;
|
||||||
pub use laguerre_rsi::LaguerreRsi;
|
pub use laguerre_rsi::LaguerreRsi;
|
||||||
pub use lead_lag_cross_correlation::{LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput};
|
pub use lead_lag_cross_correlation::{LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput};
|
||||||
pub use linreg::LinearRegression;
|
pub use linreg::LinearRegression;
|
||||||
@@ -371,6 +378,7 @@ pub use psar::Psar;
|
|||||||
pub use pvi::Pvi;
|
pub use pvi::Pvi;
|
||||||
pub use quoted_spread::QuotedSpread;
|
pub use quoted_spread::QuotedSpread;
|
||||||
pub use r_squared::RSquared;
|
pub use r_squared::RSquared;
|
||||||
|
pub use realized_spread::RealizedSpread;
|
||||||
pub use recovery_factor::RecoveryFactor;
|
pub use recovery_factor::RecoveryFactor;
|
||||||
pub use relative_strength_ab::{RelativeStrengthAB, RelativeStrengthOutput};
|
pub use relative_strength_ab::{RelativeStrengthAB, RelativeStrengthOutput};
|
||||||
pub use renko_trailing_stop::RenkoTrailingStop;
|
pub use renko_trailing_stop::RenkoTrailingStop;
|
||||||
@@ -731,9 +739,13 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
|||||||
"OrderBookImbalanceFull",
|
"OrderBookImbalanceFull",
|
||||||
"Microprice",
|
"Microprice",
|
||||||
"QuotedSpread",
|
"QuotedSpread",
|
||||||
|
"DepthSlope",
|
||||||
"SignedVolume",
|
"SignedVolume",
|
||||||
"CumulativeVolumeDelta",
|
"CumulativeVolumeDelta",
|
||||||
"TradeImbalance",
|
"TradeImbalance",
|
||||||
|
"EffectiveSpread",
|
||||||
|
"RealizedSpread",
|
||||||
|
"KylesLambda",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -790,6 +802,6 @@ mod family_tests {
|
|||||||
// the actual indicator count is the early-warning signal that an
|
// the actual indicator count is the early-warning signal that an
|
||||||
// indicator was added without being assigned a family.
|
// indicator was added without being assigned a family.
|
||||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||||
assert_eq!(total, 222, "FAMILIES total drifted from indicator count");
|
assert_eq!(total, 226, "FAMILIES total drifted from indicator count");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
//! Realized Spread — the post-trade liquidity revenue of a trade in basis
|
||||||
|
//! points.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::microstructure::TradeQuote;
|
||||||
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
|
/// Realized Spread — twice the signed deviation of a trade price from the mid
|
||||||
|
/// that prevails `horizon` trades *later*, expressed in basis points of the
|
||||||
|
/// trade's contemporaneous mid.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// realizedSpread = 2 · D · (tradePrice − mid_{t+horizon}) / mid_t · 10_000 (bps)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// where `D` is the aggressor sign (`+1` for a buy, `−1` for a sell), `mid_t`
|
||||||
|
/// is the mid at the time of the trade, and `mid_{t+horizon}` is the mid
|
||||||
|
/// `horizon` trade-quotes later. Where the [effective spread] measures the full
|
||||||
|
/// cost paid by the aggressor against the contemporaneous mid, the realized
|
||||||
|
/// spread measures the share of that cost a liquidity provider *keeps* after
|
||||||
|
/// the mid has moved: it is the effective spread net of the price impact
|
||||||
|
/// (`effective = realized + 2 · priceImpact`). A high realized spread means
|
||||||
|
/// the quote was not picked off; a low or negative one is the signature of
|
||||||
|
/// adverse selection, the trade preceding a move in its own direction.
|
||||||
|
///
|
||||||
|
/// The indicator buffers each incoming trade-quote and emits the realized
|
||||||
|
/// spread for the trade made `horizon` updates ago, once that future mid is
|
||||||
|
/// known. It warms up for `horizon + 1` trade-quotes — `update` returns `None`
|
||||||
|
/// until the first trade can be resolved — and then emits one value per update
|
||||||
|
/// in O(1).
|
||||||
|
///
|
||||||
|
/// `Input = TradeQuote`, `Output = f64`.
|
||||||
|
///
|
||||||
|
/// [effective spread]: crate::EffectiveSpread
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use wickra_core::{Indicator, RealizedSpread, Side, Trade, TradeQuote};
|
||||||
|
///
|
||||||
|
/// let mut rs = RealizedSpread::new(1).unwrap();
|
||||||
|
/// let tq = |price: f64, side, mid| TradeQuote::new(Trade::new(price, 1.0, side, 0).unwrap(), mid).unwrap();
|
||||||
|
/// // First trade buffered; nothing to resolve yet.
|
||||||
|
/// assert_eq!(rs.update(tq(100.10, Side::Buy, 100.0)), None);
|
||||||
|
/// // One trade later the mid is 100.20, resolving the first buy:
|
||||||
|
/// // 2 · (+1) · (100.10 − 100.20) / 100.0 · 10_000 = −20 bps (adverse selection).
|
||||||
|
/// let out = rs.update(tq(99.90, Side::Sell, 100.20)).unwrap();
|
||||||
|
/// assert!((out - (-20.0)).abs() < 1e-9);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RealizedSpread {
|
||||||
|
horizon: usize,
|
||||||
|
// Each pending entry is (aggressor sign, trade price, contemporaneous mid).
|
||||||
|
pending: VecDeque<(f64, f64, f64)>,
|
||||||
|
has_emitted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RealizedSpread {
|
||||||
|
/// Construct a realized-spread indicator that resolves each trade against
|
||||||
|
/// the mid `horizon` trade-quotes later.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::PeriodZero`] if `horizon` is zero (the realized spread
|
||||||
|
/// is defined against a strictly future mid).
|
||||||
|
pub fn new(horizon: usize) -> Result<Self> {
|
||||||
|
if horizon == 0 {
|
||||||
|
return Err(Error::PeriodZero);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
horizon,
|
||||||
|
pending: VecDeque::with_capacity(horizon + 1),
|
||||||
|
has_emitted: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The configured horizon, in trade-quotes.
|
||||||
|
pub const fn horizon(&self) -> usize {
|
||||||
|
self.horizon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Indicator for RealizedSpread {
|
||||||
|
type Input = TradeQuote;
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn update(&mut self, quote: TradeQuote) -> Option<f64> {
|
||||||
|
let sign = quote.trade.side.sign();
|
||||||
|
self.pending.push_back((sign, quote.trade.price, quote.mid));
|
||||||
|
if self.pending.len() <= self.horizon {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (old_sign, old_price, old_mid) = self.pending.pop_front().expect("len > horizon >= 1");
|
||||||
|
self.has_emitted = true;
|
||||||
|
// `quote.mid` is the mid prevailing `horizon` trades after the resolved
|
||||||
|
// trade; normalise by that trade's own contemporaneous mid.
|
||||||
|
Some(2.0 * old_sign * (old_price - quote.mid) / old_mid * 10_000.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.pending.clear();
|
||||||
|
self.has_emitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warmup_period(&self) -> usize {
|
||||||
|
self.horizon + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_ready(&self) -> bool {
|
||||||
|
self.has_emitted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"RealizedSpread"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::microstructure::{Side, Trade};
|
||||||
|
use crate::traits::BatchExt;
|
||||||
|
|
||||||
|
fn tq(price: f64, side: Side, mid: f64) -> TradeQuote {
|
||||||
|
TradeQuote::new(Trade::new(price, 1.0, side, 0).unwrap(), mid).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_zero_horizon() {
|
||||||
|
assert!(matches!(RealizedSpread::new(0), Err(Error::PeriodZero)));
|
||||||
|
assert!(RealizedSpread::new(1).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accessors_and_metadata() {
|
||||||
|
let rs = RealizedSpread::new(3).unwrap();
|
||||||
|
assert_eq!(rs.name(), "RealizedSpread");
|
||||||
|
assert_eq!(rs.horizon(), 3);
|
||||||
|
assert_eq!(rs.warmup_period(), 4);
|
||||||
|
assert!(!rs.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_against_future_mid() {
|
||||||
|
let mut rs = RealizedSpread::new(1).unwrap();
|
||||||
|
assert_eq!(rs.update(tq(100.10, Side::Buy, 100.0)), None);
|
||||||
|
assert!(!rs.is_ready());
|
||||||
|
// 2 · (+1) · (100.10 − 100.20) / 100.0 · 10_000 = −20 bps.
|
||||||
|
let out = rs.update(tq(99.90, Side::Sell, 100.20)).unwrap();
|
||||||
|
assert!((out - (-20.0)).abs() < 1e-9);
|
||||||
|
assert!(rs.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_adverse_move_equals_effective_spread() {
|
||||||
|
// If the mid does not move over the horizon, realized == effective.
|
||||||
|
let mut rs = RealizedSpread::new(1).unwrap();
|
||||||
|
rs.update(tq(100.05, Side::Buy, 100.0));
|
||||||
|
// mid stays at 100.0 -> 2 · (100.05 − 100.0) / 100.0 · 10_000 = 10 bps.
|
||||||
|
let out = rs.update(tq(100.0, Side::Buy, 100.0)).unwrap();
|
||||||
|
assert!((out - 10.0).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn longer_horizon_warms_up() {
|
||||||
|
let mut rs = RealizedSpread::new(3).unwrap();
|
||||||
|
for _ in 0..3 {
|
||||||
|
assert_eq!(rs.update(tq(100.0, Side::Buy, 100.0)), None);
|
||||||
|
}
|
||||||
|
assert!(!rs.is_ready());
|
||||||
|
assert!(rs.update(tq(100.0, Side::Buy, 100.0)).is_some());
|
||||||
|
assert!(rs.is_ready());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn batch_equals_streaming() {
|
||||||
|
let quotes: Vec<TradeQuote> = (0..30)
|
||||||
|
.map(|i| {
|
||||||
|
let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
|
||||||
|
let mid = 100.0 + f64::from(i % 5) * 0.05;
|
||||||
|
tq(mid + 0.02, side, mid)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut a = RealizedSpread::new(4).unwrap();
|
||||||
|
let mut b = RealizedSpread::new(4).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
a.batch("es),
|
||||||
|
quotes.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_clears_state() {
|
||||||
|
let mut rs = RealizedSpread::new(1).unwrap();
|
||||||
|
rs.update(tq(100.05, Side::Buy, 100.0));
|
||||||
|
rs.update(tq(100.0, Side::Buy, 100.0));
|
||||||
|
assert!(rs.is_ready());
|
||||||
|
rs.reset();
|
||||||
|
assert!(!rs.is_ready());
|
||||||
|
assert_eq!(rs.update(tq(100.05, Side::Buy, 100.0)), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,36 +55,36 @@ pub use indicators::{
|
|||||||
ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, Cmo,
|
ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, Cmo,
|
||||||
CoefficientOfVariation, Cointegration, CointegrationOutput, ConditionalValueAtRisk, ConnorsRsi,
|
CoefficientOfVariation, Cointegration, CointegrationOutput, ConditionalValueAtRisk, ConnorsRsi,
|
||||||
Coppock, CumulativeVolumeDelta, CyberneticCycle, Decycler, DecyclerOscillator, Dema,
|
Coppock, CumulativeVolumeDelta, CyberneticCycle, Decycler, DecyclerOscillator, Dema,
|
||||||
DemandIndex, DemarkPivots, DemarkPivotsOutput, DetrendedStdDev, Doji, Donchian, DonchianOutput,
|
DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, Donchian,
|
||||||
DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, Dpo,
|
DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, Dpo,
|
||||||
DrawdownDuration, EaseOfMovement, EhlersStochastic, ElderImpulse, Ema,
|
DrawdownDuration, EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
||||||
EmpiricalModeDecomposition, Engulfing, Evwma, Fama, FibonacciPivots, FibonacciPivotsOutput,
|
EmpiricalModeDecomposition, Engulfing, Evwma, Fama, FibonacciPivots, FibonacciPivotsOutput,
|
||||||
FisherTransform, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, GainLossRatio,
|
FisherTransform, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, GainLossRatio,
|
||||||
GarmanKlassVolatility, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput, HiLoActivator,
|
GarmanKlassVolatility, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput, HiLoActivator,
|
||||||
HilbertDominantCycle, HistoricalVolatility, Hma, HurstChannel, HurstChannelOutput,
|
HilbertDominantCycle, HistoricalVolatility, Hma, HurstChannel, HurstChannelOutput,
|
||||||
HurstExponent, Ichimoku, IchimokuOutput, Inertia, InformationRatio, InitialBalance,
|
HurstExponent, Ichimoku, IchimokuOutput, Inertia, InformationRatio, InitialBalance,
|
||||||
InitialBalanceOutput, InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma,
|
InitialBalanceOutput, InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma,
|
||||||
Kama, KellyCriterion, Keltner, KeltnerOutput, Kst, KstOutput, Kurtosis, Kvo, LaguerreRsi,
|
Kama, KellyCriterion, Keltner, KeltnerOutput, Kst, KstOutput, Kurtosis, Kvo, KylesLambda,
|
||||||
LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel,
|
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||||
LinRegChannelOutput, LinRegSlope, LinearRegression, MaEnvelope, MaEnvelopeOutput,
|
LinRegChannel, LinRegChannelOutput, LinRegSlope, LinearRegression, MaEnvelope,
|
||||||
MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
|
MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
|
||||||
MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, Mom,
|
Marubozu, MassIndex, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi,
|
||||||
MorningEveningStar, Natr, Nvi, Obv, OmegaRatio, OpeningRange, OpeningRangeOutput,
|
Microprice, Mom, MorningEveningStar, Natr, Nvi, Obv, OmegaRatio, OpeningRange,
|
||||||
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex,
|
OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN,
|
||||||
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentB,
|
PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentB,
|
||||||
PercentageTrailingStop, Pgo, PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi,
|
PercentageTrailingStop, Pgo, PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi,
|
||||||
QuotedSpread, RSquared, RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput,
|
QuotedSpread, RSquared, RealizedSpread, RecoveryFactor, RelativeStrengthAB,
|
||||||
RenkoTrailingStop, Roc, RogersSatchellVolatility, RollingVwap, RoofingFilter, Rsi, Rvi,
|
RelativeStrengthOutput, RenkoTrailingStop, Roc, RogersSatchellVolatility, RollingVwap,
|
||||||
RviVolatility, Rwi, RwiOutput, SharpeRatio, ShootingStar, SignedVolume, SineWave, Skewness,
|
RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SharpeRatio, ShootingStar,
|
||||||
Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, SpinningTop, StandardError,
|
SignedVolume, SineWave, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation,
|
||||||
StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev,
|
SpinningTop, StandardError, StandardErrorBands, StandardErrorBandsOutput, StarcBands,
|
||||||
StepTrailingStop, StochRsi, Stochastic, StochasticOutput, SuperSmoother, SuperTrend,
|
StarcBandsOutput, Stc, StdDev, StepTrailingStop, StochRsi, Stochastic, StochasticOutput,
|
||||||
SuperTrendOutput, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdLinesOutput,
|
SuperSmoother, SuperTrend, SuperTrendOutput, TdCombo, TdCountdown, TdDeMarker, TdDifferential,
|
||||||
TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel,
|
TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei,
|
||||||
TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema, ThreeInside, ThreeOutside,
|
TdRiskLevel, TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema, ThreeInside,
|
||||||
ThreeSoldiersOrCrows, Tii, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv,
|
ThreeOutside, ThreeSoldiersOrCrows, Tii, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange,
|
||||||
TtmSqueeze, TtmSqueezeOutput, Tweezer, TypicalPrice, UlcerIndex, UltimateOscillator, ValueArea,
|
Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, Tweezer, TypicalPrice, UlcerIndex, UltimateOscillator,
|
||||||
ValueAreaOutput, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VoltyStop,
|
ValueArea, ValueAreaOutput, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VoltyStop,
|
||||||
VolumeOscillator, VolumePriceTrend, Vortex, VortexOutput, Vwap, VwapStdDevBands,
|
VolumeOscillator, VolumePriceTrend, Vortex, VortexOutput, Vwap, VwapStdDevBands,
|
||||||
VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, WeightedClose, WilliamsFractals,
|
VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, WeightedClose, WilliamsFractals,
|
||||||
WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility,
|
WilliamsFractalsOutput, WilliamsR, Wma, WoodiePivots, WoodiePivotsOutput, YangZhangVolatility,
|
||||||
|
|||||||
@@ -33,14 +33,14 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Through
|
|||||||
use std::hint::black_box;
|
use std::hint::black_box;
|
||||||
use wickra::{
|
use wickra::{
|
||||||
Adx, Atr, Autocorrelation, BatchExt, BollingerBands, BollingerOutput, CalmarRatio, Candle, Cci,
|
Adx, Atr, Autocorrelation, BatchExt, BollingerBands, BollingerOutput, CalmarRatio, Candle, Cci,
|
||||||
ClassicPivots, ConnorsRsi, Ema, EmpiricalModeDecomposition, Engulfing, Frama,
|
ClassicPivots, ConnorsRsi, DepthSlope, EffectiveSpread, Ema, EmpiricalModeDecomposition,
|
||||||
HilbertDominantCycle, HurstExponent, Ichimoku, IchimokuOutput, Indicator, Jma, Level,
|
Engulfing, Frama, HilbertDominantCycle, HurstExponent, Ichimoku, IchimokuOutput, Indicator,
|
||||||
LinearRegression, MacdIndicator, MacdOutput, Mama, MamaOutput, MaxDrawdown, Microprice, Obv,
|
Jma, KylesLambda, Level, LinearRegression, MacdIndicator, MacdOutput, Mama, MamaOutput,
|
||||||
OrderBook, OrderBookImbalanceFull, OrderBookImbalanceTop1, ParkinsonVolatility, Ppo, Psar,
|
MaxDrawdown, Microprice, Obv, OrderBook, OrderBookImbalanceFull, OrderBookImbalanceTop1,
|
||||||
RollingVwap, Rsi, SharpeRatio, Side, SignedVolume, Sma, Stc, SuperTrend, SuperTrendOutput,
|
ParkinsonVolatility, Ppo, Psar, RollingVwap, Rsi, SharpeRatio, Side, SignedVolume, Sma, Stc,
|
||||||
TdSequential, TdSequentialOutput, Trade, TradeImbalance, TtmSqueeze, TtmSqueezeOutput,
|
SuperTrend, SuperTrendOutput, TdSequential, TdSequentialOutput, Trade, TradeImbalance,
|
||||||
ValueArea, ValueAreaOutput, ValueAtRisk, Vwap, VwapStdDevBands, VwapStdDevBandsOutput,
|
TradeQuote, TtmSqueeze, TtmSqueezeOutput, ValueArea, ValueAreaOutput, ValueAtRisk, Vwap,
|
||||||
WaveTrend, YangZhangVolatility, T3,
|
VwapStdDevBands, VwapStdDevBandsOutput, WaveTrend, YangZhangVolatility, T3,
|
||||||
};
|
};
|
||||||
use wickra_data::csv::CandleReader;
|
use wickra_data::csv::CandleReader;
|
||||||
|
|
||||||
@@ -159,6 +159,28 @@ where
|
|||||||
group.finish();
|
group.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bench_tradequote_input<I, F, O>(c: &mut Criterion, name: &str, quotes: &[TradeQuote], make: F)
|
||||||
|
where
|
||||||
|
F: Fn() -> I,
|
||||||
|
I: Indicator<Input = TradeQuote, Output = O>,
|
||||||
|
{
|
||||||
|
let mut group = c.benchmark_group(name);
|
||||||
|
for &n in SIZES {
|
||||||
|
let n = n.min(quotes.len());
|
||||||
|
let series = "es[..n];
|
||||||
|
group.throughput(Throughput::Elements(n as u64));
|
||||||
|
group.bench_with_input(BenchmarkId::new("streaming", n), series, |b, quotes| {
|
||||||
|
b.iter(|| {
|
||||||
|
let mut ind = make();
|
||||||
|
for q in quotes {
|
||||||
|
black_box(ind.update(*q));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
fn bench_scalar_multi<I, F, O>(c: &mut Criterion, name: &str, prices: &[f64], make: F)
|
fn bench_scalar_multi<I, F, O>(c: &mut Criterion, name: &str, prices: &[f64], make: F)
|
||||||
where
|
where
|
||||||
F: Fn() -> I,
|
F: Fn() -> I,
|
||||||
@@ -332,6 +354,7 @@ fn benches(c: &mut Criterion) {
|
|||||||
bench_orderbook_input(c, "ob_imbalance_top1", &books, OrderBookImbalanceTop1::new);
|
bench_orderbook_input(c, "ob_imbalance_top1", &books, OrderBookImbalanceTop1::new);
|
||||||
bench_orderbook_input(c, "ob_imbalance_full", &books, OrderBookImbalanceFull::new);
|
bench_orderbook_input(c, "ob_imbalance_full", &books, OrderBookImbalanceFull::new);
|
||||||
bench_orderbook_input(c, "microprice", &books, Microprice::new);
|
bench_orderbook_input(c, "microprice", &books, Microprice::new);
|
||||||
|
bench_orderbook_input(c, "depth_slope", &books, DepthSlope::new);
|
||||||
|
|
||||||
// Synthesise a trade tape from candles: one trade per bar, sided by the
|
// Synthesise a trade tape from candles: one trade per bar, sided by the
|
||||||
// candle's direction. SignedVolume is the cheapest; TradeImbalance carries
|
// candle's direction. SignedVolume is the cheapest; TradeImbalance carries
|
||||||
@@ -351,6 +374,16 @@ fn benches(c: &mut Criterion) {
|
|||||||
bench_trade_input(c, "trade_imbalance", &trades, || {
|
bench_trade_input(c, "trade_imbalance", &trades, || {
|
||||||
TradeImbalance::new(50).unwrap()
|
TradeImbalance::new(50).unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Pair each synthetic trade with the candle close as the prevailing mid to
|
||||||
|
// exercise the price-impact family. EffectiveSpread is the stateless
|
||||||
|
// representative.
|
||||||
|
let quotes: Vec<TradeQuote> = trades
|
||||||
|
.iter()
|
||||||
|
.map(|trade| TradeQuote::new_unchecked(*trade, trade.price))
|
||||||
|
.collect();
|
||||||
|
bench_tradequote_input(c, "effective_spread", "es, EffectiveSpread::new);
|
||||||
|
bench_tradequote_input(c, "kyles_lambda", "es, || KylesLambda::new(50).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
criterion_group!(name = wickra_benches; config = Criterion::default(); targets = benches);
|
criterion_group!(name = wickra_benches; config = Criterion::default(); targets = benches);
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ test = false
|
|||||||
doc = false
|
doc = false
|
||||||
bench = false
|
bench = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "indicator_update_tradequote"
|
||||||
|
path = "fuzz_targets/indicator_update_tradequote.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "tick_aggregator"
|
name = "tick_aggregator"
|
||||||
path = "fuzz_targets/tick_aggregator.rs"
|
path = "fuzz_targets/tick_aggregator.rs"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use wickra_core::{
|
use wickra_core::{
|
||||||
BatchExt, Indicator, Level, Microprice, OrderBook, OrderBookImbalanceFull,
|
BatchExt, DepthSlope, Indicator, Level, Microprice, OrderBook, OrderBookImbalanceFull,
|
||||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, QuotedSpread,
|
OrderBookImbalanceTop1, OrderBookImbalanceTopN, QuotedSpread,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,4 +51,5 @@ fuzz_target!(|data: &[u8]| {
|
|||||||
drive(OrderBookImbalanceFull::new, &books);
|
drive(OrderBookImbalanceFull::new, &books);
|
||||||
drive(Microprice::new, &books);
|
drive(Microprice::new, &books);
|
||||||
drive(QuotedSpread::new, &books);
|
drive(QuotedSpread::new, &books);
|
||||||
|
drive(DepthSlope::new, &books);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#![no_main]
|
||||||
|
//! Fuzz price-impact `Indicator<Input = TradeQuote>` implementations with
|
||||||
|
//! arbitrary trade-quote tapes.
|
||||||
|
//!
|
||||||
|
//! Each iteration consumes a byte stream, interprets it as a sequence of `f64`
|
||||||
|
//! values (8 bytes each), and packs consecutive triples into `(price, size,
|
||||||
|
//! mid)` trade-quotes whose aggressor side alternates with the sign of the size
|
||||||
|
//! field. Trade-quotes are built with the `new_unchecked` constructors so the
|
||||||
|
//! fuzzer can explore degenerate values (non-finite, negative, zero mid) that
|
||||||
|
//! the validating constructors would reject — the indicators must never panic,
|
||||||
|
//! streaming or batched.
|
||||||
|
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
use wickra_core::{
|
||||||
|
BatchExt, EffectiveSpread, Indicator, KylesLambda, RealizedSpread, Side, Trade, TradeQuote,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn drive<I>(make: impl Fn() -> I, quotes: &[TradeQuote])
|
||||||
|
where
|
||||||
|
I: Indicator<Input = TradeQuote, Output = f64> + BatchExt,
|
||||||
|
{
|
||||||
|
let mut streaming = make();
|
||||||
|
for "e in quotes {
|
||||||
|
let _ = streaming.update(quote);
|
||||||
|
}
|
||||||
|
let _ = make().batch(quotes);
|
||||||
|
}
|
||||||
|
|
||||||
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
let floats: Vec<f64> = data
|
||||||
|
.chunks_exact(8)
|
||||||
|
.map(|c| f64::from_le_bytes(c.try_into().expect("8 bytes")))
|
||||||
|
.collect();
|
||||||
|
let quotes: Vec<TradeQuote> = floats
|
||||||
|
.chunks_exact(3)
|
||||||
|
.map(|c| {
|
||||||
|
let side = if c[1] >= 0.0 { Side::Buy } else { Side::Sell };
|
||||||
|
let trade = Trade::new_unchecked(c[0], c[1], side, 0);
|
||||||
|
TradeQuote::new_unchecked(trade, c[2])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
drive(EffectiveSpread::new, "es);
|
||||||
|
drive(|| RealizedSpread::new(5).unwrap(), "es);
|
||||||
|
drive(|| KylesLambda::new(5).unwrap(), "es);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user