malkhut(T2): Code World Model — deterministic exchange simulator
CWM core (core.py): price-time priority, sequential level consumption, partial fills, queue position, latency injection, maker/taker fees. Numba acceleration (numba_core.py): JIT hot loops, 1.8x fill speedup. Replay verification (replay_verify.py): binary search, trajectory recording. Supporting: adverse_selection, correlation, latency_model, multi_level, queue_model, spread_dynamics, volatility, hftbacktest_validator.
This commit is contained in:
171
MALKHUT/malkhut/cwm/queue_model.py
Normal file
171
MALKHUT/malkhut/cwm/queue_model.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Queue Position Model — estimates fill probability based on queue position.
|
||||
|
||||
The most impactful missing piece in the CWM. In real markets, 70-80% of limit
|
||||
orders don't fill. Queue position determines fill probability.
|
||||
|
||||
This module models:
|
||||
- Queue position estimation (how many orders ahead of us)
|
||||
- Fill probability given queue position and market activity
|
||||
- Queue adverse selection (being at the front of a toxic queue)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
from numba import njit
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class QueueState:
|
||||
"""Queue position state for a price level."""
|
||||
queue_position: int # 0 = front of queue
|
||||
queue_depth: float # total qty ahead of us
|
||||
fill_probability: float # 0-1
|
||||
adverse_selection_risk: float # 0-1
|
||||
|
||||
|
||||
@njit(cache=True)
|
||||
def estimate_queue_position(
|
||||
our_qty: float,
|
||||
level_qty: float,
|
||||
recent_trade_rate: float,
|
||||
time_in_queue_s: float,
|
||||
) -> float:
|
||||
"""
|
||||
Estimate queue position based on queue dynamics.
|
||||
|
||||
Uses a simplified model:
|
||||
- Position = level_qty - our_qty (qty ahead)
|
||||
- Fill rate = recent_trade_rate / queue_depth
|
||||
- Time to fill = queue_depth / fill_rate
|
||||
|
||||
Returns estimated queue depth ahead of us.
|
||||
"""
|
||||
if level_qty <= 0:
|
||||
return 0.0
|
||||
queue_depth = max(0.0, level_qty - our_qty)
|
||||
if recent_trade_rate <= 0:
|
||||
return queue_depth
|
||||
# Adjust for time already in queue
|
||||
consumed = recent_trade_rate * time_in_queue_s
|
||||
return max(0.0, queue_depth - consumed)
|
||||
|
||||
|
||||
@njit(cache=True)
|
||||
def compute_fill_probability(
|
||||
queue_depth: float,
|
||||
our_qty: float,
|
||||
recent_trade_rate: float,
|
||||
time_horizon_s: float,
|
||||
toxicity: float,
|
||||
) -> float:
|
||||
"""
|
||||
Compute probability of fill given queue dynamics.
|
||||
|
||||
Model:
|
||||
- Base fill rate = recent_trade_rate / (queue_depth + our_qty)
|
||||
- Adjusted for toxicity (toxic flow consumes queue faster)
|
||||
- Bounded by time horizon
|
||||
|
||||
Returns 0.0-1.0 probability.
|
||||
"""
|
||||
if our_qty <= 0 or queue_depth < 0:
|
||||
return 0.0
|
||||
if recent_trade_rate <= 0:
|
||||
return 0.0
|
||||
|
||||
total_depth = queue_depth + our_qty
|
||||
if total_depth <= 0:
|
||||
return 1.0
|
||||
|
||||
# Base fill rate: fraction of queue consumed per second
|
||||
base_rate = recent_trade_rate / total_depth
|
||||
|
||||
# Toxicity adjustment: toxic flow fills queue faster (adverse for us)
|
||||
toxicity_factor = 1.0 + toxicity * 0.5
|
||||
|
||||
# Probability of fill within time horizon
|
||||
fill_prob = 1.0 - math.exp(-base_rate * toxicity_factor * time_horizon_s)
|
||||
|
||||
return min(1.0, max(0.0, fill_prob))
|
||||
|
||||
|
||||
@njit(cache=True)
|
||||
def compute_queue_adverse_selection(
|
||||
queue_position: int,
|
||||
recent_trade_rate: float,
|
||||
toxicity: float,
|
||||
spread_bps: float,
|
||||
) -> float:
|
||||
"""
|
||||
Compute adverse selection risk from queue position.
|
||||
|
||||
Adverse selection is higher when:
|
||||
- We're near the front of the queue (more likely to be picked off)
|
||||
- Toxic flow is high (adverse fills more likely)
|
||||
- Spread is tight (less buffer against adverse moves)
|
||||
|
||||
Returns 0.0-1.0 risk score.
|
||||
"""
|
||||
if queue_position <= 0:
|
||||
position_risk = 1.0 # front of queue = highest risk
|
||||
else:
|
||||
position_risk = 1.0 / (1.0 + queue_position * 0.1)
|
||||
|
||||
toxicity_risk = min(1.0, toxicity)
|
||||
spread_risk = max(0.0, 1.0 - spread_bps / 10.0)
|
||||
|
||||
# Combined risk (weighted average)
|
||||
return 0.4 * position_risk + 0.4 * toxicity_risk + 0.2 * spread_risk
|
||||
|
||||
|
||||
class QueuePositionModel:
|
||||
"""
|
||||
Full queue position model for the CWM.
|
||||
|
||||
Integrates with the CWM to provide:
|
||||
- Queue position estimation
|
||||
- Fill probability computation
|
||||
- Adverse selection risk scoring
|
||||
"""
|
||||
|
||||
def __init__(self, default_trade_rate: float = 0.5) -> None:
|
||||
self._default_trade_rate = default_trade_rate
|
||||
|
||||
def estimate_fill_probability(
|
||||
self,
|
||||
our_qty: float,
|
||||
level_qty: float,
|
||||
toxicity: float = 0.0,
|
||||
spread_bps: float = 0.0,
|
||||
time_horizon_s: float = 300.0,
|
||||
recent_trade_rate: Optional[float] = None,
|
||||
) -> float:
|
||||
"""Estimate probability of fill at a price level."""
|
||||
trade_rate = recent_trade_rate or self._default_trade_rate
|
||||
queue_depth = estimate_queue_position(our_qty, level_qty, trade_rate, 0.0)
|
||||
return compute_fill_probability(queue_depth, our_qty, trade_rate, time_horizon_s, toxicity)
|
||||
|
||||
def estimate_queue_position(
|
||||
self,
|
||||
our_qty: float,
|
||||
level_qty: float,
|
||||
recent_trade_rate: Optional[float] = None,
|
||||
time_in_queue_s: float = 0.0,
|
||||
) -> float:
|
||||
"""Estimate queue position ahead of us."""
|
||||
trade_rate = recent_trade_rate or self._default_trade_rate
|
||||
return estimate_queue_position(our_qty, level_qty, trade_rate, time_in_queue_s)
|
||||
|
||||
def adverse_selection_risk(
|
||||
self,
|
||||
queue_position: int,
|
||||
toxicity: float = 0.0,
|
||||
spread_bps: float = 0.0,
|
||||
) -> float:
|
||||
"""Compute adverse selection risk from queue position."""
|
||||
return compute_queue_adverse_selection(queue_position, self._default_trade_rate, toxicity, spread_bps)
|
||||
Reference in New Issue
Block a user