diff --git a/live/paper_trader.py b/live/paper_trader.py index 3df966f..8d8f061 100644 --- a/live/paper_trader.py +++ b/live/paper_trader.py @@ -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": diff --git a/strategies/deep_lob.py b/strategies/deep_lob.py new file mode 100644 index 0000000..7f69c6e --- /dev/null +++ b/strategies/deep_lob.py @@ -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, + } diff --git a/strategies/hawkes_ofi.py b/strategies/hawkes_ofi.py new file mode 100644 index 0000000..02acaf9 --- /dev/null +++ b/strategies/hawkes_ofi.py @@ -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), + }