""" 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 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 (realistic for paper fills) SLIPPAGE_BPS = 1.0 # 1 bps slippage # ═══════════════════════ 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, "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, "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, "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, "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, "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, "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, "description": "VWAP deviation — buys below VWAP, sells above. Oscillates around fair value.", }, } trades_log: list[dict] = [] equity_history: list[dict] = [] strategy_equity: dict = {name: deque(maxlen=300) 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 # ═══════════════════════ 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): """Simulate a trade fill at market price with fees.""" cfg = STRATEGIES[name] sz = cfg["size"] notional = sz * price fee = notional * TAKER_FEE 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"]}) # ═══════════════════════ 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, } 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" 7 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: 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 btc_bid, btc_ask = get_mainnet_orderbook("BTC") # Avellaneda-Stoikov: simulate spread capture simulate_avellaneda(btc_bid, btc_ask) # Process next strategy's signals name = strategy_names[idx % 7] idx += 1 cfg = STRATEGIES[name] if name == "Avellaneda-Stoikov": continue # Already handled above # Check for signals if not cfg["signals"]: continue sig = cfg["signals"][-1] signal_str = str(sig["signal"]) 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) log.info(f"[{name[:4]:4s}] PAPER BUY {cfg['size']} {coin} @ ${px:,.1f} | PnL: ${cfg['pnl']:+.2f}") elif "SELL" in signal_str.upper(): simulate_fill(name, "SELL", coin, px) log.info(f"[{name[:4]:4s}] PAPER SELL {cfg['size']} {coin} @ ${px:,.1f} | PnL: ${cfg['pnl']:+.2f}") # 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())