feat: HFT infrastructure — tick backtest runner, VPIN-gated A-S maker, WQI predictor, queue-aware fills

- backtests/tick_runner.py: TickBacktestRunner replays stored Parquet L2/trade events
  through sim/engine.py with queue position modeling, producing PnL breakdowns,
  equity curves, VPIN curves, and QuantVerdict significance reports
- VPINGatedASMaker: VPIN-toxicity-gated A-S market maker with inventory skew
  and dynamic spread widening; blocks quoting when VPIN >= alarm threshold
- sim/engine.py: Added SimConfig.from_fee_tier() factory — constructs sim
  config from Hyperliquid fee tier (VIP + staking)
- sim/fills.py: Added QueueAwareFillModel — realistic queue-priority fill
  simulation replacing random fills in paper trading
- strategies/wqi_predictor.py: WQI z-score directional strategy with
  adverse selection gating, timeout exit, stop-loss, and take-profit
- cli.py: Added 'tick', 'markout' analysis, and 'discover' signal-discovery
  commands for end-to-end tick-level HFT research pipeline

301 tests passing (23 new).
This commit is contained in:
ramseshk
2026-08-11 10:43:51 +08:00
parent cdc7a01986
commit 50d63e1ecc
6 changed files with 1517 additions and 3 deletions
+228
View File
@@ -0,0 +1,228 @@
"""
Tests for tick-level backtest runner and queue-aware fill model.
"""
from sim.fills import QueueAwareFillModel, FillModelConfig, FillSimulator
from sim.maker import MakerConfig
from sim.engine import SimConfig
class TestQueueAwareFillModel:
def test_price_not_crossed(self):
qm = QueueAwareFillModel()
result = qm.check_fill(
aggressor_side="buy",
agg_size=0.01,
agg_price=49900.0,
our_price=50000.0,
our_size=0.001,
depth_ahead=0.0,
)
assert not result["filled"]
assert result["reason"] == "price_not_crossed"
def test_fills_when_price_crossed_and_no_queue_ahead(self):
qm = QueueAwareFillModel()
result = qm.check_fill(
aggressor_side="buy",
agg_size=0.01,
agg_price=50005.0,
our_price=50000.0,
our_size=0.001,
depth_ahead=0.0,
)
assert result["filled"]
assert result["fill_size"] == 0.001
def test_does_not_fill_when_queue_not_reached(self):
qm = QueueAwareFillModel()
result = qm.check_fill(
aggressor_side="buy",
agg_size=0.001,
agg_price=50005.0,
our_price=50000.0,
our_size=0.001,
depth_ahead=0.005,
)
assert not result["filled"]
assert result["reason"] == "queue_not_reached"
def test_partial_fill(self):
qm = QueueAwareFillModel()
result = qm.check_fill(
aggressor_side="sell",
agg_size=0.005,
agg_price=49990.0,
our_price=50000.0,
our_size=0.003,
depth_ahead=0.002,
)
assert result["filled"]
assert result["fill_size"] == 0.003
def test_sell_fill_price_match(self):
qm = QueueAwareFillModel()
result = qm.check_fill(
aggressor_side="sell",
agg_size=0.01,
agg_price=49990.0,
our_price=50000.0,
our_size=0.001,
depth_ahead=0.0,
)
assert result["filled"]
def test_estimate_depth_ahead_at_best(self):
qm = QueueAwareFillModel()
depth = qm.estimate_depth_ahead(
our_price=50000.0,
our_side="bid",
best_bid=50000.0,
best_ask=50002.0,
bid_depth=2.0,
ask_depth=1.0,
)
assert depth == 1.0
def test_estimate_depth_ahead_not_at_best(self):
qm = QueueAwareFillModel()
depth = qm.estimate_depth_ahead(
our_price=49999.0,
our_side="bid",
best_bid=50000.0,
best_ask=50002.0,
bid_depth=2.0,
ask_depth=1.0,
)
assert depth == float("inf")
def test_fill_rate_tracking(self):
qm = QueueAwareFillModel()
qm.check_fill("buy", 0.01, 50005.0, 50000.0, 0.001, 0.0)
qm.check_fill("buy", 0.001, 50005.0, 50000.0, 0.001, 0.005)
qm.check_fill("buy", 0.01, 50005.0, 50000.0, 0.001, 0.0)
assert qm.fill_count == 2
assert qm.skip_count == 1
assert qm.fill_rate() == 2 / 3
class TestSimConfigFeeTier:
def test_from_fee_tier_default(self):
cfg = SimConfig.from_fee_tier(vip_tier=0)
assert cfg.maker_fee_pct == 0.00015
assert cfg.taker_fee_pct == 0.00045
def test_from_fee_tier_vip2(self):
cfg = SimConfig.from_fee_tier(vip_tier=2)
assert cfg.maker_fee_pct == 0.00008
assert cfg.taker_fee_pct == 0.00035
def test_from_fee_tier_with_staking(self):
cfg = SimConfig.from_fee_tier(vip_tier=0, staking_tier="gold")
assert cfg.maker_fee_pct < 0.00015
assert cfg.taker_fee_pct < 0.00045
def test_from_fee_tier_custom_params(self):
cfg = SimConfig.from_fee_tier(
vip_tier=0,
max_inventory=0.01,
initial_equity=50000.0,
)
assert cfg.max_inventory == 0.01
assert cfg.initial_equity == 50000.0
class TestVPINGatedASMaker:
def test_default_allows_quoting(self):
from backtests.tick_runner import VPINGatedASMaker
maker = VPINGatedASMaker(MakerConfig())
assert maker.allowed_to_quote() == (True, 1.0)
def test_alarm_blocks_quoting(self):
from backtests.tick_runner import VPINGatedASMaker
maker = VPINGatedASMaker(MakerConfig(), vpin_threshold=0.3, vpin_alarm=0.5)
maker._current_vpin = 0.55
assert maker.allowed_to_quote() == (False, 0.0)
def test_threshold_reduces_size(self):
from backtests.tick_runner import VPINGatedASMaker
maker = VPINGatedASMaker(MakerConfig(), vpin_threshold=0.3, vpin_alarm=0.5)
maker._current_vpin = 0.35
allowed, size_mult = maker.allowed_to_quote()
assert allowed
assert 0 < size_mult < 1.0
def test_returns_none_when_not_allowed(self):
from backtests.tick_runner import VPINGatedASMaker
maker = VPINGatedASMaker(MakerConfig(), vpin_threshold=0.3, vpin_alarm=0.5)
maker._current_vpin = 0.55
maker.observe(100000.0)
q = maker.quote(100000.0, 0.0, 0.0)
assert q is None
def test_returns_quote_when_allowed(self):
from backtests.tick_runner import VPINGatedASMaker
maker = VPINGatedASMaker(MakerConfig())
maker.observe(100000.0)
maker.observe(100100.0)
maker.observe(100050.0)
q = maker.quote(100000.0, 0.0, 0.0)
assert q is not None
assert q.bid < q.ask
assert q.bid_size > 0
class TestWQIPredictor:
def test_initial_state(self):
from strategies.wqi_predictor import WQIPredictor
wqi = WQIPredictor()
assert wqi.position == 0
assert len(wqi.trades) == 0
def test_no_signal_with_balanced_book(self):
from strategies.wqi_predictor import WQIPredictor
wqi = WQIPredictor()
bids = [(100.0, 1.0), (99.0, 1.0)]
asks = [(102.0, 1.0), (103.0, 1.0)]
signal = wqi.feed_signal(bids, asks, 101.0)
assert signal["action"] == "HOLD"
def test_buy_signal_with_bid_heavy_book(self):
from strategies.wqi_predictor import WQIPredictor
wqi = WQIPredictor(z_entry=0.5, wqi_threshold=0.05)
for _ in range(25):
wqi.feed_signal([(100.0, 1.0), (99.0, 1.0)], [(102.0, 1.0), (103.0, 1.0)], 101.0)
bids = [(100.0, 10.0), (99.0, 5.0)]
asks = [(102.0, 1.0)]
signal = wqi.feed_signal(bids, asks, 101.0)
if signal["action"] == "BUY":
assert wqi.position != 0
def test_exit_on_timeout(self):
from strategies.wqi_predictor import WQIPredictor
import time
wqi = WQIPredictor(z_entry=0.01, z_exit=999.0, wqi_threshold=0.01,
max_hold_seconds=0.001)
for _ in range(25):
wqi.feed_signal([(100.0, 1.0), (99.0, 1.0)], [(102.0, 1.0), (103.0, 1.0)], 101.0)
bids = [(100.0, 10.0), (99.0, 5.0)]
asks = [(102.0, 1.0)]
wqi.feed_signal(bids, asks, 101.0)
time.sleep(0.01)
signal = wqi.feed_signal(bids, asks, 101.0)
assert signal["action"] in ("HOLD", "EXIT")
def test_summary_returns_zero_for_no_trades(self):
from strategies.wqi_predictor import WQIPredictor
wqi = WQIPredictor()
s = wqi.summary()
assert s["total_trades"] == 0
def test_reset_clears_state(self):
from strategies.wqi_predictor import WQIPredictor
wqi = WQIPredictor()
bids = [(100.0, 1.0)]
asks = [(102.0, 1.0)]
wqi.feed_signal(bids, asks, 101.0)
wqi.reset()
assert wqi.position == 0
assert len(wqi.trades) == 0