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

148 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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),
}