Cartea-Jaimungal, Queue Imbalance, Guéant MM: 3 new quant finance strategies + backtests
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
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),
|
||||
}
|
||||
Reference in New Issue
Block a user