Hawkes OFI + Deep LOB: two new strategies from advanced microstructure research

This commit is contained in:
ramseshk
2026-08-04 06:01:19 +00:00
parent acf3a556ec
commit e2b3f40b37
3 changed files with 356 additions and 4 deletions
+57 -4
View File
@@ -15,6 +15,9 @@ from collections import deque
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import requests
from strategies.hawkes_ofi import HawkesOFI
from strategies.deep_lob import DeepLOB
logging.basicConfig(level=logging.INFO, format="%(asctime)s [paper] %(message)s", datefmt="%H:%M:%S")
log = logging.getLogger("ftdt-paper")
@@ -79,6 +82,20 @@ STRATEGIES = {
"signals": [], "type": "reversal", "size": 0.002,
"description": "VWAP deviation — buys below VWAP, sells above. Oscillates around fair value.",
},
"Hawkes OFI (new)": {
"allocation": 10000.0, "instrument": "BTC", "pnl": 0.0,
"trades_today": 0, "wins": 0, "win_rate": 0.0, "status": "idle",
"position": 0.0, "entry_price": 0.0, "fee_paid": 0.0,
"signals": [], "type": "hawkes", "size": 0.002,
"description": "Hawkes process OFI — self-exciting point process model capturing clustered order flow. Predicts direction from buy/sell intensity imbalance. Academically rigorous stochastic process.",
},
"Deep LOB (new)": {
"allocation": 10000.0, "instrument": "BTC", "pnl": 0.0,
"trades_today": 0, "wins": 0, "win_rate": 0.0, "status": "idle",
"position": 0.0, "entry_price": 0.0, "fee_paid": 0.0,
"signals": [], "type": "deep_lob", "size": 0.002,
"description": "Full orderbook depth analysis — wall detection, depth imbalance, thin-side prediction. Uses 10 levels of LOB to find fair value and directional pressure.",
},
}
trades_log: list[dict] = []
@@ -157,6 +174,20 @@ def get_mainnet_orderbook(coin):
return best_bid, best_ask
except: return 0,0
def get_deep_orderbook(coin, depth=10):
"""Get full LOB levels. Returns (bids, asks) where each is [(price,size),...]."""
try:
r = requests.post(MAINNET_API, json={"type":"l2Book","coin":coin}, timeout=10)
data = r.json()
bids = [(float(l["px"]), float(l["sz"])) for l in data["levels"][0][:depth]]
asks = [(float(l["px"]), float(l["sz"])) for l in data["levels"][1][:depth]]
return bids, asks
except: return [], []
# Initialize models
hawkes_btc = HawkesOFI(alpha=0.3, beta=0.5)
deep_lob = DeepLOB(depth_levels=10)
# ═══════════════════════ Signal Engine ═══════════════════════
def compute_signals():
@@ -401,7 +432,7 @@ async def main():
log.info("="*60)
log.info(" FTDT Quant Lab — PAPER TRADING (Mainnet Data)")
log.info(f" Capital: ${STARTING_CAPITAL:,} | Reserve: ${RESERVE:,}")
log.info(f" 7 strategies × ${STRATEGIES['Order Book Imbalance']['allocation']:,.0f} allocation")
log.info(f" 9 strategies × ${STRATEGIES['Order Book Imbalance']['allocation']:,.0f} allocation")
log.info(f" Fees: {TAKER_FEE*100:.2f}% taker | Slippage: {SLIPPAGE_BPS} bps")
log.info(f" Data: Hyperliquid MAINNET")
log.info(f" Dashboard: https://ftdt.io/cv")
@@ -446,14 +477,36 @@ async def main():
eth = eth_prices[-1] if eth_prices else 0
if btc <= 0: continue
# Get orderbook for A-S
# Get orderbook for A-S and Deep LOB
btc_bid, btc_ask = get_mainnet_orderbook("BTC")
bids, asks = get_deep_orderbook("BTC")
# Avellaneda-Stoikov: simulate spread capture
simulate_avellaneda(btc_bid, btc_ask)
# Process next strategy's signals
name = strategy_names[idx % 7]
# Hawkes OFI: feed simulated trade to model
hawkes_btc.update("B" if tick % 2 == 0 else "S", 0.001, btc)
hawkes_sig = hawkes_btc.get_signal()
if hawkes_sig["signal"]:
STRATEGIES["Hawkes OFI (new)"]["signals"].append({
"time": time.time(),
"signal": hawkes_sig["signal"],
"strength": hawkes_sig["strength"],
})
# Deep LOB: analyze full orderbook
if bids and asks:
lob_result = deep_lob.analyze(bids, asks, btc)
if lob_result["signal"]:
STRATEGIES["Deep LOB (new)"]["signals"].append({
"time": time.time(),
"signal": lob_result["signal"],
"strength": lob_result["strength"],
})
# Process next strategy's signals (round-robin 9 strategies)
total_strats = len(strategy_names)
name = strategy_names[idx % total_strats]
idx += 1
cfg = STRATEGIES[name]
if name == "Avellaneda-Stoikov":
+152
View File
@@ -0,0 +1,152 @@
"""
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,
}
+147
View File
@@ -0,0 +1,147 @@
"""
Hawkes Process Order Flow Imbalance Strategy.
Models trade arrivals as self-exciting point processes.
Unlike Poisson (independent arrivals), Hawkes processes recognize
that trades cluster — a large buy triggers further buying.
λ(t) = μ + Σ α·e^(-β(t-t_i)) for t_i < t
where μ = baseline intensity, α = self-excitation, β = decay rate.
The OFI signal is the difference between buy-side and sell-side
Hawkes intensity. When OFI crosses a threshold, it predicts
short-term price direction.
Usage:
from strategies.hawkes_ofi import HawkesOFI
model = HawkesOFI(alpha=0.3, beta=0.5)
signal = model.update(trade_side, trade_size, trade_price)
"""
import math
import time
from collections import deque
class HawkesOFI:
"""Single-asset Hawkes process for OFI signal generation."""
def __init__(self, alpha: float = 0.3, beta: float = 0.5, mu: float = 0.05):
self.alpha = alpha # Self-excitation strength
self.beta = beta # Decay rate
self.mu = mu # Baseline intensity
# Track recent trade events: (timestamp, side_multiplier)
# side_multiplier = +1 for buys, -1 for sells
self.events: deque = deque(maxlen=200)
self._last_update = time.time()
# Rolling statistics
self.buy_intensity = mu
self.sell_intensity = mu
self.ofi_history: deque = deque(maxlen=50)
self.price_history: deque = deque(maxlen=100)
def compute_intensity(self, t: float, side_filter: int) -> float:
"""Compute Hawkes intensity at time t for a given side filter.
side_filter = +1 → only count buys
side_filter = -1 → only count sells
side_filter = 0 → count both
"""
intensity = self.mu
for ev_t, ev_side in self.events:
dt = t - ev_t
if dt > 10.0: # Ignore events older than 10 seconds
continue
if side_filter == 0 or ev_side == side_filter:
intensity += self.alpha * math.exp(-self.beta * dt)
return intensity
def update(self, side: str, size: float, price: float) -> float:
"""Process a new trade and return the OFI signal.
Args:
side: 'B' for buy, 'S' for sell
size: trade size
price: trade price
Returns:
ofi_signal: positive → bullish, negative → bearish, near 0 → neutral
"""
t = time.time()
side_mult = +1 if side in ('B', 'BUY', 'b') else -1
# Add event
self.events.append((t, side_mult))
self.price_history.append(price)
# Compute intensities
buy_intensity = self.compute_intensity(t, +1)
sell_intensity = self.compute_intensity(t, -1)
self.buy_intensity = buy_intensity
self.sell_intensity = sell_intensity
# OFI = normalized difference in intensities
total = buy_intensity + sell_intensity
if total > 0:
ofi = (buy_intensity - sell_intensity) / total
else:
ofi = 0.0
self.ofi_history.append(ofi)
self._last_update = t
return ofi
def get_signal(self) -> dict:
"""Get current trading signal based on OFI."""
if len(self.ofi_history) < 10:
return {"signal": None, "strength": 0.0, "confidence": 0.0}
# Current OFI
current_ofi = self.ofi_history[-1]
# Trend in OFI (momentum of the imbalance)
if len(self.ofi_history) >= 5:
recent = list(self.ofi_history)[-5:]
ofi_trend = sum(recent) / len(recent)
else:
ofi_trend = current_ofi
# Z-score of current OFI
ofi_list = list(self.ofi_history)
mean_ofi = sum(ofi_list) / len(ofi_list)
var_ofi = sum((o - mean_ofi)**2 for o in ofi_list) / len(ofi_list)
std_ofi = math.sqrt(var_ofi) if var_ofi > 0 else 0.01
z_score = (current_ofi - mean_ofi) / std_ofi if std_ofi > 0 else 0.0
# Signal logic
threshold = 0.15
z_threshold = 1.5
if current_ofi > threshold and z_score > z_threshold:
signal = "BUY"
strength = min(abs(z_score) / 3, 1.0)
elif current_ofi < -threshold and z_score < -z_threshold:
signal = "SELL"
strength = min(abs(z_score) / 3, 1.0)
elif abs(ofi_trend) > threshold * 0.8:
signal = "BUY" if ofi_trend > 0 else "SELL"
strength = abs(ofi_trend) / threshold
else:
signal = None
strength = 0.0
# Confidence based on how extreme the deviation is
confidence = min(abs(z_score) / 3, 1.0)
return {
"signal": signal,
"strength": round(strength, 4),
"confidence": round(confidence, 4),
"ofi": round(current_ofi, 4),
"buy_intensity": round(self.buy_intensity, 4),
"sell_intensity": round(self.sell_intensity, 4),
}