171 lines
6.0 KiB
Python
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),
|
|
}
|