Files
ftdt-quant-lab/tests/test_sim_reporter.py
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

89 lines
3.3 KiB
Python

"""
Tests for sim/reporter.py — PnL breakdown and stats.
"""
from sim.reporter import PnLReporter, PnLBreakdown, SimulationStats
class TestPnlReporter:
def test_initial_equity(self):
r = PnLReporter(initial_equity=10000.0)
assert r.net_pnl() == 0.0
def test_maker_fill_bid(self):
r = PnLReporter(initial_equity=10000.0)
size = 0.001
fee = size * 50000.0 * 0.0002 # 0.01
r.record_maker_fill(side="bid", price=50000.0, size=size, mid_price=50001.0, fee=fee)
bd = r.breakdown()
assert bd.spread_capture > 0 # bought below mid
assert bd.maker_fees < 0
def test_maker_fill_ask(self):
r = PnLReporter(initial_equity=10000.0)
size = 0.001
fee = size * 50001.0 * 0.0002
r.record_maker_fill(side="ask", price=50001.0, size=size, mid_price=50000.0, fee=fee)
bd = r.breakdown()
assert bd.spread_capture > 0 # sold above mid
def test_toxic_fill(self):
r = PnLReporter(initial_equity=10000.0)
size = 0.001
fee = size * 50000.0 * 0.0002
r.record_maker_fill(side="bid", price=50000.0, size=size, mid_price=50000.5, fee=fee, is_toxic=True)
bd = r.breakdown()
assert bd.adverse_selection_cost > 0
def test_inventory_tracking(self):
r = PnLReporter(initial_equity=10000.0)
r.record_maker_fill(side="bid", price=50000.0, size=0.002, mid_price=50001.0, fee=1.0)
assert r.position == 0.002
r.record_maker_fill(side="ask", price=50001.0, size=0.002, mid_price=50000.0, fee=1.0)
assert abs(r.position) < 1e-10
def test_equity_curve(self):
r = PnLReporter(initial_equity=10000.0)
r.record_equity_update(sim_time=1.0, mid_price=100000.0)
curve = r.equity_curve
assert len(curve) >= 2
assert curve[0]["v"] == 10000.0
def test_max_drawdown(self):
r = PnLReporter(initial_equity=10000.0)
r.record_maker_fill(side="bid", price=100000.0, size=0.001, mid_price=99900.0, fee=1.0) # loss
assert r.stats().max_drawdown > 0
def test_stats_includes_all_components(self):
r = PnLReporter(initial_equity=10000.0)
r.record_maker_fill(side="bid", price=50000.0, size=0.001, mid_price=50001.0, fee=10.0)
r.record_maker_fill(side="ask", price=50001.0, size=0.001, mid_price=50000.0, fee=10.0)
r.record_spread(1.5)
r.record_latency(25.0)
stats = r.stats()
assert stats.total_trades == 2
assert stats.pnl.spread_capture > 0
assert stats.pnl.net_pnl != 0
assert stats.avg_spread_bps > 0
def test_breakdown_all_fields(self):
r = PnLReporter(initial_equity=10000.0)
bd = r.breakdown()
assert bd.spread_capture == 0
assert bd.inventory_pnl == 0
assert bd.maker_fees == 0
assert bd.taker_fees == 0
assert bd.net_pnl == 0
def test_cancel_counting(self):
r = PnLReporter(initial_equity=10000.0)
r.record_cancel()
r.record_cancel()
assert r.stats().cancels == 2
def test_downtime(self):
r = PnLReporter(initial_equity=10000.0)
r.record_downtime(60.0)
r.record_equity_update(sim_time=100.0, mid_price=100000.0)
stats = r.stats()
assert stats.uptime_pct < 100.0