172 lines
5.1 KiB
Python
172 lines
5.1 KiB
Python
|
|
"""
|
||
|
|
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)
|