Files
ftdt-quant-lab/sim/scenario.py
T
ramseshk 639dd4fb6d 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.
2026-08-07 14:39:59 +08:00

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