148 lines
4.9 KiB
Python
148 lines
4.9 KiB
Python
"""
|
||
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),
|
||
}
|