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]
|
||||
|
||||
### 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
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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>
|
||||
|
||||
[](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),
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
- **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).
|
||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||
@@ -135,7 +135,7 @@ python -m benchmarks.compare_libraries
|
||||
|
||||
## 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
|
||||
semantics tests. Each has a per-indicator deep dive (formula, parameters,
|
||||
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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) |
|
||||
|
||||
@@ -237,7 +237,7 @@ A Python live-trading example using the public `websockets` package lives at
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 227 indicators
|
||||
│ ├── wickra-core/ core engine + all 231 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
├── bindings/
|
||||
|
||||
@@ -926,6 +926,10 @@ test('order-book indicators reference values', () => {
|
||||
assert.equal(new wickra.Microprice().update([100], [1], [101], [3]), 100.25);
|
||||
// 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);
|
||||
// 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', () => {
|
||||
@@ -981,3 +985,81 @@ test('trade-flow rejects bad input', () => {
|
||||
assert.throws(() => new wickra.TradeImbalance(0));
|
||||
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
|
||||
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 declare class OrderBookImbalanceTopN {
|
||||
constructor(levels: number)
|
||||
@@ -2268,6 +2277,33 @@ export declare class TradeImbalance {
|
||||
isReady(): boolean
|
||||
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 declare class SharpeRatio {
|
||||
constructor(period: number, riskFree: number)
|
||||
|
||||
@@ -310,7 +310,7 @@ if (!nativeBinding) {
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA, MOM, CMO, DPO, StdDev, UlcerIndex, VerticalHorizontalFilter, ZScore, McGinleyDynamic, FRAMA, SuperSmoother, FisherTransform, Decycler, CenterOfGravity, CyberneticCycle, InstantaneousTrendline, EhlersStochastic, RVIVolatility, Variance, CoefficientOfVariation, Skewness, Kurtosis, StandardError, DetrendedStdDev, RSquared, MedianAbsoluteDeviation, 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.SMA = SMA
|
||||
@@ -519,10 +519,14 @@ module.exports.OrderBookImbalanceTop1 = OrderBookImbalanceTop1
|
||||
module.exports.OrderBookImbalanceFull = OrderBookImbalanceFull
|
||||
module.exports.Microprice = Microprice
|
||||
module.exports.QuotedSpread = QuotedSpread
|
||||
module.exports.DepthSlope = DepthSlope
|
||||
module.exports.OrderBookImbalanceTopN = OrderBookImbalanceTopN
|
||||
module.exports.SignedVolume = SignedVolume
|
||||
module.exports.CumulativeVolumeDelta = CumulativeVolumeDelta
|
||||
module.exports.TradeImbalance = TradeImbalance
|
||||
module.exports.EffectiveSpread = EffectiveSpread
|
||||
module.exports.RealizedSpread = RealizedSpread
|
||||
module.exports.KylesLambda = KylesLambda
|
||||
module.exports.SharpeRatio = SharpeRatio
|
||||
module.exports.SortinoRatio = SortinoRatio
|
||||
module.exports.CalmarRatio = CalmarRatio
|
||||
|
||||
@@ -8865,6 +8865,7 @@ node_ob_indicator!(
|
||||
);
|
||||
node_ob_indicator!(MicropriceNode, wc::Microprice, "Microprice");
|
||||
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.
|
||||
#[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 ==============================
|
||||
|
||||
// Risk metrics with fallible `new` (most need `period >= 2`), so each wrapper
|
||||
|
||||
@@ -246,10 +246,15 @@ from ._wickra import (
|
||||
OrderBookImbalanceFull,
|
||||
Microprice,
|
||||
QuotedSpread,
|
||||
DepthSlope,
|
||||
# Microstructure: trade flow
|
||||
SignedVolume,
|
||||
CumulativeVolumeDelta,
|
||||
TradeImbalance,
|
||||
# Microstructure: price impact
|
||||
EffectiveSpread,
|
||||
RealizedSpread,
|
||||
KylesLambda,
|
||||
# Risk / Performance
|
||||
SharpeRatio,
|
||||
SortinoRatio,
|
||||
@@ -493,10 +498,15 @@ __all__ = [
|
||||
"OrderBookImbalanceFull",
|
||||
"Microprice",
|
||||
"QuotedSpread",
|
||||
"DepthSlope",
|
||||
# Microstructure: trade flow
|
||||
"SignedVolume",
|
||||
"CumulativeVolumeDelta",
|
||||
"TradeImbalance",
|
||||
# Microstructure: price impact
|
||||
"EffectiveSpread",
|
||||
"RealizedSpread",
|
||||
"KylesLambda",
|
||||
# Risk / Performance
|
||||
"SharpeRatio",
|
||||
"SortinoRatio",
|
||||
|
||||
@@ -11714,6 +11714,7 @@ py_ob_indicator!(
|
||||
);
|
||||
py_ob_indicator!(PyMicroprice, wc::Microprice, "Microprice");
|
||||
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.
|
||||
#[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 ==============================
|
||||
|
||||
#[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::<PyMicroprice>()?;
|
||||
m.add_class::<PyQuotedSpread>()?;
|
||||
m.add_class::<PyDepthSlope>()?;
|
||||
// Microstructure: trade flow.
|
||||
m.add_class::<PySignedVolume>()?;
|
||||
m.add_class::<PyCumulativeVolumeDelta>()?;
|
||||
m.add_class::<PyTradeImbalance>()?;
|
||||
// Microstructure: price impact.
|
||||
m.add_class::<PyEffectiveSpread>()?;
|
||||
m.add_class::<PyRealizedSpread>()?;
|
||||
m.add_class::<PyKylesLambda>()?;
|
||||
// Family 15: Risk / Performance metrics.
|
||||
m.add_class::<PySharpeRatio>()?;
|
||||
m.add_class::<PySortinoRatio>()?;
|
||||
|
||||
@@ -211,3 +211,23 @@ def test_trade_non_positive_price_raises():
|
||||
def test_trade_batch_unequal_lengths_raise():
|
||||
with pytest.raises(ValueError):
|
||||
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)
|
||||
|
||||
|
||||
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():
|
||||
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)
|
||||
@@ -889,3 +899,39 @@ def test_trade_imbalance_reference_value():
|
||||
assert ti.update(100.0, 3.0, True) is None # warming up
|
||||
# Window full: buyVol 3, sellVol 1 -> (3 - 1) / 4 = 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.Microprice(),
|
||||
ta.QuotedSpread(),
|
||||
ta.DepthSlope(),
|
||||
]:
|
||||
assert ind.warmup_period() == 1
|
||||
assert not ind.is_ready()
|
||||
@@ -172,3 +173,37 @@ def test_trade_imbalance_lifecycle_and_repr():
|
||||
ti.reset()
|
||||
assert not ti.is_ready()
|
||||
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.Microprice,
|
||||
ta.QuotedSpread,
|
||||
ta.DepthSlope,
|
||||
):
|
||||
batch = make().batch(snaps)
|
||||
streamer = make()
|
||||
@@ -1918,3 +1919,23 @@ def test_tradeflow_indicators_streaming_equals_batch():
|
||||
)
|
||||
assert batch.shape == (n,)
|
||||
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.Microprice(),
|
||||
ta.QuotedSpread(),
|
||||
ta.DepthSlope(),
|
||||
]
|
||||
for ind in indicators:
|
||||
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)
|
||||
assert out.shape == (6,)
|
||||
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,
|
||||
)
|
||||
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!(WasmQuotedSpread, wc::QuotedSpread, QuotedSpread);
|
||||
wasm_ob_indicator!(WasmDepthSlope, wc::DepthSlope, DepthSlope);
|
||||
|
||||
// Top-N imbalance carries a `levels` parameter, so it is hand-written.
|
||||
#[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)]
|
||||
mod tests {
|
||||
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 demand_index;
|
||||
mod demark_pivots;
|
||||
mod depth_slope;
|
||||
mod detrended_std_dev;
|
||||
mod doji;
|
||||
mod donchian;
|
||||
@@ -62,6 +63,7 @@ mod double_bollinger;
|
||||
mod dpo;
|
||||
mod drawdown_duration;
|
||||
mod ease_of_movement;
|
||||
mod effective_spread;
|
||||
mod ehlers_stochastic;
|
||||
mod elder_impulse;
|
||||
mod ema;
|
||||
@@ -100,6 +102,7 @@ mod keltner;
|
||||
mod kst;
|
||||
mod kurtosis;
|
||||
mod kvo;
|
||||
mod kyles_lambda;
|
||||
mod laguerre_rsi;
|
||||
mod lead_lag_cross_correlation;
|
||||
mod linreg;
|
||||
@@ -144,6 +147,7 @@ mod psar;
|
||||
mod pvi;
|
||||
mod quoted_spread;
|
||||
mod r_squared;
|
||||
mod realized_spread;
|
||||
mod recovery_factor;
|
||||
mod relative_strength_ab;
|
||||
mod renko_trailing_stop;
|
||||
@@ -281,6 +285,7 @@ pub use decycler_oscillator::DecyclerOscillator;
|
||||
pub use dema::Dema;
|
||||
pub use demand_index::DemandIndex;
|
||||
pub use demark_pivots::{DemarkPivots, DemarkPivotsOutput};
|
||||
pub use depth_slope::DepthSlope;
|
||||
pub use detrended_std_dev::DetrendedStdDev;
|
||||
pub use doji::Doji;
|
||||
pub use donchian::{Donchian, DonchianOutput};
|
||||
@@ -289,6 +294,7 @@ pub use double_bollinger::{DoubleBollinger, DoubleBollingerOutput};
|
||||
pub use dpo::Dpo;
|
||||
pub use drawdown_duration::DrawdownDuration;
|
||||
pub use ease_of_movement::EaseOfMovement;
|
||||
pub use effective_spread::EffectiveSpread;
|
||||
pub use ehlers_stochastic::EhlersStochastic;
|
||||
pub use elder_impulse::ElderImpulse;
|
||||
pub use ema::Ema;
|
||||
@@ -327,6 +333,7 @@ pub use keltner::{Keltner, KeltnerOutput};
|
||||
pub use kst::{Kst, KstOutput};
|
||||
pub use kurtosis::Kurtosis;
|
||||
pub use kvo::Kvo;
|
||||
pub use kyles_lambda::KylesLambda;
|
||||
pub use laguerre_rsi::LaguerreRsi;
|
||||
pub use lead_lag_cross_correlation::{LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput};
|
||||
pub use linreg::LinearRegression;
|
||||
@@ -371,6 +378,7 @@ pub use psar::Psar;
|
||||
pub use pvi::Pvi;
|
||||
pub use quoted_spread::QuotedSpread;
|
||||
pub use r_squared::RSquared;
|
||||
pub use realized_spread::RealizedSpread;
|
||||
pub use recovery_factor::RecoveryFactor;
|
||||
pub use relative_strength_ab::{RelativeStrengthAB, RelativeStrengthOutput};
|
||||
pub use renko_trailing_stop::RenkoTrailingStop;
|
||||
@@ -731,9 +739,13 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
|
||||
"OrderBookImbalanceFull",
|
||||
"Microprice",
|
||||
"QuotedSpread",
|
||||
"DepthSlope",
|
||||
"SignedVolume",
|
||||
"CumulativeVolumeDelta",
|
||||
"TradeImbalance",
|
||||
"EffectiveSpread",
|
||||
"RealizedSpread",
|
||||
"KylesLambda",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -790,6 +802,6 @@ mod family_tests {
|
||||
// the actual indicator count is the early-warning signal that an
|
||||
// indicator was added without being assigned a family.
|
||||
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
|
||||
assert_eq!(total, 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,
|
||||
CoefficientOfVariation, Cointegration, CointegrationOutput, ConditionalValueAtRisk, ConnorsRsi,
|
||||
Coppock, CumulativeVolumeDelta, CyberneticCycle, Decycler, DecyclerOscillator, Dema,
|
||||
DemandIndex, DemarkPivots, DemarkPivotsOutput, DetrendedStdDev, Doji, Donchian, DonchianOutput,
|
||||
DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, Dpo,
|
||||
DrawdownDuration, EaseOfMovement, EhlersStochastic, ElderImpulse, Ema,
|
||||
DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope, DetrendedStdDev, Doji, Donchian,
|
||||
DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput, Dpo,
|
||||
DrawdownDuration, EaseOfMovement, EffectiveSpread, EhlersStochastic, ElderImpulse, Ema,
|
||||
EmpiricalModeDecomposition, Engulfing, Evwma, Fama, FibonacciPivots, FibonacciPivotsOutput,
|
||||
FisherTransform, ForceIndex, FractalChaosBands, FractalChaosBandsOutput, Frama, GainLossRatio,
|
||||
GarmanKlassVolatility, Hammer, HangingMan, Harami, HeikinAshi, HeikinAshiOutput, HiLoActivator,
|
||||
HilbertDominantCycle, HistoricalVolatility, Hma, HurstChannel, HurstChannelOutput,
|
||||
HurstExponent, Ichimoku, IchimokuOutput, Inertia, InformationRatio, InitialBalance,
|
||||
InitialBalanceOutput, InstantaneousTrendline, InverseFisherTransform, InvertedHammer, Jma,
|
||||
Kama, KellyCriterion, Keltner, KeltnerOutput, Kst, KstOutput, Kurtosis, Kvo, LaguerreRsi,
|
||||
LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle, LinRegChannel,
|
||||
LinRegChannelOutput, LinRegSlope, LinearRegression, MaEnvelope, MaEnvelopeOutput,
|
||||
MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex, Marubozu, MassIndex,
|
||||
MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi, Microprice, Mom,
|
||||
MorningEveningStar, Natr, Nvi, Obv, OmegaRatio, OpeningRange, OpeningRangeOutput,
|
||||
OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN, PainIndex,
|
||||
PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentB,
|
||||
Kama, KellyCriterion, Keltner, KeltnerOutput, Kst, KstOutput, Kurtosis, Kvo, KylesLambda,
|
||||
LaguerreRsi, LeadLagCrossCorrelation, LeadLagCrossCorrelationOutput, LinRegAngle,
|
||||
LinRegChannel, LinRegChannelOutput, LinRegSlope, LinearRegression, MaEnvelope,
|
||||
MaEnvelopeOutput, MacdIndicator, MacdOutput, Mama, MamaOutput, MarketFacilitationIndex,
|
||||
Marubozu, MassIndex, MaxDrawdown, McGinleyDynamic, MedianAbsoluteDeviation, MedianPrice, Mfi,
|
||||
Microprice, Mom, MorningEveningStar, Natr, Nvi, Obv, OmegaRatio, OpeningRange,
|
||||
OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN,
|
||||
PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility, PearsonCorrelation, PercentB,
|
||||
PercentageTrailingStop, Pgo, PiercingDarkCloud, Pmo, Ppo, ProfitFactor, Psar, Pvi,
|
||||
QuotedSpread, RSquared, RecoveryFactor, RelativeStrengthAB, RelativeStrengthOutput,
|
||||
RenkoTrailingStop, Roc, RogersSatchellVolatility, RollingVwap, RoofingFilter, Rsi, Rvi,
|
||||
RviVolatility, Rwi, RwiOutput, SharpeRatio, ShootingStar, SignedVolume, SineWave, Skewness,
|
||||
Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation, SpinningTop, StandardError,
|
||||
StandardErrorBands, StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev,
|
||||
StepTrailingStop, StochRsi, Stochastic, StochasticOutput, SuperSmoother, SuperTrend,
|
||||
SuperTrendOutput, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdLinesOutput,
|
||||
TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei, TdRiskLevel,
|
||||
TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema, ThreeInside, ThreeOutside,
|
||||
ThreeSoldiersOrCrows, Tii, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange, Tsi, Tsv,
|
||||
TtmSqueeze, TtmSqueezeOutput, Tweezer, TypicalPrice, UlcerIndex, UltimateOscillator, ValueArea,
|
||||
ValueAreaOutput, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VoltyStop,
|
||||
QuotedSpread, RSquared, RealizedSpread, RecoveryFactor, RelativeStrengthAB,
|
||||
RelativeStrengthOutput, RenkoTrailingStop, Roc, RogersSatchellVolatility, RollingVwap,
|
||||
RoofingFilter, Rsi, Rvi, RviVolatility, Rwi, RwiOutput, SharpeRatio, ShootingStar,
|
||||
SignedVolume, SineWave, Skewness, Sma, Smi, Smma, SortinoRatio, SpearmanCorrelation,
|
||||
SpinningTop, StandardError, StandardErrorBands, StandardErrorBandsOutput, StarcBands,
|
||||
StarcBandsOutput, Stc, StdDev, StepTrailingStop, StochRsi, Stochastic, StochasticOutput,
|
||||
SuperSmoother, SuperTrend, SuperTrendOutput, TdCombo, TdCountdown, TdDeMarker, TdDifferential,
|
||||
TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection, TdRangeProjectionOutput, TdRei,
|
||||
TdRiskLevel, TdRiskLevelOutput, TdSequential, TdSequentialOutput, TdSetup, Tema, ThreeInside,
|
||||
ThreeOutside, ThreeSoldiersOrCrows, Tii, TradeImbalance, TreynorRatio, Trima, Trix, TrueRange,
|
||||
Tsi, Tsv, TtmSqueeze, TtmSqueezeOutput, Tweezer, TypicalPrice, UlcerIndex, UltimateOscillator,
|
||||
ValueArea, ValueAreaOutput, ValueAtRisk, Variance, VerticalHorizontalFilter, Vidya, VoltyStop,
|
||||
VolumeOscillator, VolumePriceTrend, Vortex, VortexOutput, Vwap, VwapStdDevBands,
|
||||
VwapStdDevBandsOutput, Vwma, Vzo, WaveTrend, WaveTrendOutput, WeightedClose, WilliamsFractals,
|
||||
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 wickra::{
|
||||
Adx, Atr, Autocorrelation, BatchExt, BollingerBands, BollingerOutput, CalmarRatio, Candle, Cci,
|
||||
ClassicPivots, ConnorsRsi, Ema, EmpiricalModeDecomposition, Engulfing, Frama,
|
||||
HilbertDominantCycle, HurstExponent, Ichimoku, IchimokuOutput, Indicator, Jma, Level,
|
||||
LinearRegression, MacdIndicator, MacdOutput, Mama, MamaOutput, MaxDrawdown, Microprice, Obv,
|
||||
OrderBook, OrderBookImbalanceFull, OrderBookImbalanceTop1, ParkinsonVolatility, Ppo, Psar,
|
||||
RollingVwap, Rsi, SharpeRatio, Side, SignedVolume, Sma, Stc, SuperTrend, SuperTrendOutput,
|
||||
TdSequential, TdSequentialOutput, Trade, TradeImbalance, TtmSqueeze, TtmSqueezeOutput,
|
||||
ValueArea, ValueAreaOutput, ValueAtRisk, Vwap, VwapStdDevBands, VwapStdDevBandsOutput,
|
||||
WaveTrend, YangZhangVolatility, T3,
|
||||
ClassicPivots, ConnorsRsi, DepthSlope, EffectiveSpread, Ema, EmpiricalModeDecomposition,
|
||||
Engulfing, Frama, HilbertDominantCycle, HurstExponent, Ichimoku, IchimokuOutput, Indicator,
|
||||
Jma, KylesLambda, Level, LinearRegression, MacdIndicator, MacdOutput, Mama, MamaOutput,
|
||||
MaxDrawdown, Microprice, Obv, OrderBook, OrderBookImbalanceFull, OrderBookImbalanceTop1,
|
||||
ParkinsonVolatility, Ppo, Psar, RollingVwap, Rsi, SharpeRatio, Side, SignedVolume, Sma, Stc,
|
||||
SuperTrend, SuperTrendOutput, TdSequential, TdSequentialOutput, Trade, TradeImbalance,
|
||||
TradeQuote, TtmSqueeze, TtmSqueezeOutput, ValueArea, ValueAreaOutput, ValueAtRisk, Vwap,
|
||||
VwapStdDevBands, VwapStdDevBandsOutput, WaveTrend, YangZhangVolatility, T3,
|
||||
};
|
||||
use wickra_data::csv::CandleReader;
|
||||
|
||||
@@ -159,6 +159,28 @@ where
|
||||
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)
|
||||
where
|
||||
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_full", &books, OrderBookImbalanceFull::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
|
||||
// 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, || {
|
||||
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);
|
||||
|
||||
@@ -66,6 +66,13 @@ test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "indicator_update_tradequote"
|
||||
path = "fuzz_targets/indicator_update_tradequote.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "tick_aggregator"
|
||||
path = "fuzz_targets/tick_aggregator.rs"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use wickra_core::{
|
||||
BatchExt, Indicator, Level, Microprice, OrderBook, OrderBookImbalanceFull,
|
||||
BatchExt, DepthSlope, Indicator, Level, Microprice, OrderBook, OrderBookImbalanceFull,
|
||||
OrderBookImbalanceTop1, OrderBookImbalanceTopN, QuotedSpread,
|
||||
};
|
||||
|
||||
@@ -51,4 +51,5 @@ fuzz_target!(|data: &[u8]| {
|
||||
drive(OrderBookImbalanceFull::new, &books);
|
||||
drive(Microprice::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