Files
ftdt-quant-lab/strategies/queue_imbalance.py
T

171 lines
6.0 KiB
Python

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