153 lines
6.1 KiB
Python
153 lines
6.1 KiB
Python
"""
|
|
Deep Limit Order Book Strategy.
|
|
|
|
Analyzes the full orderbook beyond top-of-book to detect:
|
|
1. **Wall detection** — large resting orders that indicate support/resistance
|
|
2. **Depth imbalance** — ratio of cumulative depth on bid vs ask side
|
|
3. **Orderbook skew** — asymmetry in volume distribution across price levels
|
|
4. **Thin-side prediction** — when one side of the book is thin, price likely moves that way
|
|
|
|
When a large wall sits at a certain price level, the market is unlikely to
|
|
break through it quickly. When the ask side is thin relative to the bid side,
|
|
buying pressure is likely to push price up.
|
|
|
|
Usage:
|
|
from strategies.deep_lob import DeepLOB
|
|
model = DeepLOB()
|
|
signal = model.analyze(orderbook_bids, orderbook_asks, mark_price)
|
|
"""
|
|
import math
|
|
from collections import deque
|
|
|
|
class DeepLOB:
|
|
"""Analyze full orderbook depth for trade signals."""
|
|
|
|
def __init__(self, depth_levels: int = 10):
|
|
self.depth_levels = depth_levels
|
|
self.signal_history: deque = deque(maxlen=50)
|
|
|
|
def analyze(self, bids: list, asks: list, mark_price: float) -> dict:
|
|
"""Analyze orderbook and return a signal.
|
|
|
|
Args:
|
|
bids: list of [price, size] pairs sorted best→worst (descending)
|
|
asks: list of [price, size] pairs sorted best→worst (ascending)
|
|
mark_price: current mark price
|
|
|
|
Returns:
|
|
dict with signal, strength, and metrics
|
|
"""
|
|
if not bids or not asks or mark_price <= 0:
|
|
return {"signal": None, "strength": 0.0}
|
|
|
|
# 1. Cumulative depth on each side
|
|
bid_depth = sum(sz for _, sz in bids[:self.depth_levels])
|
|
ask_depth = sum(sz for _, sz in asks[:self.depth_levels])
|
|
|
|
# 2. Wall detection — find largest single order on each side
|
|
bid_walls = sorted([(px, sz) for px, sz in bids[:self.depth_levels]], key=lambda x: x[1], reverse=True)
|
|
ask_walls = sorted([(px, sz) for px, sz in asks[:self.depth_levels]], key=lambda x: x[1], reverse=True)
|
|
|
|
largest_bid_wall = bid_walls[0] if bid_walls else (0, 0)
|
|
largest_ask_wall = ask_walls[0] if ask_walls else (0, 0)
|
|
|
|
# 3. Wall-to-depth ratio — how concentrated is the orderbook?
|
|
bid_concentration = largest_bid_wall[1] / bid_depth if bid_depth > 0 else 0
|
|
ask_concentration = largest_ask_wall[1] / ask_depth if ask_depth > 0 else 0
|
|
|
|
# 4. Depth-weighted mid price (more accurate than top-of-book mid)
|
|
# Weight prices by their size to get "fair value"
|
|
bid_weighted = sum(px * sz for px, sz in bids[:self.depth_levels]) / bid_depth if bid_depth > 0 else 0
|
|
ask_weighted = sum(px * sz for px, sz in asks[:self.depth_levels]) / ask_depth if ask_depth > 0 else 0
|
|
fair_price = (bid_weighted + ask_weighted) / 2 if bid_weighted > 0 and ask_weighted > 0 else mark_price
|
|
|
|
# 5. Depth imbalance ratio
|
|
total_depth = bid_depth + ask_depth
|
|
depth_imbalance = (bid_depth - ask_depth) / total_depth if total_depth > 0 else 0
|
|
|
|
# 6. Thin-side detection
|
|
# If one side is much thinner, price likely moves that way
|
|
depth_ratio = bid_depth / ask_depth if ask_depth > 0 else 999
|
|
thin_side = None
|
|
if depth_ratio > 3.0:
|
|
thin_side = "ask" # Ask side thin → buyers will push through → bullish
|
|
elif depth_ratio < 0.33:
|
|
thin_side = "bid" # Bid side thin → sellers will push through → bearish
|
|
|
|
# 7. Mark price vs fair value
|
|
fv_deviation = (mark_price - fair_price) / fair_price if fair_price > 0 else 0
|
|
|
|
# 8. Volume-weighted average spread
|
|
vwap_spread = 0
|
|
for i in range(min(len(bids), len(asks), 5)):
|
|
spread_i = asks[i][0] - bids[i][0]
|
|
vwap_spread += spread_i
|
|
avg_spread = vwap_spread / min(len(bids), len(asks), 5) if bids and asks else 0
|
|
|
|
# ═══════════════ Signal Generation ═══════════════
|
|
|
|
signal = None
|
|
strength = 0.0
|
|
reasons = []
|
|
|
|
# Wall imbalance signal
|
|
if largest_bid_wall[1] > 2 * largest_ask_wall[1]:
|
|
strength += 0.3
|
|
reasons.append("large_bid_wall")
|
|
elif largest_ask_wall[1] > 2 * largest_bid_wall[1]:
|
|
strength -= 0.3
|
|
reasons.append("large_ask_wall")
|
|
|
|
# Depth imbalance signal
|
|
if depth_imbalance > 0.4:
|
|
strength += 0.25
|
|
reasons.append("bid_depth_dominant")
|
|
elif depth_imbalance < -0.4:
|
|
strength -= 0.25
|
|
reasons.append("ask_depth_dominant")
|
|
|
|
# Thin-side prediction
|
|
if thin_side == "ask":
|
|
strength += 0.35
|
|
reasons.append("thin_ask_pressure")
|
|
elif thin_side == "bid":
|
|
strength -= 0.35
|
|
reasons.append("thin_bid_pressure")
|
|
|
|
# Fair value deviation
|
|
if fv_deviation > 0.002:
|
|
strength -= 0.15 # Overpriced relative to weighted depth
|
|
reasons.append("overvalued")
|
|
elif fv_deviation < -0.002:
|
|
strength += 0.15 # Underpriced
|
|
reasons.append("undervalued")
|
|
|
|
# Determine final signal
|
|
threshold = 0.3
|
|
if strength > threshold:
|
|
signal = "BUY"
|
|
elif strength < -threshold:
|
|
signal = "SELL"
|
|
|
|
self.signal_history.append({
|
|
"signal": signal,
|
|
"strength": strength,
|
|
"depth_imbalance": round(depth_imbalance, 4),
|
|
"thin_side": thin_side,
|
|
})
|
|
|
|
return {
|
|
"signal": signal,
|
|
"strength": round(abs(strength) if signal else strength, 4),
|
|
"fair_price": round(fair_price, 1),
|
|
"depth_imbalance": round(depth_imbalance, 4),
|
|
"depth_ratio": round(depth_ratio, 2),
|
|
"thin_side": thin_side,
|
|
"bid_concentration": round(bid_concentration, 3),
|
|
"ask_concentration": round(ask_concentration, 3),
|
|
"avg_spread": round(avg_spread, 1),
|
|
"largest_bid_wall": round(largest_bid_wall[1], 2),
|
|
"largest_ask_wall": round(largest_ask_wall[1], 2),
|
|
"reasons": reasons,
|
|
}
|