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,96 @@
|
||||
"""
|
||||
Tests for sim/constraints.py — inventory, funding, fees, liquidation, circuit breakers.
|
||||
"""
|
||||
from sim.constraints import (
|
||||
InventoryConstraint,
|
||||
FundingConstraint,
|
||||
FeeSchedule,
|
||||
LiquidationRisk,
|
||||
CircuitBreaker,
|
||||
ConstraintManager,
|
||||
)
|
||||
|
||||
|
||||
class TestInventoryConstraint:
|
||||
def test_within_limits(self):
|
||||
ic = InventoryConstraint(max_long=0.01, max_short=0.01)
|
||||
result = ic.check(0.005, 0.003)
|
||||
assert result["long_ok"]
|
||||
assert result["short_ok"]
|
||||
assert result["net_ok"]
|
||||
|
||||
def test_long_exceeded(self):
|
||||
ic = InventoryConstraint(max_long=0.005)
|
||||
result = ic.check(0.006, 0.001)
|
||||
assert not result["long_ok"]
|
||||
|
||||
|
||||
class TestFundingConstraint:
|
||||
def test_cost_positive_funding(self):
|
||||
fc = FundingConstraint(funding_rate_hourly=0.0001)
|
||||
cost = fc.cost_per_hour(position=1.0, mark_price=100000.0)
|
||||
assert cost == 10.0 # 1 * 100000 * 0.0001 = 10
|
||||
|
||||
def test_annualized(self):
|
||||
fc = FundingConstraint(funding_rate_hourly=0.0001)
|
||||
ann = fc.annualized_cost_pct(0.0001)
|
||||
assert ann == 0.0001 * 365 * 24 * 100
|
||||
|
||||
|
||||
class TestFeeSchedule:
|
||||
def test_maker_fee(self):
|
||||
fs = FeeSchedule(maker_fee_pct=0.0002)
|
||||
assert fs.maker_fee(100000.0) == 20.0
|
||||
|
||||
def test_taker_fee(self):
|
||||
fs = FeeSchedule(taker_fee_pct=0.0005)
|
||||
assert fs.taker_fee(100000.0) == 50.0
|
||||
|
||||
|
||||
class TestLiquidationRisk:
|
||||
def test_liquidation_price_long(self):
|
||||
lr = LiquidationRisk(maintenance_margin_pct=0.03, initial_margin_pct=0.05)
|
||||
liq = lr.liquidation_price(entry_price=100000.0, size=1.0, position_side="long", wallet_balance=10000.0)
|
||||
assert 0 < liq < 100000.0 # below entry
|
||||
|
||||
def test_liquidation_price_short(self):
|
||||
lr = LiquidationRisk(maintenance_margin_pct=0.03, initial_margin_pct=0.05)
|
||||
liq = lr.liquidation_price(entry_price=100000.0, size=1.0, position_side="short", wallet_balance=10000.0)
|
||||
assert liq > 100000.0 # above entry
|
||||
|
||||
def test_safe_distance(self):
|
||||
lr = LiquidationRisk()
|
||||
assert lr.is_safe(5.0, threshold_pct=2.0)
|
||||
assert not lr.is_safe(1.0, threshold_pct=2.0)
|
||||
|
||||
|
||||
class TestCircuitBreaker:
|
||||
def test_no_trip(self):
|
||||
cb = CircuitBreaker()
|
||||
result = cb.evaluate({"pnl_pct": -1.0, "daily_trades": 10, "toxic_rate": 0.1, "api_errors": 0})
|
||||
assert not result["tripped"]
|
||||
|
||||
def test_drawdown_trip(self):
|
||||
cb = CircuitBreaker(max_drawdown_pct=-5.0)
|
||||
result = cb.evaluate({"pnl_pct": -6.0, "daily_trades": 10, "toxic_rate": 0.1, "api_errors": 0})
|
||||
assert result["tripped"]
|
||||
|
||||
def test_trade_count_trip(self):
|
||||
cb = CircuitBreaker(max_daily_trades=100)
|
||||
result = cb.evaluate({"pnl_pct": -1.0, "daily_trades": 150, "toxic_rate": 0.1, "api_errors": 0})
|
||||
assert result["tripped"]
|
||||
|
||||
|
||||
class TestConstraintManager:
|
||||
def test_can_place_bid(self):
|
||||
cm = ConstraintManager()
|
||||
result = cm.can_place(side="bid", size=0.001, current_position=0.0, mark_price=100000.0)
|
||||
assert result["allowed"]
|
||||
assert round(result["fee_estimate"], 4) == 0.02 # 0.001 * 100000 * 0.0002 = 0.02
|
||||
|
||||
def test_cannot_exceed_inventory(self):
|
||||
cm = ConstraintManager(
|
||||
inventory=InventoryConstraint(max_long=0.002, max_short=0.002)
|
||||
)
|
||||
result = cm.can_place(side="bid", size=0.001, current_position=0.0015, mark_price=100000.0)
|
||||
assert not result["allowed"]
|
||||
Reference in New Issue
Block a user