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:
+23
-2
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user