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.
89 lines
3.3 KiB
Python
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
|