""" Queue Imbalance Model — Order Book Dynamics for Short-Term Prediction. Based on the Stoikov & Sağlam and Cont et al. frameworks. Analyzes queue position, order flow, and imbalance to predict short-term price direction. Key concepts: 1. Queue position estimation — where we sit in the LOB queue 2. Order flow imbalance at each level — net adds vs cancels 3. Probability of mid-price move based on queue dynamics 4. Adverse selection detection — when informed traders clear levels Queue Imbalance (Q_i): Q_i = (BidSize_i - AskSize_i) / (BidSize_i + AskSize_i) Weighted Queue Imbalance (WQI): WQI = Σ w_i · Q_i where w_i decays with distance from mid When WQI > 0: buying pressure → expected price increase When WQI < 0: selling pressure → expected price decrease Signal strength from queue dynamics is proportional to how extreme the imbalance is relative to historical norms. Usage: from strategies.queue_imbalance import QueueImbalance qi = QueueImbalance() signal = qi.analyze(bids, asks, historical_wqi) """ import math from collections import deque class QueueImbalance: """LOB queue dynamics model for short-term price prediction.""" def __init__(self, depth_levels: int = 10): self.depth_levels = depth_levels self.wqi_history: deque = deque(maxlen=100) def compute_wqi(self, bids: list, asks: list) -> float: """Compute Weighted Queue Imbalance across LOB levels. Weights decay exponentially: w_i = e^(-i/3) for level i. This gives 3x more weight to top-of-book than 3 levels deep. """ if not bids or not asks: return 0.0 wqi = 0.0 total_weight = 0.0 for i in range(min(len(bids), len(asks), self.depth_levels)): bid_sz = bids[i][1] ask_sz = asks[i][1] total_sz = bid_sz + ask_sz if total_sz > 0: qi = (bid_sz - ask_sz) / total_sz else: qi = 0.0 # Exponential decay weight weight = math.exp(-i / 3.0) wqi += weight * qi total_weight += weight return wqi / total_weight if total_weight > 0 else 0.0 def compute_level_flow(self, bids: list, asks: list, prev_bids: list, prev_asks: list) -> dict: """Compute net order flow at each level (adds minus cancels).""" flow = {"bid_flow": 0.0, "ask_flow": 0.0, "net_flow": 0.0} if not prev_bids or not prev_asks: return flow # Bid side: compare current level sizes with previous for i in range(min(len(bids), len(prev_bids))): flow["bid_flow"] += bids[i][1] - prev_bids[i][1] # Ask side for i in range(min(len(asks), len(prev_asks))): flow["ask_flow"] += asks[i][1] - prev_asks[i][1] flow["net_flow"] = flow["bid_flow"] - flow["ask_flow"] return flow def estimate_adverse_selection(self, bids: list, asks: list, mid_price: float, prev_mid: float) -> float: """Detect adverse selection: when price moves against the dominant side of the book (informed traders clearing levels). Returns 0-1 score where 1 = high adverse selection risk. """ if prev_mid <= 0 or mid_price <= 0: return 0.0 # Compute which side was dominant in the previous tick wqi = self.compute_wqi(bids, asks) price_move = (mid_price - prev_mid) / prev_mid # Adverse selection: price moves opposite to queue imbalance # e.g., bids dominant (WQI > 0) but price goes down if wqi > 0.1 and price_move < -0.0005: return min(abs(price_move) * 1000, 1.0) elif wqi < -0.1 and price_move > 0.0005: return min(abs(price_move) * 1000, 1.0) return 0.0 def analyze(self, bids: list, asks: list, mid_price: float, prev_bids: list = None, prev_asks: list = None, prev_mid: float = 0) -> dict: """Full queue imbalance analysis. Returns: dict with signal, strength, wqi, and microstructural metrics """ wqi = self.compute_wqi(bids, asks) self.wqi_history.append(wqi) # Compute WQI z-score if len(self.wqi_history) >= 20: history = list(self.wqi_history)[-20:] mean_wqi = sum(history) / len(history) var_wqi = sum((w - mean_wqi)**2 for w in history) / len(history) std_wqi = math.sqrt(var_wqi) if var_wqi > 0 else 0.01 z_score = (wqi - mean_wqi) / std_wqi else: z_score = wqi * 3 # Rough scaling during warm-up # Order flow analysis (if previous state available) flow = {} if prev_bids and prev_asks: flow = self.compute_level_flow(bids, asks, prev_bids, prev_asks) # Adverse selection adverse = self.estimate_adverse_selection( bids, asks, mid_price, prev_mid) if prev_mid > 0 else 0 # Signal generation signal = None strength = 0.0 z_threshold = 1.2 wqi_threshold = 0.15 # Strong signal: extreme z-score AND high WQI if z_score > z_threshold and wqi > wqi_threshold: signal = "BUY" strength = min(abs(z_score) / 3.0, 1.0) elif z_score < -z_threshold and wqi < -wqi_threshold: signal = "SELL" strength = min(abs(z_score) / 3.0, 1.0) # Reduce confidence if adverse selection detected if adverse > 0.3: strength *= (1.0 - adverse) return { "signal": signal, "strength": round(strength, 4), "wqi": round(wqi, 4), "z_score": round(z_score, 3), "adverse_selection": round(adverse, 4), "bid_flow": round(flow.get("bid_flow", 0), 4), "ask_flow": round(flow.get("ask_flow", 0), 4), "net_flow": round(flow.get("net_flow", 0), 4), }