From cc2b7df740ac144b77dd9395b9910191fd84e293 Mon Sep 17 00:00:00 2001 From: ramseshk Date: Tue, 4 Aug 2026 06:30:36 +0000 Subject: [PATCH] Professional dashboard: strategy detail panel with chart, trades, and signal reasons --- dashboard/static/index.html | 444 ++++++++++++------------ live/paper_trader.py | 660 ++++++++++++++++++++++++++++++++++++ 2 files changed, 894 insertions(+), 210 deletions(-) diff --git a/dashboard/static/index.html b/dashboard/static/index.html index 2081c9f..233af11 100644 --- a/dashboard/static/index.html +++ b/dashboard/static/index.html @@ -2,255 +2,279 @@ - -FTDT Quant Lab — Live Strategy Dashboard - + +FTDT Quant Lab — Professional Dashboard + -
-
- -
Portfolio PnL
$0.00
0.00%
+ +
+
+

FTDT Quant LabProfessional Quant Dashboard

+
+
Portfolio Equity
+
$0.00
+
+
+
+
- - - + + +
- - + +
-
-

Equity Curve real-time · Hyperliquid Testnet

-
-

Trade Log

TimeStrategySideSizePriceFeePnL
+
+
- -
-
-

Equity Curves per-strategy ·

-
-

Trade Log

TimeStrategySideSizePriceFeePnL
+
+
+
+
+
+
+ - -
- -

Saved Backtests click to view

+ +
+
+
+

Strategy Detail

+ +
+
+
+
+
+

Trade History

+
TimeSideSizePricePnLFeeReason / Signal
+
-
diff --git a/live/paper_trader.py b/live/paper_trader.py index e69de29..25d6219 100644 --- a/live/paper_trader.py +++ b/live/paper_trader.py @@ -0,0 +1,660 @@ +""" +Paper trading engine — runs strategies against HYPERLIQUID MAINNET data. + +Pulls real mainnet prices, orderbooks, and funding rates every second. +Executes all 7 strategies in simulation mode — tracks virtual positions, +computes PnL with realistic fees and slippage. No real orders. + +Writes to /tmp/ftdt-paper-metrics.json for the dashboard. +""" +import os, sys, asyncio, json, time, logging, random, math +from pathlib import Path +from datetime import datetime +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 +from strategies.cartea_jaimungal import CarteaJaimungal +from strategies.queue_imbalance import QueueImbalance +from strategies.gueant import GueantMM + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [paper] %(message)s", datefmt="%H:%M:%S") +log = logging.getLogger("ftdt-paper") + +# ═══════════════════════ Config ═══════════════════════ + +MAINNET_API = "https://api.hyperliquid.xyz/info" +METRICS_FILE = "/tmp/ftdt-paper-metrics.json" +STARTING_CAPITAL = 100000.0 # $100,000 paper trading capital +RESERVE = 30000.0 +TAKER_FEE = 0.0005 # 5 bps taker +MAKER_FEE = 0.0002 # 2 bps maker +SLIPPAGE_BPS = 1.0 # 1 bps slippage +MIN_SIGNAL_STRENGTH = 0.25 # Minimum signal strength to overcome fees + +# ═══════════════════════ Strategy state ═══════════════════════ + +STRATEGIES = { + "Order Book Imbalance": { + "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": "reversal", "size": 0.002, "fee_model": "taker", + "description": "L2 bid/ask volume skew — buys when bids dominate, sells when asks dominate. Mean-reverting at volume extremes.", + }, + "Iceberg Detection": { + "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": "momentum", "size": 0.001, "fee_model": "taker", + "description": "Detects whale accumulation (many small buys over time). Follows the smart money flow.", + }, + "Funding Rate Arb": { + "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": "carry", "size": 0.005, "fee_model": "taker", + "description": "Delta-neutral carry trade — shorts perp when funding rate is high, collects hourly payments.", + }, + "Pairs Trading": { + "allocation": 10000.0, "instrument": "ETH", "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": "stat_arb", "size": 0.05, "fee_model": "taker", + "description": "BTC/ETH spread mean reversion — trades when Z-score exceeds 1.5 sigma. Pairs converge back to equilibrium.", + }, + "Avellaneda-Stoikov": { + "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": "market_making", "size": 0.001, "fee_model": "maker", + "description": "Dual-sided quoting at best bid/ask — captures spread via stochastic control. Simulated fill when spread is crossed.", + }, + "Momentum Breakout": { + "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": "momentum", "size": 0.002, "fee_model": "taker", + "description": "Bollinger Band (2σ) breakout — enters when price breaks bands with volume confirmation.", + }, + "Mean Reversion": { + "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": "reversal", "size": 0.002, "fee_model": "taker", + "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, "fee_model": "taker", + "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, "fee_model": "maker", + "description": "Full orderbook depth analysis — wall detection, depth imbalance, thin-side prediction. Uses 10 levels of LOB to find fair value and directional pressure.", + }, + "Cartea-Jaimungal": { + "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": "cartea", "size": 0.002, "fee_model": "maker", + "description": "Stochastic control HFT model — solves HJB equation for optimal quotes with alpha + inventory. Reservation price dynamically shifts to manage risk. (Cartea-Jaimungal 2015)", + }, + "Queue Imbalance": { + "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": "queue_imb", "size": 0.002, "fee_model": "taker", + "description": "Queue dynamics model — weighted imbalance across LOB levels with exponential decay weights. Detects adverse selection when price moves against queue dominance. (Stoikov-Sağlam framework)", + }, + "Guéant Market Making": { + "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": "gueant", "size": 0.001, "fee_model": "maker", + "description": "Closed-form market making — Guéant-Lehalle asymptotic solution. Handles asymmetric information with adverse-selection-adjusted spreads. Computationally efficient closed form.", + }, +} + +trades_log: list[dict] = [] +equity_history: list[dict] = [] +strategy_equity: dict = {name: deque(maxlen=300) for name in STRATEGIES} +per_strategy_trades: dict = {name: deque(maxlen=200) for name in STRATEGIES} +btc_prices: deque = deque(maxlen=120) +eth_prices: deque = deque(maxlen=120) +funding_rates: deque = deque(maxlen=100) + +# ═══════════════════════ Regime Detection ═══════════════════════ +# Uses rolling volatility to classify market regime: +# LOW_VOL: quiet markets → tight spreads, aggressive size +# NORMAL: standard conditions → baseline parameters +# HIGH_VOL: turbulence → wide spreads, reduced size, cautious signals + +current_regime = "NORMAL" +regime_confidence = 0.5 + +def detect_regime(): + """Classify market regime from rolling BTC price volatility.""" + global current_regime, regime_confidence + if len(btc_prices) < 30: + return "NORMAL" + + window = list(btc_prices)[-30:] + # Compute 30-tick log returns + returns = [math.log(window[i] / window[i-1]) for i in range(1, len(window))] + realized_vol = math.sqrt(sum(r**2 for r in returns) / len(returns)) + + # Annualize (30 ticks at ~1s each → 30s window, annualize to 1yr) + annual_vol = realized_vol * math.sqrt(365 * 24 * 60 * 60 / 30) + regime_confidence = min(0.95, max(0.2, annual_vol / 2.0)) + + if annual_vol < 0.15: # <15% annualized + return "LOW_VOL" + elif annual_vol > 0.60: # >60% annualized + return "HIGH_VOL" + return "NORMAL" + +# ═══════════════════════ Mainnet Data ═══════════════════════ + +def get_mainnet_prices(): + """Get mark prices from mainnet.""" + try: + r = requests.post(MAINNET_API, json={"type":"metaAndAssetCtxs"}, timeout=10) + data = r.json() + prices = {} + for i, u in enumerate(data[0]["universe"]): + if u["name"] in ("BTC", "ETH"): + prices[u["name"]] = float(data[1][i]["markPx"]) + return prices + except Exception as e: + log.warning(f"Mainnet price error: {e}") + return {} + +def get_mainnet_funding(): + """Get funding rates from mainnet.""" + try: + r = requests.post(MAINNET_API, json={"type":"metaAndAssetCtxs"}, timeout=10) + data = r.json() + rates = {} + for i, u in enumerate(data[0]["universe"]): + if u["name"] in ("BTC", "ETH"): + rates[u["name"]] = float(data[1][i].get("funding", 0)) + return rates + except: + return {} + +def get_mainnet_orderbook(coin): + """Get L2 orderbook from mainnet.""" + try: + r = requests.post(MAINNET_API, json={"type":"l2Book","coin":coin}, timeout=10) + data = r.json() + best_bid = float(data["levels"][0][0]["px"]) if data["levels"][0] else 0 + best_ask = float(data["levels"][1][0]["px"]) if data["levels"][1] else 0 + 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) +cartea = CarteaJaimungal(gamma=0.1, sigma=0.015, kappa=1.5, T=3600, max_inventory=0.01) +queue_imb = QueueImbalance(depth_levels=10) +gueant = GueantMM(gamma=0.1, sigma=0.015, k=1.5, T=3600, max_pos=0.005) +prev_bids = None +prev_asks = None + +# ═══════════════════════ Signal Engine ═══════════════════════ + +def compute_signals(): + if len(btc_prices) < 20: return + btc = btc_prices[-1]; eth = eth_prices[-1] if eth_prices else btc/34 + + # OFI + if len(btc_prices) >= 5: + ret = (btc - btc_prices[-5]) / btc_prices[-5] + if ret > 0.0005: + STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"SELL","strength":ret}) + elif ret < -0.0005: + STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(ret)}) + + # Iceberg + if len(btc_prices) >= 10: + up = sum(1 for i in range(-9,0) if btc_prices[i+1] > btc_prices[i]) + if up >= 7: + STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"BUY","strength":up/10}) + elif up <= 3: + STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"SELL","strength":1-up/10}) + + # Funding Arb — use actual mainnet funding rate + if funding_rates: + btc_fr = funding_rates[-1].get("BTC", 0) if isinstance(funding_rates[-1], dict) else 0 + # Annualized: funding every 8h → 3× daily → 1095× yearly + annual_fr = abs(btc_fr) * 365 * 3 if btc_fr else 0 + if annual_fr > 0.05: # >5% APR + STRATEGIES["Funding Rate Arb"]["signals"].append( + {"time":time.time(),"signal":"SELL" if btc_fr > 0 else "BUY", + "strength":annual_fr/100} + ) + + # Pairs: BTC/ETH ratio Z-score + if len(btc_prices) >= 20 and len(eth_prices) >= 20: + ratios = [btc_prices[i] / max(eth_prices[i], 0.01) for i in range(-20, 0)] + mu = sum(ratios) / len(ratios) + std = math.sqrt(sum((r-mu)**2 for r in ratios) / len(ratios)) + cur = btc / max(eth, 0.01) + if std > 0: + z = (cur - mu) / std + if z > 1.5: + STRATEGIES["Pairs Trading"]["signals"].append({"time":time.time(),"signal":"SELL_ETH","strength":z}) + elif z < -1.5: + STRATEGIES["Pairs Trading"]["signals"].append({"time":time.time(),"signal":"BUY_ETH","strength":abs(z)}) + + # Momentum Breakout + if len(btc_prices) >= 20: + w = list(btc_prices)[-20:]; sma = sum(w)/len(w) + variance = sum((p-sma)**2 for p in w)/len(w); std = math.sqrt(variance) + if std > 0: + if btc > sma + 2*std: + STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"BUY","strength":(btc-sma-2*std)/std}) + elif btc < sma - 2*std: + STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"SELL","strength":(sma-2*std-btc)/std}) + + # Mean Reversion + if len(btc_prices) >= 20: + w = list(btc_prices)[-20:]; vols = [1 + i/len(w) for i in range(len(w))] + vwap = sum(p*v for p,v in zip(w, vols)) / sum(vols) + vstd = math.sqrt(sum((p-vwap)**2 for p in w) / len(w)) + dev = (btc - vwap) / vstd if vstd > 0 else 0 + if dev > 1.5: + STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"SELL","strength":dev}) + elif dev < -1.5: + STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(dev)}) + + for s in STRATEGIES.values(): + s["signals"] = s["signals"][-20:] + +# ═══════════════════════ Fill Simulation ═══════════════════════ + +def simulate_fill(name: str, side: str, coin: str, price: float, reason: str = ""): + """Simulate a trade fill at market price with strategy-specific fees.""" + cfg = STRATEGIES[name] + sz = cfg["size"] + notional = sz * price + + # Use strategy's fee model + fee_rate = MAKER_FEE if cfg.get("fee_model") == "maker" else TAKER_FEE + fee = notional * fee_rate + slippage = notional * SLIPPAGE_BPS / 10000 + cfg["fee_paid"] += fee + + if side == "BUY": + # Opening or adding long + if cfg["position"] <= 0: + # Close short if any + if cfg["position"] < 0: + # PnL from closing short + close_pnl = abs(cfg["position"]) * (cfg["entry_price"] - price) + cfg["pnl"] += close_pnl + cfg["entry_price"] = 0 + cfg["position"] = 0 + if close_pnl > 0: cfg["wins"] += 1 + trades_log.append({ + "time": datetime.now().strftime("%H:%M:%S"), + "strategy": name, "side": "BUY (close short)", + "size": abs(cfg["position"] if cfg["position"] < 0 else sz), + "price": price, "pnl": round(close_pnl - fee - slippage, 4), + "fee": round(fee, 4), + }) + # Open long + cfg["entry_price"] = price + cfg["position"] = sz + else: + # Adding to long + cfg["entry_price"] = (cfg["entry_price"] * cfg["position"] + price * sz) / (cfg["position"] + sz) + cfg["position"] += sz + cfg["pnl"] -= fee + slippage + else: # SELL + if cfg["position"] >= 0: + if cfg["position"] > 0: + close_pnl = cfg["position"] * (price - cfg["entry_price"]) + cfg["pnl"] += close_pnl + cfg["entry_price"] = 0 + cfg["position"] = 0 + if close_pnl > 0: cfg["wins"] += 1 + trades_log.append({ + "time": datetime.now().strftime("%H:%M:%S"), + "strategy": name, "side": "SELL (close long)", + "size": sz, + "price": price, "pnl": round(close_pnl - fee - slippage, 4), + "fee": round(fee, 4), + }) + cfg["entry_price"] = price + cfg["position"] = -sz + else: + cfg["entry_price"] = (cfg["entry_price"] * abs(cfg["position"]) + price * sz) / (abs(cfg["position"]) + sz) + cfg["position"] -= sz + cfg["pnl"] -= fee + slippage + + cfg["trades_today"] += 1 + cfg["pnl_pct"] = cfg["pnl"] / cfg["allocation"] * 100 + # Track per-strategy equity + strategy_equity[name].append({"t": time.time(), "v": cfg["allocation"] + cfg["pnl"]}) + # Per-strategy trade with reason + trade_entry = { + "time": datetime.now().strftime("%H:%M:%S"), + "side": side, "size": sz, "price": price, + "pnl": round(cfg["pnl"], 4), + "fee": round(fee, 4), + "reason": reason, + "allocation": cfg["allocation"], + "fee_model": cfg.get("fee_model", "taker"), + } + per_strategy_trades[name].append(trade_entry) + + +# ═══════════════════════ A-S Spread Capture ═══════════════════════ + +def simulate_avellaneda(btc_bid, btc_ask): + """Avellaneda-Stoikov: regime-adaptive spread capture. + + Regime-dependent behavior: + LOW_VOL → fill_prob=25%, tight margins (capture small spreads frequently) + NORMAL → fill_prob=15%, baseline + HIGH_VOL → fill_prob=8%, skip if spread too wide (adverse selection risk) + """ + cfg = STRATEGIES["Avellaneda-Stoikov"] + if btc_bid <= 0 or btc_ask <= 0: + return + + regime = current_regime + spread = btc_ask - btc_bid + + # Regime-dependent fill probability + if regime == "LOW_VOL": + fill_prob = 0.25 + elif regime == "HIGH_VOL": + fill_prob = 0.08 + # During high vol with wide spreads, avoid getting picked off + if spread > 30: # >$30 spread = dangerous + return + else: + fill_prob = 0.15 + + if random.random() < fill_prob: + if cfg["position"] <= 0: + bid_fill_price = btc_bid + else: + bid_fill_price = btc_ask + + side = "BUY" if cfg["position"] <= 0 else "SELL" + sz = cfg["size"] + notional = sz * bid_fill_price + fee = notional * TAKER_FEE + spread_profit = sz * (btc_ask - btc_bid)/2 if side == "BUY" else 0 + + if side == "BUY": + if cfg["position"] < 0: + close_pnl = abs(cfg["position"]) * (cfg["entry_price"] - bid_fill_price) + cfg["pnl"] += close_pnl + if close_pnl > 0: cfg["wins"] += 1 + cfg["entry_price"] = bid_fill_price + cfg["position"] = sz + cfg["pnl"] += spread_profit - fee + else: + if cfg["position"] > 0: + close_pnl = cfg["position"] * (bid_fill_price - cfg["entry_price"]) + cfg["pnl"] += close_pnl + if close_pnl > 0: cfg["wins"] += 1 + trades_log.append({ + "time": datetime.now().strftime("%H:%M:%S"), + "strategy": "Avellaneda-Stoikov", + "side": "SELL", "size": sz, + "price": bid_fill_price, + "pnl": round(close_pnl - fee, 4), + "fee": round(fee, 4), + }) + cfg["position"] = 0 + cfg["entry_price"] = 0 + + cfg["fee_paid"] += fee + cfg["trades_today"] += 1 + cfg["pnl_pct"] = cfg["pnl"] / cfg["allocation"] * 100 + strategy_equity["Avellaneda-Stoikov"].append({"t": time.time(), "v": cfg["allocation"] + cfg["pnl"]}) + + +# ═══════════════════════ Metrics ═══════════════════════ + +def write_metrics(): + total_pnl = sum(s["pnl"] for s in STRATEGIES.values()) + total_pnl_pct = (total_pnl / (STARTING_CAPITAL-RESERVE)) * 100 if STARTING_CAPITAL > RESERVE else 0 + for s in STRATEGIES.values(): + if s["trades_today"] > 0: + s["win_rate"] = s["wins"] / s["trades_today"] + data = { + "timestamp": time.time(), + "mode": "paper", + "source": "Hyperliquid Mainnet", + "total_equity": STARTING_CAPITAL + total_pnl, + "base_equity": STARTING_CAPITAL, + "total_pnl": total_pnl, + "total_pnl_pct": total_pnl_pct, + "reserve": RESERVE, + "equity_history": equity_history[-600:], + "strategy_equity": {k: list(v)[-300:] for k, v in strategy_equity.items()}, + "strategies": STRATEGIES, + "trades": trades_log[-200:], + "status": "running", + "btc_price": btc_prices[-1] if btc_prices else 0, + "eth_price": eth_prices[-1] if eth_prices else 0, + "regime": current_regime, + "regime_confidence": regime_confidence, + "per_strategy_trades": {k: list(v)[-100:] for k, v in per_strategy_trades.items()}, + } + try: + with open(METRICS_FILE, "w") as f: + json.dump(data, f, default=str) + except IOError: pass + +# ═══════════════════════ Main ═══════════════════════ + +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" 12 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") + log.info("="*60) + + for s in STRATEGIES.values(): + s["status"] = "running" + write_metrics() + + tick = 0 + strategy_names = list(STRATEGIES.keys()) + idx = 0 + + try: + while True: + global prev_bids, prev_asks + tick += 1 + + # Fetch mainnet data + if tick % 2 == 0: # Every 2 seconds to respect rate limits + prices = get_mainnet_prices() + btc = prices.get("BTC", 0) + eth = prices.get("ETH", 0) + if btc > 0: + btc_prices.append(btc) + if eth > 0: + eth_prices.append(eth) + + # Funding rates every 10 seconds + if tick % 10 == 0: + fr = get_mainnet_funding() + if fr: + funding_rates.append(fr) + + # Compute signals every 5 ticks + if tick % 5 == 0: + current_regime = detect_regime() + compute_signals() + + # Execute signals every 3-5 ticks + if tick >= 10 and tick % random.randint(3, 6) == 0: + btc = btc_prices[-1] if btc_prices else 0 + eth = eth_prices[-1] if eth_prices else 0 + if btc <= 0: continue + + # 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) + + # 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"], + }) + + # Queue Imbalance: weighted queue dynamics + if bids and asks: + qi_result = queue_imb.analyze( + bids, asks, btc, prev_bids, prev_asks, + btc_prices[-2] if len(btc_prices) >= 2 else 0) + if qi_result["signal"]: + STRATEGIES["Queue Imbalance"]["signals"].append({ + "time": time.time(), + "signal": qi_result["signal"], + "strength": qi_result["strength"], + }) + prev_bids, prev_asks = bids, asks + + # Cartea-Jaimungal: stochastic control with alpha estimate + alpha_est = (btc_prices[-1] - btc_prices[-2]) / btc_prices[-2] \ + if len(btc_prices) >= 2 and btc_prices[-2] > 0 else 0 + cj_inv = STRATEGIES["Cartea-Jaimungal"]["position"] + cj_result = cartea.should_trade(btc, alpha_est, cj_inv, tick % 3600) + if cj_result["signal"]: + STRATEGIES["Cartea-Jaimungal"]["signals"].append({ + "time": time.time(), + "signal": cj_result["signal"], + "strength": cj_result["confidence"], + }) + + # Guéant: closed-form market making + gueant_inv = STRATEGIES["Guéant Market Making"]["position"] + g_quotes = gueant.optimal_quotes( + btc, gueant_inv, tick % 3600, + adverse_prob=queue_imb.wqi_history[-1] if queue_imb.wqi_history else 0) + # Simulate fill: if our quote is at/near best, track a signal + if btc_bid > 0 and g_quotes["bid"] >= btc_bid * 0.999: + STRATEGIES["Guéant Market Making"]["signals"].append({ + "time": time.time(), "signal": "BUY", + "strength": 0.5, + }) + elif btc_ask > 0 and g_quotes["ask"] <= btc_ask * 1.001: + STRATEGIES["Guéant Market Making"]["signals"].append({ + "time": time.time(), "signal": "SELL", + "strength": 0.5, + }) + + # 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": + continue # Already handled above + + # Check for signals with strength > fee barrier + if not cfg["signals"]: + continue + + sig = cfg["signals"][-1] + signal_str = str(sig["signal"]) + strength = abs(sig.get("strength", 0)) + signal_reason = sig.get("reason", signal_str) + + # Skip weak signals that can't overcome fees + if strength < MIN_SIGNAL_STRENGTH: + continue + + coin = cfg["instrument"] + px = btc if coin == "BTC" else eth + if px <= 0: continue + + if "BUY" in signal_str.upper(): + simulate_fill(name, "BUY", coin, px, signal_reason) + log.info(f"[{name[:4]:4s}] PAPER BUY {cfg['size']} {coin} @ ${px:,.1f} | PnL: ${cfg['pnl']:+.2f} | {signal_reason}") + elif "SELL" in signal_str.upper(): + simulate_fill(name, "SELL", coin, px, signal_reason) + log.info(f"[{name[:4]:4s}] PAPER SELL {cfg['size']} {coin} @ ${px:,.1f} | PnL: ${cfg['pnl']:+.2f} | {signal_reason}") + + # Equity history + total_pnl = sum(s["pnl"] for s in STRATEGIES.values()) + if tick % 3 == 0: + equity_history.append({"t": time.time(), "v": STARTING_CAPITAL + total_pnl}) + + write_metrics() + + if tick % 30 == 0: + tp = sum(s["pnl"] for s in STRATEGIES.values()) + tr = sum(s["trades_today"] for s in STRATEGIES.values()) + tf = sum(s["fee_paid"] for s in STRATEGIES.values()) + btc_now = btc_prices[-1] if btc_prices else 0 + log.info( + f"Tick {tick:4d} | BTC: ${btc_now:,.0f} | " + f"PnL: ${tp:+.2f} | Trades: {tr:3d} | Fees: ${tf:.2f} | " + f"Regime: {current_regime}" + ) + + await asyncio.sleep(1) + + except KeyboardInterrupt: + log.info("Stopping paper trader...") + + for s in STRATEGIES.values(): + s["status"] = "idle" + write_metrics() + tp = sum(s["pnl"] for s in STRATEGIES.values()) + tr = sum(s["trades_today"] for s in STRATEGIES.values()) + log.info(f"Paper trading stopped. Final PnL: ${tp:+.2f}, Trades: {tr}") + + +if __name__ == "__main__": + asyncio.run(main())