""" 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 # Order Book Imbalance — MOVED to main loop (uses real L2 bid/ask volume) # 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 and isinstance(funding_rates[-1], dict): btc_fr = funding_rates[-1].get("BTC", 0) # Annualized: funding every 8h → 3× daily → 1095× yearly annual_fr = abs(btc_fr) * 365 * 3 if btc_fr else 0 # Log funding rate periodically import random as _random_fr if _random_fr.random() < 0.02: import logging logging.getLogger("ftdt-paper").info( "{} Funding rate: {:.6f}% 8h | {:.2f}% APR | signal={}".format( "[Fund]", btc_fr*100, annual_fr*100, "SELL" if btc_fr > 0 else "BUY" if btc_fr < 0 else "NONE" ) ) if annual_fr > 0.05: # >5% APR (production threshold) STRATEGIES["Funding Rate Arb"]["signals"].append( {"time":time.time(),"signal":"SELL" if btc_fr > 0 else "BUY", "strength": min(0.6, annual_fr * 50), "reason": "funding_{:.1f}pct_apr".format(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)}) # Kalman Pairs: adaptive hedge ratio if len(btc_prices)>=20 and len(eth_prices)>=20: try: from strategies.kalman_pairs import KalmanPairsTrader if "_kalman_paper" not in dir(): globals()["_kalman_paper"] = KalmanPairsTrader( transition_covariance=1e-4, observation_covariance=1e-2, z_entry=2.0, z_exit=0.5, warmup_bars=20, ) result = globals()["_kalman_paper"].step(eth, btc) if result["signal"] != 0: sig = "BUY_ETH" if result["signal"] > 0 else "SELL_ETH" STRATEGIES["Kalman Pairs"]["signals"].append({ "time": time.time(), "signal": sig, "strength": abs(result["z_score"]) }) except: pass # 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 * MAKER_FEE # A-S is a MAKER strategy — pay maker fee, not taker 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) # Order Book Imbalance: real L2 bid/ask volume skew if bids and asks: total_bids = sum(sz for _, sz in bids) total_asks = sum(sz for _, sz in asks) if total_asks > 0 and total_bids > total_asks * 1.5: STRATEGIES["Order Book Imbalance"]["signals"].append({ "time": time.time(), "signal": "BUY", "strength": min(1.0, (total_bids / total_asks - 1.0)), "reason": "bid_skew_{:.1f}x".format(total_bids/total_asks) }) elif total_bids > 0 and total_asks > total_bids * 1.5: STRATEGIES["Order Book Imbalance"]["signals"].append({ "time": time.time(), "signal": "SELL", "strength": min(1.0, (total_asks / total_bids - 1.0)), "reason": "ask_skew_{:.1f}x".format(total_asks/total_bids) }) 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())