639dd4fb6d
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.
173 lines
5.6 KiB
Python
173 lines
5.6 KiB
Python
"""
|
|
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
|