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
+23 -2
View File
@@ -60,10 +60,31 @@ class SimConfig:
scenario: ScenarioConfig = field(default_factory=ScenarioConfig)
# Simulation behavior
cancel_after_ms: float = 5000.0 # cancel and re-quote every N ms
quote_refresh_ms: float = 2000.0 # refresh quotes every N ms
cancel_after_ms: float = 5000.0
quote_refresh_ms: float = 2000.0
seed: int | None = None
@classmethod
def from_fee_tier(
cls,
vip_tier: int = 0,
staking_tier: str = "none",
maker_rebate_tier: int = 0,
**kwargs,
) -> "SimConfig":
from config.fee_tiers import get_perp_fees, PERPS_TIERS, STAKING_TIERS
maker_fee = get_perp_fees(vip_tier, staking_tier, "maker", maker_rebate_tier)
taker_fee = get_perp_fees(vip_tier, staking_tier, "taker", maker_rebate_tier)
tier_name = PERPS_TIERS[vip_tier]["name"]
staking_name = STAKING_TIERS.get(staking_tier, STAKING_TIERS["none"])["name"]
return cls(
maker_fee_pct=maker_fee,
taker_fee_pct=taker_fee,
**kwargs,
)
class SimulationEngine:
"""Event-driven market-making simulator.
+115
View File
@@ -180,3 +180,118 @@ def adverse_selection_intensity(
"total_cost_bps": round(sum(adverse_costs), 2),
"n_fills": len(fills),
}
class QueueAwareFillModel:
"""Realistic queue-priority fill model for paper trading.
Unlike random fills, this models whether an aggressor trade at a given
price level would exhaust the queue ahead of our order. An order fills
only when the total aggressor volume at that price exceeds the volume
of orders ahead of ours in the FIFO queue.
Usage:
model = QueueAwareFillModel()
fill_info = model.check_fill(
aggressor_side="buy",
agg_size=0.005,
our_price=50000.0,
our_size=0.001,
depth_ahead=0.002,
)
"""
def __init__(self, base_fill_prob: float = 0.20):
self._base_fill_prob = base_fill_prob
self._fill_count: int = 0
self._skip_count: int = 0
def check_fill(
self,
aggressor_side: str,
agg_size: float,
agg_price: float,
our_price: float,
our_size: float,
depth_ahead: float,
) -> dict:
"""Determine if a market order would fill our limit order.
Args:
aggressor_side: "buy" (market buy hits asks) or "sell" (market sell hits bids)
agg_size: size of the aggressor trade
agg_price: price of the aggressor trade
our_price: our limit order price
our_size: our order size
depth_ahead: total size of orders ahead of ours in the queue at this price
Returns:
dict with filled (bool), fill_size, reason
"""
price_match = False
if aggressor_side == "buy" and agg_price >= our_price:
price_match = True
elif aggressor_side == "sell" and agg_price <= our_price:
price_match = True
if not price_match:
return {"filled": False, "fill_size": 0.0, "reason": "price_not_crossed"}
remaining_after_queue = agg_size - depth_ahead
if remaining_after_queue <= 0:
self._skip_count += 1
return {"filled": False, "fill_size": 0.0, "reason": "queue_not_reached"}
fill_size = min(our_size, remaining_after_queue)
self._fill_count += 1
return {
"filled": True,
"fill_size": round(fill_size, 8),
"fill_ratio": round(fill_size / our_size, 4),
"reason": f"reached_queue_pos",
"depth_consumed": round(depth_ahead + fill_size, 8),
}
def estimate_depth_ahead(
self,
our_price: float,
our_side: str,
best_bid: float,
best_ask: float,
bid_depth: float,
ask_depth: float,
) -> float:
"""Estimate the volume ahead of our order at a price level.
This is a heuristic since HL doesn't expose queue position. We estimate
based on whether we're at the best level and how much total depth is there.
"""
at_best = (
(our_side == "bid" and our_price >= best_bid) or
(our_side == "ask" and our_price <= best_ask)
)
if not at_best:
return float("inf")
if our_side == "bid":
return bid_depth * 0.5
else:
return ask_depth * 0.5
@property
def fill_count(self) -> int:
return self._fill_count
@property
def skip_count(self) -> int:
return self._skip_count
def fill_rate(self) -> float:
total = self._fill_count + self._skip_count
return self._fill_count / total if total > 0 else 0.0
def reset(self):
self._fill_count = 0
self._skip_count = 0