Files
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

70 lines
2.7 KiB
Python

"""
Tests for sim/maker.py — market-making quoting logic.
"""
from sim.maker import AvellanedaStoikovMaker, GridMaker, MakerConfig, Quote
class TestAvellanedaStoikovMaker:
def test_initial_sigma(self):
maker = AvellanedaStoikovMaker()
assert maker.sigma == 0.02 # default
def test_observe_updates_volatility(self):
maker = AvellanedaStoikovMaker()
for i in range(20):
maker.observe(100000.0 + i * 100)
assert maker.sigma > 0
def test_quote_no_inventory(self):
maker = AvellanedaStoikovMaker()
maker.observe(100000.0)
q = maker.quote(mid_price=100000.0, inventory=0.0, elapsed_hours=0.0)
assert q.bid > 0
assert q.ask > q.bid
assert q.spread_bps >= 0
def test_quote_short_inventory_skew(self):
maker = AvellanedaStoikovMaker(MakerConfig(gamma=0.5, k=1.0))
for i in range(50):
maker.observe(100000.0 + i * 50 * (1 if i % 2 == 0 else -1))
q_short = maker.quote(mid_price=100000.0, inventory=-0.003, elapsed_hours=0.0)
q_long = maker.quote(mid_price=100000.0, inventory=0.003, elapsed_hours=0.0)
assert q_short.reservation > q_long.reservation
def test_quote_long_inventory_skew(self):
maker = AvellanedaStoikovMaker()
maker.observe(100000.0)
q = maker.quote(mid_price=100000.0, inventory=0.004, elapsed_hours=0.1)
assert q.reservation < q.ask
def test_quote_with_skew(self):
maker = AvellanedaStoikovMaker()
maker.observe(100000.0)
q = maker.quote_with_skew(mid_price=100000.0, inventory=0.003, elapsed_hours=0.0, target_inventory=0.0)
assert q.ask_size > q.bid_size # long → bigger asks to reduce position
def test_elapsed_reduces_spread(self):
maker = AvellanedaStoikovMaker()
maker.observe(100000.0)
q_early = maker.quote(mid_price=100000.0, inventory=0.0, elapsed_hours=0.0)
q_late = maker.quote(mid_price=100000.0, inventory=0.0, elapsed_hours=0.9)
# Near session end → narrower spread (less time to recover)
assert q_late.spread_bps <= q_early.spread_bps + 1
class TestGridMaker:
def test_grid_levels(self):
gm = GridMaker(grid_levels=3, spacing_bps=10.0, size_per_level=0.001)
quotes = gm.quotes(100000.0)
assert len(quotes) == 6 # 3 bid + 3 ask levels
def test_symmetric(self):
gm = GridMaker(grid_levels=2, spacing_bps=50.0, size_per_level=0.001)
quotes = gm.quotes(100000.0)
bids = [q for q in quotes if q["side"] == "bid"]
asks = [q for q in quotes if q["side"] == "ask"]
assert len(bids) == 2
assert len(asks) == 2
for b, a in zip(bids[::-1], asks):
assert a["price"] > b["price"]