""" 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)