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.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Tests for sim/queue.py — queue position model.
|
||||
"""
|
||||
from sim.queue import QueueModel, QueuePosition, fill_probability
|
||||
|
||||
|
||||
class TestQueueModel:
|
||||
def test_place_bid(self):
|
||||
qm = QueueModel()
|
||||
oid = qm.place_order("bid", 50000.0, 0.01, sim_time=100.0)
|
||||
tob = qm.top_of_book()
|
||||
assert tob["best_bid"] == 50000.0
|
||||
assert tob["best_ask"] == 0 # no asks placed
|
||||
|
||||
def test_queue_position_is_front(self):
|
||||
qm = QueueModel()
|
||||
oid = qm.place_order("ask", 50002.0, 0.01, sim_time=100.0)
|
||||
qp = qm.queue_position("ask", 50002.0, oid)
|
||||
assert qp is not None
|
||||
assert qp.position == 0
|
||||
assert qp.is_front
|
||||
|
||||
def test_queue_position_behind_others(self):
|
||||
qm = QueueModel()
|
||||
qm.place_order("bid", 50000.0, 0.01, sim_time=99.0)
|
||||
oid2 = qm.place_order("bid", 50000.0, 0.01, sim_time=100.0)
|
||||
qp = qm.queue_position("bid", 50000.0, oid2)
|
||||
assert qp is not None
|
||||
assert qp.position == 1 # behind first order
|
||||
|
||||
def test_cancel_order(self):
|
||||
qm = QueueModel()
|
||||
oid = qm.place_order("bid", 50000.0, 0.01, sim_time=100.0)
|
||||
filled = qm.cancel_order(oid, sim_time=105.0)
|
||||
assert filled == 0.0
|
||||
assert qm.order_status(oid)["status"] == "cancelled"
|
||||
assert len(qm.active_orders()) == 0
|
||||
|
||||
def test_trade_eats_ask(self):
|
||||
qm = QueueModel()
|
||||
oid = qm.place_order("ask", 50002.0, 0.01, sim_time=100.0)
|
||||
fills = qm.process_trade("buy", 50002.0, 0.01, sim_time=101.0)
|
||||
assert len(fills) == 1
|
||||
assert fills[0]["size"] == 0.01
|
||||
|
||||
def test_trade_eats_bid(self):
|
||||
qm = QueueModel()
|
||||
oid = qm.place_order("bid", 50000.0, 0.02, sim_time=100.0)
|
||||
fills = qm.process_trade("sell", 50000.0, 0.01, sim_time=101.0)
|
||||
assert fills[0]["size"] == 0.01
|
||||
assert qm.order_status(oid)["filled"] == 0.01
|
||||
|
||||
def test_partial_fill_remaining(self):
|
||||
qm = QueueModel()
|
||||
oid = qm.place_order("bid", 50000.0, 0.03, sim_time=100.0)
|
||||
qm.process_trade("sell", 50000.0, 0.01, sim_time=101.0)
|
||||
status = qm.order_status(oid)
|
||||
assert status["filled"] == 0.01
|
||||
assert status["status"] == "active"
|
||||
|
||||
def test_fully_filled_status(self):
|
||||
qm = QueueModel()
|
||||
oid = qm.place_order("bid", 50000.0, 0.01, sim_time=100.0)
|
||||
qm.process_trade("sell", 50000.0, 0.01, sim_time=101.0)
|
||||
assert qm.order_status(oid)["status"] == "filled"
|
||||
|
||||
def test_trade_crosses_spread(self):
|
||||
qm = QueueModel()
|
||||
qm.place_order("ask", 50002.0, 0.01, sim_time=100.0)
|
||||
qm.place_order("ask", 50003.0, 0.01, sim_time=100.0)
|
||||
fills = qm.process_trade("buy", 50003.0, 0.02, sim_time=101.0)
|
||||
assert len(fills) == 2
|
||||
|
||||
def test_top_of_book(self):
|
||||
qm = QueueModel()
|
||||
qm.place_order("bid", 50000.0, 0.01, sim_time=100.0)
|
||||
qm.place_order("ask", 50002.0, 0.02, sim_time=100.0)
|
||||
tob = qm.top_of_book()
|
||||
assert tob["best_bid"] == 50000.0
|
||||
assert tob["best_ask"] == 50002.0
|
||||
assert tob["spread"] == 2.0
|
||||
|
||||
|
||||
class TestFillProbability:
|
||||
def test_front_of_queue(self):
|
||||
result = fill_probability(queue_pos=0, total_queue_depth=1.0, order_size=0.01, arrival_rate=1.0, time_horizon=1.0)
|
||||
assert result["fill_probability"] > 0.5
|
||||
|
||||
def test_deep_in_queue(self):
|
||||
result = fill_probability(queue_pos=5, total_queue_depth=10.0, order_size=0.01, arrival_rate=1.0, time_horizon=1.0)
|
||||
assert result["fill_probability"] < 0.5
|
||||
|
||||
def test_no_arrivals_zero_prob(self):
|
||||
result = fill_probability(queue_pos=0, total_queue_depth=1.0, order_size=0.01, arrival_rate=0.0, time_horizon=1.0)
|
||||
assert result["fill_probability"] == 0.0
|
||||
Reference in New Issue
Block a user