feat: Phase 4 — controlled strategy deployment module + 38 tests
New live/ sub-modules for production-ready market making:
live/filters/toxicity.py (ToxicityFilter):
VPIN-based pre-trade filter. Accumulates buy/sell volume, computes
VPIN via microstructure module, produces quoting decision:
- allow_quoting: bool
- size_multiplier: 0.0–1.0 (graduated reduction approaching alarm)
- granular thresholds (threshold vs alarm) with smooth reduction
live/treasury.py (Treasury):
Central capital/risk management — single source of truth:
- Position tracking per coin (opening, closing, average entry)
- Realized + unrealized PnL computation
- Pre-trade constraint checks (inventory limits, fee estimates)
- Circuit breaker (drawdown, trade count, toxic fill rate, API errors)
- Liquidation distance monitoring
- Automatic cooldown reset after trip expiry
live/makers/hl_btc_eth.py:
HlMaker — per-coin market maker integrating:
- AvellanedaStoikovMaker (Phase 3) for optimal quotes
- ToxicityFilter for pre-trade gating
- Treasury for position/risk checks
HlMakerPool — manages multiple HlMaker instances with shared treasury
and coordinated observe_all()/quote_all()
live/monitors/cross_venue.py (CrossVenueMonitor):
Cross-exchange lead-lag detection via cross-correlation at multiple
lags. Spot premium (basis proxy) computation. Multi-venue summary.
live/monitors/funding_basis.py (FundingBasisMonitor):
Funding regime classification, momentum detection, carry PnL
estimation, basis spread analysis. Uses microstructure/funding.py.
live/monitors/liq_risk.py (LiquidationRiskOverlay):
Per-position liquidation distance monitoring with tiered warnings
(safe/warning/danger/critical). Recommended position reduction.
38 tests across 4 files (all pass):
test_live_filters.py (5)
test_live_maker.py (9)
test_live_monitors.py (11)
test_live_treasury.py (13)
Total test suite: 172 tests, all passing.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Tests for live/filters/toxicity.py — pre-trade toxicity filter.
|
||||
"""
|
||||
from live.filters.toxicity import ToxicityFilter
|
||||
|
||||
|
||||
class TestToxicityFilter:
|
||||
def test_initial_state_allows_quoting(self):
|
||||
tf = ToxicityFilter()
|
||||
result = tf.check()
|
||||
assert result["allow_quoting"]
|
||||
assert result["size_multiplier"] == 1.0
|
||||
|
||||
def test_balanced_flow_keeps_quoting(self):
|
||||
tf = ToxicityFilter()
|
||||
for _ in range(200):
|
||||
tf.update_trade(buy_vol=1.0, sell_vol=1.0)
|
||||
result = tf.check()
|
||||
assert result["allow_quoting"]
|
||||
|
||||
def test_imbalanced_flow_reduces_or_blocks(self):
|
||||
tf = ToxicityFilter(vpin_threshold=0.2, vpin_alarm=0.3, volume_bucket_size=1.0, vpin_window=10)
|
||||
for _ in range(200):
|
||||
tf.update_trade(buy_vol=3.0, sell_vol=1.0)
|
||||
result = tf.check()
|
||||
assert result["vpin"] >= 0
|
||||
|
||||
def test_book_imbalance_tracking(self):
|
||||
tf = ToxicityFilter()
|
||||
tf.update_book_imbalance(0.5)
|
||||
result = tf.check()
|
||||
assert result["obi"] == 0.5
|
||||
|
||||
def test_trade_count_increments(self):
|
||||
tf = ToxicityFilter()
|
||||
tf.update_trade(0.1, 0.05)
|
||||
tf.update_trade(0.2, 0.1)
|
||||
assert tf.trade_count == 2
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Tests for live/makers/hl_btc_eth.py — HL maker strategy.
|
||||
"""
|
||||
from live.treasury import Treasury
|
||||
from live.makers.hl_btc_eth import HlMaker, HlMakerPool
|
||||
|
||||
|
||||
class TestHlMaker:
|
||||
def test_initial_quote_returns_none(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
maker = HlMaker("BTC", treasury=t)
|
||||
assert maker.quote() is None # no mid price yet
|
||||
|
||||
def test_quote_after_observe(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
maker = HlMaker("BTC", treasury=t, base_size=0.001)
|
||||
maker.observe(50000.0)
|
||||
maker.update_book(49999.0, 50001.0)
|
||||
q = maker.quote()
|
||||
assert q is not None
|
||||
assert q.bid < 50000.0 < q.ask
|
||||
assert q.bid_size > 0
|
||||
assert q.ask_size > 0
|
||||
|
||||
def test_quote_blocked_by_halte(self):
|
||||
t = Treasury(initial_equity=10000.0, max_drawdown_pct=-1.0)
|
||||
t._realized_pnl = -5000.0
|
||||
t._fees_paid = 0
|
||||
t._check_breakers()
|
||||
maker = HlMaker("BTC", treasury=t)
|
||||
maker.observe(50000.0)
|
||||
assert maker.quote() is None
|
||||
|
||||
def test_should_skip_at_max_inventory(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
maker = HlMaker("BTC", treasury=t, max_inventory=0.001)
|
||||
t.record_fill("BTC", side="buy", size=0.001, price=50000.0, fee=10.0, pnl=0)
|
||||
maker.observe(50000.0)
|
||||
assert maker.should_skip()
|
||||
|
||||
def test_record_fill_updates_treasury(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
maker = HlMaker("BTC", treasury=t)
|
||||
maker.observe(50000.0)
|
||||
maker.record_fill("buy", 0.001, 50000.0, 10.0)
|
||||
assert t.position("BTC") == 0.001
|
||||
|
||||
def test_quote_never_crosses_book(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
maker = HlMaker("BTC", treasury=t, base_size=0.001, gamma=0.5)
|
||||
maker.observe(50000.0)
|
||||
maker.update_book(49995.0, 50005.0)
|
||||
for _ in range(20):
|
||||
q = maker.quote()
|
||||
if q:
|
||||
assert q.bid <= 49995.0 # never above best bid
|
||||
assert q.ask >= 50005.0 # never below best ask
|
||||
assert q.bid < q.ask
|
||||
|
||||
|
||||
class TestHlMakerPool:
|
||||
def test_add_and_get_makers(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
pool = HlMakerPool(treasury=t)
|
||||
pool.add_maker("BTC", max_inventory=0.002)
|
||||
pool.add_maker("ETH", max_inventory=0.01)
|
||||
assert pool.get("BTC") is not None
|
||||
assert pool.get("ETH") is not None
|
||||
assert pool.get("SOL") is None
|
||||
|
||||
def test_observe_and_quote_all(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
pool = HlMakerPool(treasury=t)
|
||||
maker_btc = pool.add_maker("BTC", max_inventory=0.002, base_size=0.001)
|
||||
pool.observe_all({"BTC": 50000.0})
|
||||
quotes = pool.quote_all()
|
||||
assert "BTC" in quotes
|
||||
assert quotes["BTC"] is not None
|
||||
|
||||
def test_summary(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
pool = HlMakerPool(treasury=t)
|
||||
pool.add_maker("BTC", max_inventory=0.002)
|
||||
pool.observe_all({"BTC": 50000.0})
|
||||
s = pool.summary()
|
||||
assert "BTC" in s
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Tests for live/monitors — cross-venue, funding/basis, liquidation risk.
|
||||
"""
|
||||
from live.monitors.cross_venue import CrossVenueMonitor
|
||||
from live.monitors.funding_basis import FundingBasisMonitor
|
||||
from live.monitors.liq_risk import LiquidationRiskOverlay
|
||||
from live.treasury import Treasury
|
||||
|
||||
|
||||
class TestCrossVenueMonitor:
|
||||
def test_update_and_lead_lag(self):
|
||||
cm = CrossVenueMonitor(window=50, max_lag=5)
|
||||
for i in range(50):
|
||||
cm.update("hl", "BTC", 50000.0 + i * 10, float(i))
|
||||
cm.update("binance", "BTC", 50000.0 + i * 10 + 2, float(i))
|
||||
result = cm.lead_lag("BTC", "hl", "binance")
|
||||
assert result is not None
|
||||
assert "correlation" in result
|
||||
assert "lag" in result
|
||||
|
||||
def test_spot_premium(self):
|
||||
cm = CrossVenueMonitor()
|
||||
for _ in range(10):
|
||||
cm.update("hl", "BTC", 50005.0, 0)
|
||||
cm.update("binance", "BTC", 50000.0, 0)
|
||||
premium = cm.spot_premium("BTC")
|
||||
assert premium is not None
|
||||
assert premium["basis_bps"] > 0
|
||||
|
||||
def test_nonexistent_coin_returns_none(self):
|
||||
cm = CrossVenueMonitor()
|
||||
assert cm.lead_lag("XYZ", "hl", "binance") is None
|
||||
|
||||
def test_summary(self):
|
||||
cm = CrossVenueMonitor()
|
||||
for i in range(50):
|
||||
cm.update("hl", "BTC", 50000.0 + i * 10, float(i))
|
||||
cm.update("binance", "BTC", 50000.0 + i * 10, float(i))
|
||||
s = cm.summary("BTC")
|
||||
assert "hl_binance" in s
|
||||
|
||||
|
||||
class TestFundingBasisMonitor:
|
||||
def test_initial_no_signal(self):
|
||||
fm = FundingBasisMonitor()
|
||||
result = fm.signal("BTC")
|
||||
assert result["signal"] == "insufficient_data"
|
||||
|
||||
def test_signal_with_data(self):
|
||||
fm = FundingBasisMonitor(funding_window=100, samples_per_hour=60)
|
||||
for _ in range(200):
|
||||
fm.update_funding("BTC", 0.00001)
|
||||
result = fm.signal("BTC")
|
||||
assert result["signal"] in ("neutral", "positive", "negative", "high_positive", "high_negative")
|
||||
assert "funding_mean_annual_pct" in result
|
||||
|
||||
def test_basis_requires_spot_and_perp(self):
|
||||
fm = FundingBasisMonitor()
|
||||
for i in range(50):
|
||||
fm.update_perp("BTC", 50005.0)
|
||||
fm.update_spot("BTC", 50000.0)
|
||||
fm.update_funding("BTC", 0.00001)
|
||||
result = fm.signal("BTC")
|
||||
assert result["basis_current_bps"] > 0
|
||||
|
||||
|
||||
class TestLiquidationRiskOverlay:
|
||||
def test_safe_position(self):
|
||||
t = Treasury(initial_equity=100000.0)
|
||||
t.record_fill("BTC", side="buy", size=0.001, price=50000.0, fee=10.0, pnl=0)
|
||||
overlay = LiquidationRiskOverlay(treasury=t)
|
||||
result = overlay.check("BTC")
|
||||
assert result["level"] == "safe"
|
||||
|
||||
def test_no_position(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
overlay = LiquidationRiskOverlay(treasury=t)
|
||||
result = overlay.check("BTC")
|
||||
assert result["distance_pct"] > 1e5 # capped at 999999 for display
|
||||
|
||||
def test_recommended_action(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
t.record_fill("BTC", side="buy", size=0.001, price=50000.0, fee=10.0, pnl=0)
|
||||
overlay = LiquidationRiskOverlay(treasury=t)
|
||||
assert overlay.recommended_action("BTC") == "none"
|
||||
|
||||
def test_summary(self):
|
||||
t = Treasury(initial_equity=100000.0)
|
||||
t.record_fill("BTC", side="buy", size=0.001, price=50000.0, fee=10.0, pnl=0)
|
||||
overlay = LiquidationRiskOverlay(treasury=t)
|
||||
s = overlay.summary()
|
||||
assert "positions" in s
|
||||
assert "worst_case" in s
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Tests for live/treasury.py — central treasury and risk management.
|
||||
"""
|
||||
from live.treasury import Treasury
|
||||
|
||||
|
||||
class TestTreasury:
|
||||
def test_initial_equity(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
assert t.equity == 10000.0
|
||||
assert t.total_pnl() == 0.0
|
||||
|
||||
def test_can_open_within_limits(self):
|
||||
t = Treasury(initial_equity=10000.0, max_position_per_asset=0.005)
|
||||
result = t.can_open("BTC", side="buy", size=0.001, mark_price=50000.0)
|
||||
assert result["allowed"]
|
||||
|
||||
def test_cannot_open_exceed_inventory(self):
|
||||
t = Treasury(initial_equity=10000.0, max_position_per_asset=0.002)
|
||||
t.can_open("BTC", side="buy", size=0.001, mark_price=50000.0)
|
||||
t.record_fill("BTC", side="buy", size=0.001, price=50000.0, fee=10.0, pnl=0)
|
||||
result = t.can_open("BTC", side="buy", size=0.0015, mark_price=50000.0)
|
||||
assert not result["allowed"]
|
||||
|
||||
def test_record_fill_updates_position(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
t.record_fill("BTC", side="buy", size=0.001, price=50000.0, fee=10.0, pnl=0)
|
||||
assert t.position("BTC") == 0.001
|
||||
|
||||
def test_record_close_updates_pnl(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
t.record_fill("BTC", side="buy", size=0.001, price=50000.0, fee=10.0, pnl=0)
|
||||
t.record_fill("BTC", side="sell", size=0.001, price=50100.0, fee=10.0, pnl=100.0)
|
||||
assert t.position("BTC") == 0.0
|
||||
assert t.total_pnl() > 0
|
||||
|
||||
def test_unrealized_pnl(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
t.record_fill("BTC", side="buy", size=0.001, price=50000.0, fee=10.0, pnl=0)
|
||||
t.update_mark_price("BTC", 50200.0)
|
||||
assert t.unrealized_pnl() > 0
|
||||
|
||||
def test_pnl_pct(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
t.record_fill("BTC", side="buy", size=0.1, price=50000.0, fee=0.1, pnl=0)
|
||||
t.update_mark_price("BTC", 50200.0) # 0.1 * 200 = $20 unrealized >> $0.10 fee
|
||||
assert t.pnl_pct() > 0
|
||||
|
||||
def test_circuit_breaker_drawdown(self):
|
||||
t = Treasury(initial_equity=10000.0, max_drawdown_pct=-1.0)
|
||||
# Force large negative PnL
|
||||
t._realized_pnl = -5000.0
|
||||
t._fees_paid = 0
|
||||
t._check_breakers()
|
||||
assert t.is_halted()
|
||||
|
||||
def test_liquidation_distance(self):
|
||||
t = Treasury(initial_equity=50000.0)
|
||||
t.record_fill("BTC", side="buy", size=1.0, price=50000.0, fee=10.0, pnl=0)
|
||||
dist = t.liquidation_distance("BTC")
|
||||
assert dist > 0
|
||||
|
||||
def test_all_positions(self):
|
||||
t = Treasury()
|
||||
t.record_fill("BTC", side="buy", size=0.001, price=50000.0, fee=10.0, pnl=0)
|
||||
t.record_fill("ETH", side="sell", size=0.01, price=3000.0, fee=10.0, pnl=0)
|
||||
positions = t.all_positions
|
||||
assert positions["BTC"] == 0.001
|
||||
assert positions["ETH"] == -0.01
|
||||
|
||||
def test_net_exposure(self):
|
||||
t = Treasury()
|
||||
t.record_fill("BTC", side="buy", size=0.002, price=50000.0, fee=10.0, pnl=0)
|
||||
t.record_fill("ETH", side="buy", size=0.003, price=3000.0, fee=10.0, pnl=0)
|
||||
assert t.net_exposure == 0.005
|
||||
|
||||
def test_summary(self):
|
||||
t = Treasury(initial_equity=10000.0)
|
||||
t.record_fill("BTC", side="buy", size=0.001, price=50000.0, fee=10.0, pnl=0)
|
||||
s = t.summary()
|
||||
assert "equity" in s
|
||||
assert "pnl_pct" in s
|
||||
assert "positions" in s
|
||||
assert "BTC" in s["positions"]
|
||||
|
||||
def test_toxic_fill_tracking(self):
|
||||
t = Treasury(initial_equity=10000.0, max_toxic_rate=0.1)
|
||||
t.record_fill("BTC", side="buy", size=0.001, price=50000.0, fee=10.0, pnl=0)
|
||||
for _ in range(9):
|
||||
t.record_fill("BTC", side="buy", size=0.001, price=50000.0, fee=10.0, pnl=-1.0)
|
||||
t.record_toxic_fill()
|
||||
t.record_toxic_fill()
|
||||
t._check_breakers()
|
||||
# 2/10 = 20% toxic > 10% threshold
|
||||
assert t.is_halted()
|
||||
Reference in New Issue
Block a user