feat: Phase 3 — event-driven market-making simulator + 53 tests
New sim/ module — 7 files + init, replays stored L2/trade data
through a realistic market-making simulation:
sim/engine.py (SimulationEngine):
Event-driven core — processes L2 updates, trades, mark prices
sequentially. Orchestrates queue model, maker quotes, fill sim,
constraints, scenarios. Supports periodic re-quoting and
stale order cancellation.
sim/queue.py (QueueModel):
Price-time FIFO queue per price level. Tracks where maker orders
sit in queue. Simulates order eating by aggressor trades.
fill_probability() — Poisson thinning model for fill odds.
sim/maker.py:
AvellanedaStoikovMaker — stochastic control quoting with
aeta, k, tau parameters. Reservation price based on inventory.
quote() and quote_with_skew() with configurable inventory tilt.
GridMaker — evenly-spaced grid quoting at N levels.
sim/fills.py:
FillSimulator — partial fills, adverse selection probability,
cancel latency (gaussian RTT). FillEvent/CancelEvent tracking.
adverse_selection_intensity() — measures post-fill price moves.
sim/constraints.py:
InventoryConstraint — long/short/net/gross position limits.
FundingConstraint — hourly funding cost estimation.
FeeSchedule — maker/taker fee calculation.
LiquidationRisk — liquidation price and safety distance.
CircuitBreaker — PnL, trade count, toxic rate, slippage trips.
ConstraintManager — unified pre-trade constraint check.
sim/scenario.py:
ScenarioEngine — randomized exchange downtimes, latency spikes,
volatility bursts. State query per sim_time for spread/trade-rate.
sim/reporter.py:
PnLReporter — component-level PnL breakdown:
spread_capture, inventory_pnl, fees, funding, adverse_selection.
SimulationStats — trade counts, fill rates, drawdown, sharpe.
Equity curve tracking and max drawdown computation.
53 new tests across 4 files (all pass):
test_sim_queue.py (12) — order placement, FIFO, fills, cancels
test_sim_maker.py (9) — A-S quotes, inventory skew, grid maker
test_sim_constraints.py (14) — limits, funding, fees, liquidation, breakers
test_sim_reporter.py (12) — PnL components, equity curve, stats
test_sim_engine.py (6) — full engine integration
Total test suite: 134 tests, all passing.
This commit is contained in:
+172
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Scenario simulation: exchange downtime, volatility bursts, regime switches.
|
||||
|
||||
Models realistic exchange behaviors that affect market-making performance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExchangeDowntime:
|
||||
"""Scheduled or unscheduled exchange outage."""
|
||||
|
||||
start_time: float
|
||||
end_time: float
|
||||
reason: str = "scheduled_maintenance"
|
||||
|
||||
def is_active(self, sim_time: float) -> bool:
|
||||
return self.start_time <= sim_time < self.end_time
|
||||
|
||||
@property
|
||||
def duration_seconds(self) -> float:
|
||||
return self.end_time - self.start_time
|
||||
|
||||
|
||||
@dataclass
|
||||
class LatencySpike:
|
||||
"""Temporary latency increase."""
|
||||
|
||||
start_time: float
|
||||
end_time: float
|
||||
multiplier: float = 5.0 # 5x normal latency
|
||||
|
||||
def is_active(self, sim_time: float) -> bool:
|
||||
return self.start_time <= sim_time < self.end_time
|
||||
|
||||
|
||||
@dataclass
|
||||
class VolatilityBurst:
|
||||
"""Sudden increase in volatility."""
|
||||
|
||||
start_time: float
|
||||
end_time: float
|
||||
vol_multiplier: float = 3.0
|
||||
|
||||
def is_active(self, sim_time: float) -> bool:
|
||||
return self.start_time <= sim_time < self.end_time
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScenarioConfig:
|
||||
"""Configuration for simulation scenarios."""
|
||||
|
||||
duration_seconds: float = 3600.0 # 1 hour default
|
||||
seed: int | None = None
|
||||
|
||||
# Exchange behavior
|
||||
downtime_prob: float = 0.0 # probability of a downtime event
|
||||
downtime_duration_ms: int = 30000 # 30s typical
|
||||
latency_spike_prob: float = 0.05
|
||||
latency_spike_ms: int = 5000
|
||||
latency_multiplier: float = 5.0
|
||||
|
||||
# Market behavior
|
||||
volatility_burst_prob: float = 0.02
|
||||
volatility_burst_ms: int = 60000
|
||||
vol_multiplier: float = 3.0
|
||||
|
||||
# Trade intensity
|
||||
base_trade_rate: float = 1.0 # trades per second
|
||||
burst_trade_rate: float = 5.0 # trades per second during bursts
|
||||
|
||||
# Spread behavior
|
||||
base_spread_bps: float = 1.5
|
||||
wide_spread_bps: float = 15.0
|
||||
|
||||
|
||||
class ScenarioEngine:
|
||||
"""Generates and manages simulation scenarios."""
|
||||
|
||||
def __init__(self, config: ScenarioConfig | None = None, seed: int | None = None):
|
||||
self._cfg = config or ScenarioConfig()
|
||||
self._rng = random.Random(seed or self._cfg.seed)
|
||||
self._downtimes: list[ExchangeDowntime] = []
|
||||
self._latency_spikes: list[LatencySpike] = []
|
||||
self._volatility_bursts: list[VolatilityBurst] = []
|
||||
|
||||
def generate(self, duration: float | None = None):
|
||||
"""Pre-generate random scenarios for the simulation duration."""
|
||||
d = duration or self._cfg.duration_seconds
|
||||
self._downtimes.clear()
|
||||
self._latency_spikes.clear()
|
||||
self._volatility_bursts.clear()
|
||||
|
||||
t = 0.0
|
||||
while t < d:
|
||||
t += self._rng.expovariate(1.0 / (d / 100))
|
||||
|
||||
if self._rng.random() < self._cfg.downtime_prob:
|
||||
dur = self._rng.uniform(self._cfg.downtime_duration_ms / 1000 * 0.5,
|
||||
self._cfg.downtime_duration_ms / 1000 * 2)
|
||||
self._downtimes.append(ExchangeDowntime(t, t + dur))
|
||||
|
||||
if self._rng.random() < self._cfg.latency_spike_prob:
|
||||
dur = self._rng.uniform(self._cfg.latency_spike_ms / 1000 * 0.5,
|
||||
self._cfg.latency_spike_ms / 1000 * 2)
|
||||
self._latency_spikes.append(
|
||||
LatencySpike(t, t + dur, self._cfg.latency_multiplier)
|
||||
)
|
||||
|
||||
if self._rng.random() < self._cfg.volatility_burst_prob:
|
||||
dur = self._rng.uniform(self._cfg.volatility_burst_ms / 1000 * 0.5,
|
||||
self._cfg.volatility_burst_ms / 1000 * 2)
|
||||
self._volatility_bursts.append(
|
||||
VolatilityBurst(t, t + dur, self._cfg.vol_multiplier)
|
||||
)
|
||||
|
||||
def is_exchange_up(self, sim_time: float) -> bool:
|
||||
return not any(d.is_active(sim_time) for d in self._downtimes)
|
||||
|
||||
def current_latency_multiplier(self, sim_time: float) -> float:
|
||||
for spike in self._latency_spikes:
|
||||
if spike.is_active(sim_time):
|
||||
return spike.multiplier
|
||||
return 1.0
|
||||
|
||||
def current_vol_multiplier(self, sim_time: float) -> float:
|
||||
for burst in self._volatility_bursts:
|
||||
if burst.is_active(sim_time):
|
||||
return burst.vol_multiplier
|
||||
return 1.0
|
||||
|
||||
def current_trade_rate(self, sim_time: float) -> float:
|
||||
if self.current_vol_multiplier(sim_time) > 2.0:
|
||||
return self._cfg.burst_trade_rate
|
||||
return self._cfg.base_trade_rate
|
||||
|
||||
def current_spread_bps(self, sim_time: float) -> float:
|
||||
if self.current_vol_multiplier(sim_time) > 2.0:
|
||||
return self._cfg.wide_spread_bps
|
||||
return self._cfg.base_spread_bps
|
||||
|
||||
def state(self, sim_time: float) -> dict:
|
||||
return {
|
||||
"exchange_up": self.is_exchange_up(sim_time),
|
||||
"latency_multiplier": self.current_latency_multiplier(sim_time),
|
||||
"vol_multiplier": self.current_vol_multiplier(sim_time),
|
||||
"trade_rate": self.current_trade_rate(sim_time),
|
||||
"spread_bps": self.current_spread_bps(sim_time),
|
||||
}
|
||||
|
||||
def active_downtime(self, sim_time: float) -> Optional[ExchangeDowntime]:
|
||||
for d in self._downtimes:
|
||||
if d.is_active(sim_time):
|
||||
return d
|
||||
return None
|
||||
|
||||
@property
|
||||
def downtimes(self) -> list[ExchangeDowntime]:
|
||||
return self._downtimes
|
||||
|
||||
@property
|
||||
def latency_spikes(self) -> list[LatencySpike]:
|
||||
return self._latency_spikes
|
||||
|
||||
@property
|
||||
def volatility_bursts(self) -> list[VolatilityBurst]:
|
||||
return self._volatility_bursts
|
||||
Reference in New Issue
Block a user