From 2176910fab7597a8f9736d12b13d5b509d0d7541 Mon Sep 17 00:00:00 2001 From: ramseshk Date: Thu, 6 Aug 2026 06:19:42 +0000 Subject: [PATCH] QuantReport: handle API error responses, restart paper trader --- dashboard/static/index.html | 4 +- live/node.py.bak | 384 +++++++++++++++++++ live/node.py.bak2 | 425 +++++++++++++++++++++ live/node.py.bak3 | 443 ++++++++++++++++++++++ live/node.py.bak5 | 452 +++++++++++++++++++++++ live/node.py.bak6 | 456 +++++++++++++++++++++++ live/paper_trader.py | 81 ++-- live/paper_trader.py.bak | 712 ++++++++++++++++++++++++++++++++++++ live/paper_trader.py.bak2 | 682 ++++++++++++++++++++++++++++++++++ live/paper_trader.py.bak3 | 699 +++++++++++++++++++++++++++++++++++ 10 files changed, 4308 insertions(+), 30 deletions(-) create mode 100644 live/node.py.bak create mode 100644 live/node.py.bak2 create mode 100644 live/node.py.bak3 create mode 100644 live/node.py.bak5 create mode 100644 live/node.py.bak6 create mode 100644 live/paper_trader.py.bak create mode 100644 live/paper_trader.py.bak2 create mode 100644 live/paper_trader.py.bak3 diff --git a/dashboard/static/index.html b/dashboard/static/index.html index de6052c..36eb086 100644 --- a/dashboard/static/index.html +++ b/dashboard/static/index.html @@ -1,4 +1,4 @@ -Quant Dashboard
Live Testnet
OFFLINE · ···
\ No newline at end of file + text-[#6e7381] hover:text-[#1a1c23]">Historical
\ No newline at end of file diff --git a/live/node.py.bak b/live/node.py.bak new file mode 100644 index 0000000..3ec41e7 --- /dev/null +++ b/live/node.py.bak @@ -0,0 +1,384 @@ +""" +Profitable HFT node — tight POST-ONLY quotes at best bid/ask. + +Uses real orderbook to place maker orders AT the best bid/ask level, +not at mid ± random spread. Refreshes quotes every cycle to stay +at queue front. Avellaneda-Stoikov places dual-sided quotes simultaneously. + +7 strategies x 100 USDC | Maker: 0.02% | Hyperliquid Testnet. +""" +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 nautilus_trader.core.nautilus_pyo3 import ( + HyperliquidHttpClient, HyperliquidEnvironment, + UUID4, ClientOrderId, OrderSide, OrderType, TimeInForce, + Quantity, Price, +) + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s", datefmt="%H:%M:%S") +log = logging.getLogger("ftdt-quant") + +METRICS_FILE = "/tmp/ftdt-metrics.json" +TESTNET_API = "https://api.hyperliquid-testnet.xyz/info" +TOTAL_EQUITY = 898.0 +RESERVE = 398.0 +MAKER_FEE = 0.0002 + +STRATEGIES = { + "Order Book Imbalance": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"reversal","description":"L2 bid/ask volume skew — buys when bids dominate, sells when asks dominate."}, + "Iceberg Detection": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Detects whale TWAP accumulation — follows smart money flow."}, + "Funding Rate Arb": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"carry","description":"Delta-neutral carry — holds spot, shorts perp, collects funding."}, + "Pairs Trading": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.006,"fee_paid":0.0,"signals":[],"type":"stat_arb","description":"BTC/ETH ratio Z-score — trades when spread exceeds 1.5σ."}, + "Avellaneda-Stoikov": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"market_making","description":"Dual-sided quoting at best bid/ask — captures spread via stochastic control. Places both sides simultaneously."}, + "Momentum Breakout": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Bollinger Band (2σ) breakout — enters with volume confirmation."}, + "Mean Reversion": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"reversal","description":"VWAP deviation — buys below VWAP, sells above. Oscillates around fair value."}, +} + +trades_log: list[dict] = [] +equity_history: list[dict] = [] +strategy_equity: dict[str, list] = {} +seen_fills: set[int] = set() +btc_prices: deque = deque(maxlen=60) +eth_prices: deque = deque(maxlen=60) +active_cloids: dict = {} # Track active order IDs per strategy + +# ═══════════════════════ Helpers ═══════════════════════ + +def load_key(): + key = os.getenv("HYPERLIQUID_TESTNET_PK") + if key: return key + env_file = Path(__file__).resolve().parent.parent / ".env" + if env_file.exists(): + for line in env_file.read_text().splitlines(): + if line.startswith("HYPERLIQUID_TESTNET_PK="): + return line.split("=", 1)[1].strip() + return None + +def get_fills(addr): + r = requests.post(TESTNET_API, json={"type":"userFills","user":addr}, timeout=10) + return r.json() if r.status_code==200 else [] + +def get_mark_prices(): + try: + r = requests.post(TESTNET_API, json={"type":"metaAndAssetCtxs"}, timeout=10) + data = r.json() + if not data or data[0] is None or "universe" not in data[0]: + return {} + 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: + return {} + +def get_orderbook(coin): + """Get best bid, best ask, and mid from L2 orderbook.""" + try: + r = requests.post(TESTNET_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, (best_bid+best_ask)/2 if best_bid and best_ask else 0 + except: return 0,0,0 + +def write_metrics(addr): + total_pnl = sum(s["pnl"] for s in STRATEGIES.values()) + total_pnl_pct = (total_pnl/TOTAL_EQUITY)*100 if TOTAL_EQUITY>0 else 0 + for s in STRATEGIES.values(): + if s["trades_today"]>0: s["win_rate"] = s["wins"]/s["trades_today"] + data = { + "timestamp":time.time(),"wallet":addr, + "total_equity":TOTAL_EQUITY+total_pnl,"base_equity":TOTAL_EQUITY, + "total_pnl":total_pnl,"total_pnl_pct":total_pnl_pct, + "reserve":RESERVE,"equity_history":equity_history[-600:], + "strategies":STRATEGIES,"trades":trades_log[-200:],"status":"running","testnet_up":True, + "strategy_equity":{k: v[-600:] for k,v in strategy_equity.items()}, + "open_positions":[],"open_orders":[] + } + try: + with open(METRICS_FILE,"w") as f: json.dump(data,f,default=str) + except IOError: pass + +# ═══════════════════════ Signals ═══════════════════════ + +def compute_signals(): + if len(btc_prices)<20 or len(eth_prices)<10: return + btc = btc_prices[-1]; eth = eth_prices[-1] + + # OFI: 5-tick reversal + if len(btc_prices)>=5: + ret = (btc-btc_prices[-5])/btc_prices[-5] + if ret>0.0008: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"SELL","strength":ret}) + elif ret<-0.0008: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(ret)}) + + # Iceberg: trend count + 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: rate proxy + if len(btc_prices)>=20: + fr = (btc/btc_prices[-20]-1)/20 + if abs(fr)>0.0008: + STRATEGIES["Funding Rate Arb"]["signals"].append({"time":time.time(),"signal":"SELL" if fr>0 else "BUY","strength":abs(fr)}) + + # Pairs: ratio Z-score + if len(btc_prices)>=20 and len(eth_prices)>=20: + ratios = [btc_prices[i]/eth_prices[i] 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/eth if eth>0 else 0 + 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: Bollinger + 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: VWAP + 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)}) + + # Trim signals + for s in STRATEGIES.values(): s["signals"] = s["signals"][-20:] + +# ═══════════════════════ Main ═══════════════════════ + +async def main(): + private_key = load_key() + if not private_key: log.error("No key"); sys.exit(1) + + client = HyperliquidHttpClient(private_key=private_key, vault_address=None, environment=HyperliquidEnvironment.TESTNET) + addr = client.get_user_address() + client.set_account_id("HYPERLIQUID-"+addr) + + # Load instrument definitions — try testnet SDK first, fallback to raw APIs + insts = []; perps = {} + try: + insts = await client.load_instrument_definitions(include_perps=True) + perps = {str(i.id.symbol): i for i in insts if "PERP" in str(i.id.symbol)} + for inst in perps.values(): client.cache_instrument(inst) + except Exception as e: + log.warning(f"SDK instrument load failed: {e}") + if not perps: + log.info("Loading perps from mainnet API directly...") + try: + meta_r = requests.post(MAINNET_INFO, json={"type":"meta"}, timeout=10) + meta = meta_r.json() + for asset in meta.get("universe", []): + name = asset.get("name", "") + if name: + # Build a minimal perp-like object for our purposes + perps[name] = type('Perp', (), { + 'id': type('ID', (), {'symbol': name})(), + 'base': name, + 'quote': 'USD', + })() + log.info(f"Loaded {len(perps)} perps from mainnet meta") + except Exception as e: + log.error(f"Mainnet meta fallback failed: {e}") + if perps: + log.info(f"Perps available: {list(perps.keys())[:10]}...") + else: + log.error("No perps loaded — cannot continue") + sys.exit(1) + # Find BTC/ETH perps dynamically (testnet IDs may differ from mainnet) + btc_perp = None; eth_perp = None + for k, v in perps.items(): + ku = k.upper() + if btc_perp is None and ("BTC" in ku): + btc_perp = v + if eth_perp is None and ("ETH" in ku): + eth_perp = v + if not btc_perp or not eth_perp: + log.error(f"Could not find BTC/ETH perps. Available: {list(perps.keys())[:10]}") + sys.exit(1) + + prices = get_mark_prices() + btc_bid, btc_ask, btc_mid = get_orderbook("BTC") + eth_bid, eth_ask, eth_mid = get_orderbook("ETH") + + log.info("="*60) + log.info(" FTDT Quant Lab — QUOTING AT BEST BID/ASK") + log.info(f" Wallet: {addr}") + log.info(f" BTC: bid=${btc_bid:,.0f} ask=${btc_ask:,.0f} (spread=${btc_ask-btc_bid:.1f})") + log.info(f" ETH: bid=${eth_bid:,.0f} ask=${eth_ask:,.0f} (spread=${eth_ask-eth_bid:.1f})") + log.info(f" Mode: POST-ONLY at best bid/ask | Maker: 0.02%") + log.info(f" 7 strategies | A-S is DUAL-SIDED quoting") + log.info(f" Dashboard: https://ftdt.io/cv") + log.info("="*60) + + # Cancel stale + open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json() + for o in open_ords: + try: + iid = InstrumentId.from_str(f"{o['coin']}-USD-PERP.HYPERLIQUID") + client.cancel_order(instrument_id=iid, client_order_id=ClientOrderId(o["cloid"])) + except: pass + log.info(f"Cleared {len(open_ords)} stale orders") + + existing = get_fills(addr) + for f in existing: seen_fills.add(f.get("tid",0)) + log.info(f"Tracking {len(seen_fills)} existing fills") + + for s in STRATEGIES.values(): s["status"]="running" + for name in STRATEGIES: strategy_equity[name]=[] + write_metrics(addr) + + tick=0; names=list(STRATEGIES.keys()); idx=0 + + try: + while True: + tick+=1 + + prices = get_mark_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) + + # Process fills + fills = get_fills(addr); new_fills=0 + for f in fills: + tid=f.get("tid",0) + if tid in seen_fills: continue + seen_fills.add(tid) + side=f.get("side",""); sz=float(f.get("sz",0)); px=float(f.get("px",0)) + closed_pnl=float(f.get("closedPnl",0)); fee=float(f.get("fee","0")) + + strat=None + for n,cfg in STRATEGIES.items(): + if abs(sz-cfg["size"])<0.00001: strat=n; break + if not strat: continue + + net=closed_pnl-abs(fee) + STRATEGIES[strat]["pnl"]+=net; STRATEGIES[strat]["trades_today"]+=1 + STRATEGIES[strat]["fee_paid"]+=abs(fee) + if closed_pnl>0: STRATEGIES[strat]["wins"]+=1 + STRATEGIES[strat]["pnl_pct"]=STRATEGIES[strat]["pnl"]/STRATEGIES[strat]["allocation"]*100 + strategy_equity[strat].append({"t":time.time(),"v":STRATEGIES[strat]["allocation"]+STRATEGIES[strat]["pnl"]}) + trades_log.append({"time":datetime.now().strftime("%H:%M:%S"),"strategy":strat,"side":"BUY" if side=="B" else "SELL","size":sz,"price":px,"pnl":round(net,4),"fee":round(abs(fee),4)}) + new_fills+=1 + + # Signals every 5 ticks + if tick%5==0: compute_signals() + + # Place/refresh orders every 3-5 ticks + if tick>=3 and tick%random.randint(3,5)==0: + btc_bid, btc_ask, btc_mid = get_orderbook("BTC") + try: + btc_bid, btc_ask, btc_mid = get_orderbook("BTC") + except Exception as e: + log.debug(f"OB BTC error: {e}") + btc_bid = btc_ask = btc_mid = 0 + try: + eth_bid, eth_ask, eth_mid = get_orderbook("ETH") + except Exception as e: + eth_bid = eth_ask = eth_mid = 0 + + name = names[idx%7]; idx+=1; cfg=STRATEGIES[name] + coin="BTC" if "BTC" in cfg["instrument"] else "ETH" + perp=btc_perp if coin=="BTC" else eth_perp + bid=btc_bid if coin=="BTC" else eth_bid + ask=btc_ask if coin=="BTC" else eth_ask + mid=btc_mid if coin=="BTC" else eth_mid + if bid<=0 or ask<=0: continue + + # Cancel previous order for this strategy + if name in active_cloids: + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + + # Determine side from signal or market-making pattern + signal=None + if cfg["signals"]: signal=cfg["signals"][-1]["signal"] if cfg["signals"] else None + + if name=="Avellaneda-Stoikov": + # DUAL-SIDED: place both bid and ask simultaneously + cid_bid=ClientOrderId(str(UUID4())); cid_ask=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid_bid,order_side=OrderSide.BUY,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(bid))),time_in_force=TimeInForce.GTC,post_only=True) + client.submit_order(instrument_id=perp.id,client_order_id=cid_ask,order_side=OrderSide.SELL,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(ask))),time_in_force=TimeInForce.GTC,post_only=True) + log.info(f"[Avel] DUAL: BID {cfg['size']} @ ${int(bid):,} | ASK {cfg['size']} @ ${int(ask):,} | spread=${ask-bid:.1f}") + active_cloids[name]=str(cid_bid) # track one + except Exception as e: log.warning(f"Avel dual error: {str(e)[:60]}") + continue + + # Single-sided for other strategies + side=None; px_level=0 + if signal and "SELL" in str(signal).upper(): + side=OrderSide.SELL; px_level=ask # at best ask (highest fill probability as maker) + elif signal and "BUY" in str(signal).upper(): + side=OrderSide.BUY; px_level=bid # at best bid + else: + # No signal: market-making default — alternate sides at best bid/ask + side=OrderSide.BUY if tick%2==0 else OrderSide.SELL + px_level=bid if side==OrderSide.BUY else ask + + if not side or px_level<=0: continue + + cid=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.GTC,post_only=True) + side_str="BUY " if side==OrderSide.BUY else "SELL" + log.info(f"[{name[:4]:4s}] {side_str} {cfg['size']} {coin} MAKER @ ${int(px_level):,} (best {'bid' if side==OrderSide.BUY else 'ask'}: ${int(px_level):,})") + active_cloids[name]=str(cid) + except Exception as e: + err=str(e) + if "would have immediately matched" in err or "cross" in err.lower(): + # Post-only would cross — fall back to regular limit at same level + cid2=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid2,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.IOC) + log.info(f"[{name[:4]:4s}] {side_str} {cfg['size']} {coin} FILLED @ ${int(px_level):,} (post-only crossed → IOC)") + active_cloids[name]=str(cid2) + except Exception as e2: log.debug(f"[{name[:8]}] fallback failed: {str(e2)[:50]}") + else: log.warning(f"Order [{name[:8]}]: {err[:60]}") + + # Equity + tp=sum(s["pnl"] for s in STRATEGIES.values()) + if tick%2==0: equity_history.append({"t":time.time(),"v":TOTAL_EQUITY+tp}) + write_metrics(addr) + + if tick%20==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()) + log.info(f"Tick {tick:4d} | PnL: ${tp:+.2f} | Trades: {tr:3d} | Fees: ${tf:.4f} | New fills: {new_fills}") + + await asyncio.sleep(1) + except KeyboardInterrupt: log.info("Stopping...") + + # Cancel all + open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json() + for o in open_ords: + try: + iid=InstrumentId.from_str(f"{o['coin']}-USD-PERP.HYPERLIQUID") + client.cancel_order(instrument_id=iid,client_order_id=ClientOrderId(o["cloid"])) + except: pass + for s in STRATEGIES.values(): s["status"]="idle" + write_metrics(addr) + tf=sum(s["fee_paid"] for s in STRATEGIES.values()) + tp=sum(s["pnl"] for s in STRATEGIES.values()) + log.info(f"Stopped. PnL: ${tp:+.2f}, Fees: ${tf:.4f}") + +if __name__=="__main__": asyncio.run(main()) diff --git a/live/node.py.bak2 b/live/node.py.bak2 new file mode 100644 index 0000000..e7dfbb3 --- /dev/null +++ b/live/node.py.bak2 @@ -0,0 +1,425 @@ +""" +Profitable HFT node — tight POST-ONLY quotes at best bid/ask. + +Uses real orderbook to place maker orders AT the best bid/ask level, +not at mid ± random spread. Refreshes quotes every cycle to stay +at queue front. Avellaneda-Stoikov places dual-sided quotes simultaneously. + +7 strategies x 100 USDC | Maker: 0.02% | Hyperliquid Testnet. +""" +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 nautilus_trader.core.nautilus_pyo3 import ( + HyperliquidHttpClient, HyperliquidEnvironment, + UUID4, ClientOrderId, OrderSide, OrderType, TimeInForce, + Quantity, Price, +) + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s", datefmt="%H:%M:%S") +log = logging.getLogger("ftdt-quant") + +METRICS_FILE = "/tmp/ftdt-metrics.json" +TESTNET_API = "https://api.hyperliquid-testnet.xyz/info" +TOTAL_EQUITY = 898.0 +RESERVE = 398.0 +MAKER_FEE = 0.0002 + +STRATEGIES = { + "Order Book Imbalance": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"reversal","description":"L2 bid/ask volume skew — buys when bids dominate, sells when asks dominate."}, + "Iceberg Detection": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Detects whale TWAP accumulation — follows smart money flow."}, + "Funding Rate Arb": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"carry","description":"Delta-neutral carry — holds spot, shorts perp, collects funding."}, + "Pairs Trading": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.006,"fee_paid":0.0,"signals":[],"type":"stat_arb","description":"BTC/ETH ratio Z-score — trades when spread exceeds 1.5σ."}, + "Avellaneda-Stoikov": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"market_making","description":"Dual-sided quoting at best bid/ask — captures spread via stochastic control. Places both sides simultaneously."}, + "Momentum Breakout": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Bollinger Band (2σ) breakout — enters with volume confirmation."}, + "Mean Reversion": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"reversal","description":"VWAP deviation — buys below VWAP, sells above. Oscillates around fair value."}, +} + +trades_log: list[dict] = [] +equity_history: list[dict] = [] +strategy_equity: dict[str, list] = {} +seen_fills: set[int] = set() +btc_prices: deque = deque(maxlen=60) +eth_prices: deque = deque(maxlen=60) +active_cloids: dict = {} # Track active order IDs per strategy +active_cloids_times: dict = {} # Tick when order was placed +active_cloids_px: dict = {} # Entry price for take-profit + +# ═══════════════════════ Helpers ═══════════════════════ + +def load_key(): + key = os.getenv("HYPERLIQUID_TESTNET_PK") + if key: return key + env_file = Path(__file__).resolve().parent.parent / ".env" + if env_file.exists(): + for line in env_file.read_text().splitlines(): + if line.startswith("HYPERLIQUID_TESTNET_PK="): + return line.split("=", 1)[1].strip() + return None + +def get_fills(addr): + r = requests.post(TESTNET_API, json={"type":"userFills","user":addr}, timeout=10) + return r.json() if r.status_code==200 else [] + +def get_mark_prices(): + try: + r = requests.post(TESTNET_API, json={"type":"metaAndAssetCtxs"}, timeout=10) + data = r.json() + if not data or data[0] is None or "universe" not in data[0]: + return {} + 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: + return {} + +def get_orderbook(coin): + """Get best bid, best ask, and mid from L2 orderbook.""" + try: + r = requests.post(TESTNET_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, (best_bid+best_ask)/2 if best_bid and best_ask else 0 + except: return 0,0,0 + +def write_metrics(addr): + total_pnl = sum(s["pnl"] for s in STRATEGIES.values()) + total_pnl_pct = (total_pnl/TOTAL_EQUITY)*100 if TOTAL_EQUITY>0 else 0 + for s in STRATEGIES.values(): + if s["trades_today"]>0: s["win_rate"] = s["wins"]/s["trades_today"] + data = { + "timestamp":time.time(),"wallet":addr, + "total_equity":TOTAL_EQUITY+total_pnl,"base_equity":TOTAL_EQUITY, + "total_pnl":total_pnl,"total_pnl_pct":total_pnl_pct, + "reserve":RESERVE,"equity_history":equity_history[-600:], + "strategies":STRATEGIES,"trades":trades_log[-200:],"status":"running","testnet_up":True, + "strategy_equity":{k: v[-600:] for k,v in strategy_equity.items()}, + "open_positions":[],"open_orders":[] + } + try: + with open(METRICS_FILE,"w") as f: json.dump(data,f,default=str) + except IOError: pass + +# ═══════════════════════ Signals ═══════════════════════ + +def compute_signals(): + if len(btc_prices)<20 or len(eth_prices)<10: return + btc = btc_prices[-1]; eth = eth_prices[-1] + + # OFI: 5-tick reversal + if len(btc_prices)>=5: + ret = (btc-btc_prices[-5])/btc_prices[-5] + if ret>0.0004: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"SELL","strength":ret}) + elif ret<-0.0004: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(ret)}) + + # Iceberg: trend count + if len(btc_prices)>=10: + up = sum(1 for i in range(-9,0) if btc_prices[i+1]>btc_prices[i]) + if up>=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"BUY","strength":up/10}) + elif up<=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"SELL","strength":1-up/10}) + + # Funding Arb: use real funding rate if available, else wider proxy + if len(btc_prices)>=20: + try: + fr = requests.post(TESTNET_API, json={"type":"funding","coin":"BTC"}, timeout=5).json() + if isinstance(fr, list) and fr: + rate = float(fr[0].get("funding_rate", 0)) + else: + rate = (btc/btc_prices[-20]-1)/20 + except: + rate = (btc/btc_prices[-20]-1)/20 + if abs(rate)>0.0001: + STRATEGIES["Funding Rate Arb"]["signals"].append({"time":time.time(),"signal":"SELL" if rate>0 else "BUY","strength":abs(rate)*10000}) + + # Pairs: ratio Z-score + if len(btc_prices)>=20 and len(eth_prices)>=20: + ratios = [btc_prices[i]/eth_prices[i] 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/eth if eth>0 else 0 + 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: Bollinger + 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+1.5*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"BUY","strength":(btc-sma-2*std)/std}) + elif btc < sma-1.5*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"SELL","strength":(sma-2*std-btc)/std}) + + # Mean Reversion: VWAP + 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.0: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"SELL","strength":dev}) + elif dev<-1.0: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(dev)}) + + # Trim signals + for s in STRATEGIES.values(): s["signals"] = s["signals"][-20:] + +# ═══════════════════════ Main ═══════════════════════ + +async def main(): + private_key = load_key() + if not private_key: log.error("No key"); sys.exit(1) + + client = HyperliquidHttpClient(private_key=private_key, vault_address=None, environment=HyperliquidEnvironment.TESTNET) + addr = client.get_user_address() + client.set_account_id("HYPERLIQUID-"+addr) + + # Load instrument definitions — try testnet SDK first, fallback to raw APIs + insts = []; perps = {} + try: + insts = await client.load_instrument_definitions(include_perps=True) + perps = {str(i.id.symbol): i for i in insts if "PERP" in str(i.id.symbol)} + for inst in perps.values(): client.cache_instrument(inst) + except Exception as e: + log.warning(f"SDK instrument load failed: {e}") + if not perps: + log.info("Loading perps from mainnet API directly...") + try: + meta_r = requests.post(MAINNET_INFO, json={"type":"meta"}, timeout=10) + meta = meta_r.json() + for asset in meta.get("universe", []): + name = asset.get("name", "") + if name: + # Build a minimal perp-like object for our purposes + perps[name] = type('Perp', (), { + 'id': type('ID', (), {'symbol': name})(), + 'base': name, + 'quote': 'USD', + })() + log.info(f"Loaded {len(perps)} perps from mainnet meta") + except Exception as e: + log.error(f"Mainnet meta fallback failed: {e}") + if perps: + log.info(f"Perps available: {list(perps.keys())[:10]}...") + else: + log.error("No perps loaded — cannot continue") + sys.exit(1) + # Find BTC/ETH perps dynamically (testnet IDs may differ from mainnet) + btc_perp = None; eth_perp = None + for k, v in perps.items(): + ku = k.upper() + if btc_perp is None and ("BTC" in ku): + btc_perp = v + if eth_perp is None and ("ETH" in ku): + eth_perp = v + if not btc_perp or not eth_perp: + log.error(f"Could not find BTC/ETH perps. Available: {list(perps.keys())[:10]}") + sys.exit(1) + + prices = get_mark_prices() + btc_bid, btc_ask, btc_mid = get_orderbook("BTC") + eth_bid, eth_ask, eth_mid = get_orderbook("ETH") + + log.info("="*60) + log.info(" FTDT Quant Lab — QUOTING AT BEST BID/ASK") + log.info(f" Wallet: {addr}") + log.info(f" BTC: bid=${btc_bid:,.0f} ask=${btc_ask:,.0f} (spread=${btc_ask-btc_bid:.1f})") + log.info(f" ETH: bid=${eth_bid:,.0f} ask=${eth_ask:,.0f} (spread=${eth_ask-eth_bid:.1f})") + log.info(f" Mode: POST-ONLY at best bid/ask | Maker: 0.02%") + log.info(f" 7 strategies | A-S is DUAL-SIDED quoting") + log.info(f" Dashboard: https://ftdt.io/cv") + log.info("="*60) + + # Cancel stale + open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json() + for o in open_ords: + try: + iid = InstrumentId.from_str(f"{o['coin']}-USD-PERP.HYPERLIQUID") + client.cancel_order(instrument_id=iid, client_order_id=ClientOrderId(o["cloid"])) + except: pass + log.info(f"Cleared {len(open_ords)} stale orders") + + existing = get_fills(addr) + for f in existing: seen_fills.add(f.get("tid",0)) + log.info(f"Tracking {len(seen_fills)} existing fills") + + for s in STRATEGIES.values(): s["status"]="running" + for name in STRATEGIES: strategy_equity[name]=[] + write_metrics(addr) + + tick=0; names=list(STRATEGIES.keys()); idx=0 + + try: + while True: + tick+=1 + + prices = get_mark_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) + + # Process fills + fills = get_fills(addr); new_fills=0 + for f in fills: + tid=f.get("tid",0) + if tid in seen_fills: continue + seen_fills.add(tid) + side=f.get("side",""); sz=float(f.get("sz",0)); px=float(f.get("px",0)) + closed_pnl=float(f.get("closedPnl",0)); fee=float(f.get("fee","0")) + + strat=None + for n,cfg in STRATEGIES.items(): + if abs(sz-cfg["size"])<0.00001: strat=n; break + if not strat: continue + + net=closed_pnl-abs(fee) + STRATEGIES[strat]["pnl"]+=net; STRATEGIES[strat]["trades_today"]+=1 + STRATEGIES[strat]["fee_paid"]+=abs(fee) + if closed_pnl>0: STRATEGIES[strat]["wins"]+=1 + STRATEGIES[strat]["pnl_pct"]=STRATEGIES[strat]["pnl"]/STRATEGIES[strat]["allocation"]*100 + strategy_equity[strat].append({"t":time.time(),"v":STRATEGIES[strat]["allocation"]+STRATEGIES[strat]["pnl"]}) + trades_log.append({"time":datetime.now().strftime("%H:%M:%S"),"strategy":strat,"side":"BUY" if side=="B" else "SELL","size":sz,"price":px,"pnl":round(net,4),"fee":round(abs(fee),4)}) + new_fills+=1 + + # Signals every 5 ticks + if tick%5==0: compute_signals() + + # Execute ALL strategies every 4 seconds + if tick>=3 and tick%4==0: + btc_bid, btc_ask, btc_mid = get_orderbook("BTC") + try: + eth_bid, eth_ask, eth_mid = get_orderbook("ETH") + except Exception as e: + eth_bid = eth_ask = eth_mid = 0 + if btc_bid<=0 or btc_ask<=0: continue + + for name in names: + cfg=STRATEGIES[name] + coin="BTC" if "BTC" in cfg["instrument"] else "ETH" + perp=btc_perp if coin=="BTC" else eth_perp + bid=btc_bid if coin=="BTC" else eth_bid + ask=btc_ask if coin=="BTC" else eth_ask + mid=btc_mid if coin=="BTC" else eth_mid + if bid<=0 or ask<=0: continue + + # Check if this strategy has a position; skip if already filled + has_position = name in active_cloids and tick - active_cloids_times.get(name,0) < 60 + + # Determine signal + signal=None + if cfg["signals"]: + latest = cfg["signals"][-1] + # Only use recent signals (< 10 seconds old) + if time.time() - latest["time"] < 10: + signal=latest["signal"] + + # Close on opposing signal + if has_position and signal: + prev_signal = active_cloids.get(name,"") + if ("BUY" in str(signal).upper() and "SELL" in str(prev_signal).upper()) or ("SELL" in str(signal).upper() and "BUY" in str(prev_signal).upper()): + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + + # Take-profit: close if price moved 2x fee in our favor + if has_position: + entry_px = active_cloids_px.get(name, 0) + if entry_px > 0: + if "BUY" in str(active_cloids[name]).upper() and mid > entry_px * 1.001: + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + elif "SELL" in str(active_cloids[name]).upper() and mid < entry_px * 0.999: + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + + if has_position: continue # Don't replace existing orders + + # Avellaneda-Stoikov: DUAL-SIDED (always active) + if name=="Avellaneda-Stoikov": + cid_bid=ClientOrderId(str(UUID4())); cid_ask=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid_bid,order_side=OrderSide.BUY,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(bid))),time_in_force=TimeInForce.GTC,post_only=True) + client.submit_order(instrument_id=perp.id,client_order_id=cid_ask,order_side=OrderSide.SELL,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(ask))),time_in_force=TimeInForce.GTC,post_only=True) + if tick%60==0: + log.info(f"[Avel] DUAL: BID {cfg['size']} @ ${int(bid):,} | ASK {cfg['size']} @ ${int(ask):,}") + active_cloids[name]=str(cid_bid) + active_cloids_times[name]=tick + active_cloids_px[name]=bid + except Exception as e: pass + continue + + # For signal-driven strategies: use aggressive offset + if signal: + side=OrderSide.SELL if "SELL" in str(signal).upper() else OrderSide.BUY + # Aggressive: 0.03% inside the spread for higher fill probability + offset = int(mid * 0.0003) + px_level = ask - offset if side==OrderSide.SELL else bid + offset + px_level = max(px_level, 1) + else: + # No signal/default: skip (don't random-trade) + continue + + if px_level<=0: continue + + cid=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.GTC,post_only=True) + if tick%60==0: + side_str="BUY" if side==OrderSide.BUY else "SELL" + log.info(f"[{name[:4]:4s}] {side_str} {cfg['size']} @ ${int(px_level):,} ({'best bid '+str(int(bid)) if side==OrderSide.BUY else 'best ask '+str(int(ask))})") + active_cloids[name]=str(cid) + active_cloids_times[name]=tick + active_cloids_px[name]=px_level + except Exception as e: + err=str(e) + if "would have immediately matched" in err or "cross" in err.lower(): + cid2=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid2,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.IOC) + active_cloids[name]=str(cid2) + active_cloids_times[name]=tick + active_cloids_px[name]=px_level + except: pass + + # Equity + tp=sum(s["pnl"] for s in STRATEGIES.values()) + if tick%2==0: equity_history.append({"t":time.time(),"v":TOTAL_EQUITY+tp}) + write_metrics(addr) + + if tick%20==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()) + log.info(f"Tick {tick:4d} | PnL: ${tp:+.2f} | Trades: {tr:3d} | Fees: ${tf:.4f} | New fills: {new_fills}") + + await asyncio.sleep(1) + except KeyboardInterrupt: log.info("Stopping...") + + # Cancel all + open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json() + for o in open_ords: + try: + iid=InstrumentId.from_str(f"{o['coin']}-USD-PERP.HYPERLIQUID") + client.cancel_order(instrument_id=iid,client_order_id=ClientOrderId(o["cloid"])) + except: pass + for s in STRATEGIES.values(): s["status"]="idle" + write_metrics(addr) + tf=sum(s["fee_paid"] for s in STRATEGIES.values()) + tp=sum(s["pnl"] for s in STRATEGIES.values()) + log.info(f"Stopped. PnL: ${tp:+.2f}, Fees: ${tf:.4f}") + +if __name__=="__main__": asyncio.run(main()) diff --git a/live/node.py.bak3 b/live/node.py.bak3 new file mode 100644 index 0000000..48f6439 --- /dev/null +++ b/live/node.py.bak3 @@ -0,0 +1,443 @@ +""" +Profitable HFT node — tight POST-ONLY quotes at best bid/ask. + +Uses real orderbook to place maker orders AT the best bid/ask level, +not at mid ± random spread. Refreshes quotes every cycle to stay +at queue front. Avellaneda-Stoikov places dual-sided quotes simultaneously. + +7 strategies x 100 USDC | Maker: 0.02% | Hyperliquid Testnet. +""" +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 nautilus_trader.core.nautilus_pyo3 import ( + HyperliquidHttpClient, HyperliquidEnvironment, + UUID4, ClientOrderId, OrderSide, OrderType, TimeInForce, + Quantity, Price, +) + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s", datefmt="%H:%M:%S") +log = logging.getLogger("ftdt-quant") + +METRICS_FILE = "/tmp/ftdt-metrics.json" +TESTNET_API = "https://api.hyperliquid-testnet.xyz/info" +TOTAL_EQUITY = 898.0 +RESERVE = 398.0 +MAKER_FEE = 0.0002 + +STRATEGIES = { + "Order Book Imbalance": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"reversal","description":"L2 bid/ask volume skew — buys when bids dominate, sells when asks dominate."}, + "Iceberg Detection": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Detects whale TWAP accumulation — follows smart money flow."}, + "Funding Rate Arb": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"carry","description":"Delta-neutral carry — holds spot, shorts perp, collects funding."}, + "Pairs Trading": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.006,"fee_paid":0.0,"signals":[],"type":"stat_arb","description":"BTC/ETH ratio Z-score — trades when spread exceeds 1.5σ."}, + "Avellaneda-Stoikov": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"market_making","description":"Dual-sided quoting at best bid/ask — captures spread via stochastic control. Places both sides simultaneously."}, + "Momentum Breakout": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Bollinger Band (2σ) breakout — enters with volume confirmation."}, + "Mean Reversion": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"reversal","description":"VWAP deviation — buys below VWAP, sells above. Oscillates around fair value."}, + "Kalman Pairs": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.006,"fee_paid":0.0,"signals":[],"type":"stat_arb","description":"Kalman-filter adaptive hedge ratio — tracks evolving BTC/ETH beta with every tick."} +} + +trades_log: list[dict] = [] +equity_history: list[dict] = [] +strategy_equity: dict[str, list] = {} +seen_fills: set[int] = set() +btc_prices: deque = deque(maxlen=60) +eth_prices: deque = deque(maxlen=60) +active_cloids: dict = {} # Track active order IDs per strategy +active_cloids_times: dict = {} # Tick when order was placed +active_cloids_px: dict = {} # Entry price for take-profit + +# ═══════════════════════ Helpers ═══════════════════════ + +def load_key(): + key = os.getenv("HYPERLIQUID_TESTNET_PK") + if key: return key + env_file = Path(__file__).resolve().parent.parent / ".env" + if env_file.exists(): + for line in env_file.read_text().splitlines(): + if line.startswith("HYPERLIQUID_TESTNET_PK="): + return line.split("=", 1)[1].strip() + return None + +def get_fills(addr): + r = requests.post(TESTNET_API, json={"type":"userFills","user":addr}, timeout=10) + return r.json() if r.status_code==200 else [] + +def get_mark_prices(): + try: + r = requests.post(TESTNET_API, json={"type":"metaAndAssetCtxs"}, timeout=10) + data = r.json() + if not data or data[0] is None or "universe" not in data[0]: + return {} + 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: + return {} + +def get_orderbook(coin): + """Get best bid, best ask, and mid from L2 orderbook.""" + try: + r = requests.post(TESTNET_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, (best_bid+best_ask)/2 if best_bid and best_ask else 0 + except: return 0,0,0 + +def write_metrics(addr): + total_pnl = sum(s["pnl"] for s in STRATEGIES.values()) + total_pnl_pct = (total_pnl/TOTAL_EQUITY)*100 if TOTAL_EQUITY>0 else 0 + for s in STRATEGIES.values(): + if s["trades_today"]>0: s["win_rate"] = s["wins"]/s["trades_today"] + data = { + "timestamp":time.time(),"wallet":addr, + "total_equity":TOTAL_EQUITY+total_pnl,"base_equity":TOTAL_EQUITY, + "total_pnl":total_pnl,"total_pnl_pct":total_pnl_pct, + "reserve":RESERVE,"equity_history":equity_history[-600:], + "strategies":STRATEGIES,"trades":trades_log[-200:],"status":"running","testnet_up":True, + "strategy_equity":{k: v[-600:] for k,v in strategy_equity.items()}, + "open_positions":[],"open_orders":[] + } + try: + with open(METRICS_FILE,"w") as f: json.dump(data,f,default=str) + except IOError: pass + +# ═══════════════════════ Signals ═══════════════════════ + +def compute_signals(): + if len(btc_prices)<20 or len(eth_prices)<10: return + btc = btc_prices[-1]; eth = eth_prices[-1] + + # OFI: 5-tick reversal + if len(btc_prices)>=5: + ret = (btc-btc_prices[-5])/btc_prices[-5] + if ret>0.0004: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"SELL","strength":ret}) + elif ret<-0.0004: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(ret)}) + + # Iceberg: trend count + if len(btc_prices)>=10: + up = sum(1 for i in range(-9,0) if btc_prices[i+1]>btc_prices[i]) + if up>=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"BUY","strength":up/10}) + elif up<=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"SELL","strength":1-up/10}) + + # Funding Arb: use real funding rate if available, else wider proxy + if len(btc_prices)>=20: + try: + fr = requests.post(TESTNET_API, json={"type":"funding","coin":"BTC"}, timeout=5).json() + if isinstance(fr, list) and fr: + rate = float(fr[0].get("funding_rate", 0)) + else: + rate = (btc/btc_prices[-20]-1)/20 + except: + rate = (btc/btc_prices[-20]-1)/20 + if abs(rate)>0.0001: + STRATEGIES["Funding Rate Arb"]["signals"].append({"time":time.time(),"signal":"SELL" if rate>0 else "BUY","strength":abs(rate)*10000}) + + # Pairs: ratio Z-score + if len(btc_prices)>=20 and len(eth_prices)>=20: + ratios = [btc_prices[i]/eth_prices[i] 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/eth if eth>0 else 0 + 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 via Kalman filter (falls back to Pairs logic) + if len(btc_prices)>=20 and len(eth_prices)>=20: + try: + from strategies.kalman_pairs import KalmanPairsTrader + if "_kalman_live" not in dir(): + globals()["_kalman_live"] = KalmanPairsTrader( + transition_covariance=1e-4, observation_covariance=1e-2, + z_entry=2.0, z_exit=0.5, warmup_bars=20, + ) + result = globals()["_kalman_live"].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: Bollinger + 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+1.5*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"BUY","strength":(btc-sma-2*std)/std}) + elif btc < sma-1.5*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"SELL","strength":(sma-2*std-btc)/std}) + + # Mean Reversion: VWAP + 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.0: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"SELL","strength":dev}) + elif dev<-1.0: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(dev)}) + + # Trim signals + for s in STRATEGIES.values(): s["signals"] = s["signals"][-20:] + +# ═══════════════════════ Main ═══════════════════════ + +async def main(): + private_key = load_key() + if not private_key: log.error("No key"); sys.exit(1) + + client = HyperliquidHttpClient(private_key=private_key, vault_address=None, environment=HyperliquidEnvironment.TESTNET) + addr = client.get_user_address() + client.set_account_id("HYPERLIQUID-"+addr) + + # Load instrument definitions — try testnet SDK first, fallback to raw APIs + insts = []; perps = {} + try: + insts = await client.load_instrument_definitions(include_perps=True) + perps = {str(i.id.symbol): i for i in insts if "PERP" in str(i.id.symbol)} + for inst in perps.values(): client.cache_instrument(inst) + except Exception as e: + log.warning(f"SDK instrument load failed: {e}") + if not perps: + log.info("Loading perps from mainnet API directly...") + try: + meta_r = requests.post(MAINNET_INFO, json={"type":"meta"}, timeout=10) + meta = meta_r.json() + for asset in meta.get("universe", []): + name = asset.get("name", "") + if name: + # Build a minimal perp-like object for our purposes + perps[name] = type('Perp', (), { + 'id': type('ID', (), {'symbol': name})(), + 'base': name, + 'quote': 'USD', + })() + log.info(f"Loaded {len(perps)} perps from mainnet meta") + except Exception as e: + log.error(f"Mainnet meta fallback failed: {e}") + if perps: + log.info(f"Perps available: {list(perps.keys())[:10]}...") + else: + log.error("No perps loaded — cannot continue") + sys.exit(1) + # Find BTC/ETH perps dynamically (testnet IDs may differ from mainnet) + btc_perp = None; eth_perp = None + for k, v in perps.items(): + ku = k.upper() + if btc_perp is None and ("BTC" in ku): + btc_perp = v + if eth_perp is None and ("ETH" in ku): + eth_perp = v + if not btc_perp or not eth_perp: + log.error(f"Could not find BTC/ETH perps. Available: {list(perps.keys())[:10]}") + sys.exit(1) + + prices = get_mark_prices() + btc_bid, btc_ask, btc_mid = get_orderbook("BTC") + eth_bid, eth_ask, eth_mid = get_orderbook("ETH") + + log.info("="*60) + log.info(" FTDT Quant Lab — QUOTING AT BEST BID/ASK") + log.info(f" Wallet: {addr}") + log.info(f" BTC: bid=${btc_bid:,.0f} ask=${btc_ask:,.0f} (spread=${btc_ask-btc_bid:.1f})") + log.info(f" ETH: bid=${eth_bid:,.0f} ask=${eth_ask:,.0f} (spread=${eth_ask-eth_bid:.1f})") + log.info(f" Mode: POST-ONLY at best bid/ask | Maker: 0.02%") + log.info(f" 7 strategies | A-S is DUAL-SIDED quoting") + log.info(f" Dashboard: https://ftdt.io/cv") + log.info("="*60) + + # Cancel stale + open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json() + for o in open_ords: + try: + iid = InstrumentId.from_str(f"{o['coin']}-USD-PERP.HYPERLIQUID") + client.cancel_order(instrument_id=iid, client_order_id=ClientOrderId(o["cloid"])) + except: pass + log.info(f"Cleared {len(open_ords)} stale orders") + + existing = get_fills(addr) + for f in existing: seen_fills.add(f.get("tid",0)) + log.info(f"Tracking {len(seen_fills)} existing fills") + + for s in STRATEGIES.values(): s["status"]="running" + for name in STRATEGIES: strategy_equity[name]=[] + write_metrics(addr) + + tick=0; names=list(STRATEGIES.keys()); idx=0 + + try: + while True: + tick+=1 + + prices = get_mark_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) + + # Process fills + fills = get_fills(addr); new_fills=0 + for f in fills: + tid=f.get("tid",0) + if tid in seen_fills: continue + seen_fills.add(tid) + side=f.get("side",""); sz=float(f.get("sz",0)); px=float(f.get("px",0)) + closed_pnl=float(f.get("closedPnl",0)); fee=float(f.get("fee","0")) + + strat=None + for n,cfg in STRATEGIES.items(): + if abs(sz-cfg["size"])<0.00001: strat=n; break + if not strat: continue + + net=closed_pnl-abs(fee) + STRATEGIES[strat]["pnl"]+=net; STRATEGIES[strat]["trades_today"]+=1 + STRATEGIES[strat]["fee_paid"]+=abs(fee) + if closed_pnl>0: STRATEGIES[strat]["wins"]+=1 + STRATEGIES[strat]["pnl_pct"]=STRATEGIES[strat]["pnl"]/STRATEGIES[strat]["allocation"]*100 + strategy_equity[strat].append({"t":time.time(),"v":STRATEGIES[strat]["allocation"]+STRATEGIES[strat]["pnl"]}) + trades_log.append({"time":datetime.now().strftime("%H:%M:%S"),"strategy":strat,"side":"BUY" if side=="B" else "SELL","size":sz,"price":px,"pnl":round(net,4),"fee":round(abs(fee),4)}) + new_fills+=1 + + # Signals every 5 ticks + if tick%5==0: compute_signals() + + # Execute ALL strategies every 4 seconds + if tick>=3 and tick%4==0: + btc_bid, btc_ask, btc_mid = get_orderbook("BTC") + try: + eth_bid, eth_ask, eth_mid = get_orderbook("ETH") + except Exception as e: + eth_bid = eth_ask = eth_mid = 0 + if btc_bid<=0 or btc_ask<=0: continue + + for name in names: + cfg=STRATEGIES[name] + coin="BTC" if "BTC" in cfg["instrument"] else "ETH" + perp=btc_perp if coin=="BTC" else eth_perp + bid=btc_bid if coin=="BTC" else eth_bid + ask=btc_ask if coin=="BTC" else eth_ask + mid=btc_mid if coin=="BTC" else eth_mid + if bid<=0 or ask<=0: continue + + # Check if this strategy has a position; skip if already filled + has_position = name in active_cloids and tick - active_cloids_times.get(name,0) < 60 + + # Determine signal + signal=None + if cfg["signals"]: + latest = cfg["signals"][-1] + # Only use recent signals (< 10 seconds old) + if time.time() - latest["time"] < 10: + signal=latest["signal"] + + # Close on opposing signal + if has_position and signal: + prev_signal = active_cloids.get(name,"") + if ("BUY" in str(signal).upper() and "SELL" in str(prev_signal).upper()) or ("SELL" in str(signal).upper() and "BUY" in str(prev_signal).upper()): + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + + # Take-profit: close if price moved 2x fee in our favor + if has_position: + entry_px = active_cloids_px.get(name, 0) + if entry_px > 0: + if "BUY" in str(active_cloids[name]).upper() and mid > entry_px * 1.001: + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + elif "SELL" in str(active_cloids[name]).upper() and mid < entry_px * 0.999: + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + + if has_position: continue # Don't replace existing orders + + # Avellaneda-Stoikov: DUAL-SIDED (always active) + if name=="Avellaneda-Stoikov": + cid_bid=ClientOrderId(str(UUID4())); cid_ask=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid_bid,order_side=OrderSide.BUY,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(bid))),time_in_force=TimeInForce.GTC,post_only=True) + client.submit_order(instrument_id=perp.id,client_order_id=cid_ask,order_side=OrderSide.SELL,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(ask))),time_in_force=TimeInForce.GTC,post_only=True) + if tick%60==0: + log.info(f"[Avel] DUAL: BID {cfg['size']} @ ${int(bid):,} | ASK {cfg['size']} @ ${int(ask):,}") + active_cloids[name]=str(cid_bid) + active_cloids_times[name]=tick + active_cloids_px[name]=bid + except Exception as e: pass + continue + + # For signal-driven strategies: use aggressive offset + if signal: + side=OrderSide.SELL if "SELL" in str(signal).upper() else OrderSide.BUY + # Aggressive: 0.03% inside the spread for higher fill probability + offset = int(mid * 0.0003) + px_level = ask - offset if side==OrderSide.SELL else bid + offset + px_level = max(px_level, 1) + else: + # No signal/default: skip (don't random-trade) + continue + + if px_level<=0: continue + + cid=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.GTC,post_only=True) + if tick%60==0: + side_str="BUY" if side==OrderSide.BUY else "SELL" + log.info(f"[{name[:4]:4s}] {side_str} {cfg['size']} @ ${int(px_level):,} ({'best bid '+str(int(bid)) if side==OrderSide.BUY else 'best ask '+str(int(ask))})") + active_cloids[name]=str(cid) + active_cloids_times[name]=tick + active_cloids_px[name]=px_level + except Exception as e: + err=str(e) + if "would have immediately matched" in err or "cross" in err.lower(): + cid2=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid2,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.IOC) + active_cloids[name]=str(cid2) + active_cloids_times[name]=tick + active_cloids_px[name]=px_level + except: pass + + # Equity + tp=sum(s["pnl"] for s in STRATEGIES.values()) + if tick%2==0: equity_history.append({"t":time.time(),"v":TOTAL_EQUITY+tp}) + write_metrics(addr) + + if tick%20==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()) + log.info(f"Tick {tick:4d} | PnL: ${tp:+.2f} | Trades: {tr:3d} | Fees: ${tf:.4f} | New fills: {new_fills}") + + await asyncio.sleep(1) + except KeyboardInterrupt: log.info("Stopping...") + + # Cancel all + open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json() + for o in open_ords: + try: + iid=InstrumentId.from_str(f"{o['coin']}-USD-PERP.HYPERLIQUID") + client.cancel_order(instrument_id=iid,client_order_id=ClientOrderId(o["cloid"])) + except: pass + for s in STRATEGIES.values(): s["status"]="idle" + write_metrics(addr) + tf=sum(s["fee_paid"] for s in STRATEGIES.values()) + tp=sum(s["pnl"] for s in STRATEGIES.values()) + log.info(f"Stopped. PnL: ${tp:+.2f}, Fees: ${tf:.4f}") + +if __name__=="__main__": asyncio.run(main()) diff --git a/live/node.py.bak5 b/live/node.py.bak5 new file mode 100644 index 0000000..6a9d26e --- /dev/null +++ b/live/node.py.bak5 @@ -0,0 +1,452 @@ +""" +Profitable HFT node — tight POST-ONLY quotes at best bid/ask. + +Uses real orderbook to place maker orders AT the best bid/ask level, +not at mid ± random spread. Refreshes quotes every cycle to stay +at queue front. Avellaneda-Stoikov places dual-sided quotes simultaneously. + +7 strategies x 100 USDC | Maker: 0.02% | Hyperliquid Testnet. +""" +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 nautilus_trader.core.nautilus_pyo3 import ( + HyperliquidHttpClient, HyperliquidEnvironment, + UUID4, ClientOrderId, OrderSide, OrderType, TimeInForce, + Quantity, Price, +) + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s", datefmt="%H:%M:%S") +log = logging.getLogger("ftdt-quant") + +METRICS_FILE = "/tmp/ftdt-metrics.json" +TESTNET_API = "https://api.hyperliquid-testnet.xyz/info" +TOTAL_EQUITY = 898.0 +RESERVE = 398.0 +MAKER_FEE = 0.0002 + +STRATEGIES = { + "Order Book Imbalance": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000250,"fee_paid":0.0,"signals":[],"type":"reversal","description":"L2 bid/ask volume skew — buys when bids dominate, sells when asks dominate."}, + "Iceberg Detection": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Detects whale TWAP accumulation — follows smart money flow."}, + "Funding Rate Arb": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"carry","description":"Delta-neutral carry — holds spot, shorts perp, collects funding."}, + "Pairs Trading": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.006,"fee_paid":0.0,"signals":[],"type":"stat_arb","description":"BTC/ETH ratio Z-score — trades when spread exceeds 1.5σ."}, + "Avellaneda-Stoikov": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"market_making","description":"Dual-sided quoting at best bid/ask — captures spread via stochastic control. Places both sides simultaneously."}, + "Momentum Breakout": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Bollinger Band (2σ) breakout — enters with volume confirmation."}, + "Mean Reversion": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0002,"fee_paid":0.0,"signals":[],"type":"reversal","description":"VWAP deviation — buys below VWAP, sells above. Oscillates around fair value."}, + "Kalman Pairs": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.006,"fee_paid":0.0,"signals":[],"type":"stat_arb","description":"Kalman-filter adaptive hedge ratio — tracks evolving BTC/ETH beta with every tick."} +} + +trades_log: list[dict] = [] +equity_history: list[dict] = [] +strategy_equity: dict[str, list] = {} +seen_fills: set[int] = set() +btc_prices: deque = deque(maxlen=60) +eth_prices: deque = deque(maxlen=60) +active_cloids: dict = {} # Track active order IDs per strategy +active_cloids_times: dict = {} # Tick when order was placed +active_cloids_px: dict = {} # Entry price for take-profit + +# ═══════════════════════ Helpers ═══════════════════════ + +def load_key(): + key = os.getenv("HYPERLIQUID_TESTNET_PK") + if key: return key + env_file = Path(__file__).resolve().parent.parent / ".env" + if env_file.exists(): + for line in env_file.read_text().splitlines(): + if line.startswith("HYPERLIQUID_TESTNET_PK="): + return line.split("=", 1)[1].strip() + return None + +def get_fills(addr): + r = requests.post(TESTNET_API, json={"type":"userFills","user":addr}, timeout=10) + return r.json() if r.status_code==200 else [] + +def get_mark_prices(): + try: + r = requests.post(TESTNET_API, json={"type":"metaAndAssetCtxs"}, timeout=10) + data = r.json() + if not data or data[0] is None or "universe" not in data[0]: + return {} + 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: + return {} + +def get_orderbook(coin): + """Get best bid, best ask, and mid from L2 orderbook.""" + try: + r = requests.post(TESTNET_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, (best_bid+best_ask)/2 if best_bid and best_ask else 0 + except: return 0,0,0 + +def write_metrics(addr): + total_pnl = sum(s["pnl"] for s in STRATEGIES.values()) + total_pnl_pct = (total_pnl/TOTAL_EQUITY)*100 if TOTAL_EQUITY>0 else 0 + for s in STRATEGIES.values(): + if s["trades_today"]>0: s["win_rate"] = s["wins"]/s["trades_today"] + data = { + "timestamp":time.time(),"wallet":addr, + "total_equity":TOTAL_EQUITY+total_pnl,"base_equity":TOTAL_EQUITY, + "total_pnl":total_pnl,"total_pnl_pct":total_pnl_pct, + "reserve":RESERVE,"equity_history":equity_history[-600:], + "strategies":STRATEGIES,"trades":trades_log[-200:],"status":"running","testnet_up":True, + "strategy_equity":{k: v[-600:] for k,v in strategy_equity.items()}, + "open_positions":[],"open_orders":[] + } + try: + with open(METRICS_FILE,"w") as f: json.dump(data,f,default=str) + except IOError: pass + +# ═══════════════════════ Signals ═══════════════════════ + +def compute_signals(): + if len(btc_prices)<20 or len(eth_prices)<10: return + btc = btc_prices[-1]; eth = eth_prices[-1] + + # OFI: 5-tick reversal + if len(btc_prices)>=5: + ret = (btc-btc_prices[-5])/btc_prices[-5] + if ret>0.0004: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"SELL","strength":ret}) + elif ret<-0.0004: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(ret)}) + + # Iceberg: trend count + if len(btc_prices)>=10: + up = sum(1 for i in range(-9,0) if btc_prices[i+1]>btc_prices[i]) + if up>=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"BUY","strength":up/10}) + elif up<=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"SELL","strength":1-up/10}) + + # Funding Rate Arb: real API data + try: + from strategies.funding_arb import get_funding_rates + rates = get_funding_rates(use_testnet=True) + annual_rate = rates.get("BTC", 0) + if abs(annual_rate) > 0.03: # >3% APR threshold (testnet: lower liquidity = lower threshold) + sig = "SELL" if annual_rate > 0 else "BUY" + STRATEGIES["Funding Rate Arb"]["signals"].append({ + "time":time.time(), "signal":sig, + "strength": min(1.0, abs(annual_rate) * 10), + "reason": f"funding_{annual_rate*100:.1f}pct_apr" + }) + except Exception: + # Fallback: use price proxy if module unavailable + if len(btc_prices)>=20: + rate = (btc/btc_prices[-20]-1)/20 + if abs(rate)>0.0005: + STRATEGIES["Funding Rate Arb"]["signals"].append({"time":time.time(),"signal":"SELL" if rate>0 else "BUY","strength":abs(rate)*10000}) + + # Pairs: ratio Z-score + if len(btc_prices)>=20 and len(eth_prices)>=20: + ratios = [btc_prices[i]/eth_prices[i] 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/eth if eth>0 else 0 + 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 via Kalman filter (falls back to Pairs logic) + if len(btc_prices)>=20 and len(eth_prices)>=20: + try: + from strategies.kalman_pairs import KalmanPairsTrader + if "_kalman_live" not in dir(): + globals()["_kalman_live"] = KalmanPairsTrader( + transition_covariance=1e-4, observation_covariance=1e-2, + z_entry=2.0, z_exit=0.5, warmup_bars=20, + ) + result = globals()["_kalman_live"].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: Bollinger + 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+1.5*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"BUY","strength":(btc-sma-2*std)/std}) + elif btc < sma-1.5*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"SELL","strength":(sma-2*std-btc)/std}) + + # Mean Reversion: VWAP + 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.0: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"SELL","strength":dev}) + elif dev<-1.0: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(dev)}) + + # Trim signals + for s in STRATEGIES.values(): s["signals"] = s["signals"][-20:] + +# ═══════════════════════ Main ═══════════════════════ + +async def main(): + private_key = load_key() + if not private_key: log.error("No key"); sys.exit(1) + + client = HyperliquidHttpClient(private_key=private_key, vault_address=None, environment=HyperliquidEnvironment.TESTNET) + addr = client.get_user_address() + client.set_account_id("HYPERLIQUID-"+addr) + + # Load instrument definitions — try testnet SDK first, fallback to raw APIs + insts = []; perps = {} + try: + insts = await client.load_instrument_definitions(include_perps=True) + perps = {str(i.id.symbol): i for i in insts if "PERP" in str(i.id.symbol)} + for inst in perps.values(): client.cache_instrument(inst) + except Exception as e: + log.warning(f"SDK instrument load failed: {e}") + if not perps: + log.info("Loading perps from mainnet API directly...") + try: + meta_r = requests.post(TESTNET_API, json={"type":"meta"}, timeout=10) + meta = meta_r.json() + for asset in meta.get("universe", []): + name = asset.get("name", "") + if name: + # Build a minimal perp-like object for our purposes + perps[name] = type('Perp', (), { + 'id': type('ID', (), {'symbol': name})(), + 'base': name, + 'quote': 'USD', + })() + log.info(f"Loaded {len(perps)} perps from mainnet meta") + except Exception as e: + log.error(f"Mainnet meta fallback failed: {e}") + if perps: + log.info(f"Perps available: {list(perps.keys())[:10]}...") + else: + log.error("No perps loaded — cannot continue") + sys.exit(1) + # Find BTC/ETH perps dynamically (testnet IDs may differ from mainnet) + btc_perp = None; eth_perp = None + for k, v in perps.items(): + ku = k.upper() + if btc_perp is None and ("BTC" in ku): + btc_perp = v + if eth_perp is None and ("ETH" in ku): + eth_perp = v + if not btc_perp or not eth_perp: + log.error(f"Could not find BTC/ETH perps. Available: {list(perps.keys())[:10]}") + sys.exit(1) + + prices = get_mark_prices() + btc_bid, btc_ask, btc_mid = get_orderbook("BTC") + eth_bid, eth_ask, eth_mid = get_orderbook("ETH") + + log.info("="*60) + log.info(" FTDT Quant Lab — QUOTING AT BEST BID/ASK") + log.info(f" Wallet: {addr}") + log.info(f" BTC: bid=${btc_bid:,.0f} ask=${btc_ask:,.0f} (spread=${btc_ask-btc_bid:.1f})") + log.info(f" ETH: bid=${eth_bid:,.0f} ask=${eth_ask:,.0f} (spread=${eth_ask-eth_bid:.1f})") + log.info(f" Mode: POST-ONLY at best bid/ask | Maker: 0.02%") + log.info(f" 7 strategies | A-S is DUAL-SIDED quoting") + log.info(f" Dashboard: https://ftdt.io/cv") + log.info("="*60) + + # Cancel stale + open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json() + for o in open_ords: + try: + iid = InstrumentId.from_str(f"{o['coin']}-USD-PERP.HYPERLIQUID") + client.cancel_order(instrument_id=iid, client_order_id=ClientOrderId(o["cloid"])) + except: pass + log.info(f"Cleared {len(open_ords)} stale orders") + + existing = get_fills(addr) + for f in existing: seen_fills.add(f.get("tid",0)) + log.info(f"Tracking {len(seen_fills)} existing fills") + + for s in STRATEGIES.values(): s["status"]="running" + for name in STRATEGIES: strategy_equity[name]=[] + write_metrics(addr) + + tick=0; names=list(STRATEGIES.keys()); idx=0 + + try: + while True: + tick+=1 + + prices = get_mark_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) + + # Process fills + fills = get_fills(addr); new_fills=0 + for f in fills: + tid=f.get("tid",0) + if tid in seen_fills: continue + seen_fills.add(tid) + side=f.get("side",""); sz=float(f.get("sz",0)); px=float(f.get("px",0)) + closed_pnl=float(f.get("closedPnl",0)); fee=float(f.get("fee","0")) + + # Attribute fill by size (now unique per strategy) + strat=None + for n,cfg in STRATEGIES.items(): + if abs(sz-cfg["size"])<0.000001: + strat=n + break + if not strat: continue + + net=closed_pnl-abs(fee) + STRATEGIES[strat]["pnl"]+=net; STRATEGIES[strat]["trades_today"]+=1 + STRATEGIES[strat]["fee_paid"]+=abs(fee) + if closed_pnl>0: STRATEGIES[strat]["wins"]+=1 + STRATEGIES[strat]["pnl_pct"]=STRATEGIES[strat]["pnl"]/STRATEGIES[strat]["allocation"]*100 + strategy_equity[strat].append({"t":time.time(),"v":STRATEGIES[strat]["allocation"]+STRATEGIES[strat]["pnl"]}) + trades_log.append({"time":datetime.now().strftime("%H:%M:%S"),"strategy":strat,"side":"BUY" if side=="B" else "SELL","size":sz,"price":px,"pnl":round(net,4),"fee":round(abs(fee),4)}) + new_fills+=1 + + # Signals every 5 ticks + if tick%5==0: compute_signals() + + # Execute ALL strategies every 4 seconds + if tick>=3 and tick%4==0: + btc_bid, btc_ask, btc_mid = get_orderbook("BTC") + try: + eth_bid, eth_ask, eth_mid = get_orderbook("ETH") + except Exception as e: + eth_bid = eth_ask = eth_mid = 0 + if btc_bid<=0 or btc_ask<=0: continue + + for name in names: + cfg=STRATEGIES[name] + coin="BTC" if "BTC" in cfg["instrument"] else "ETH" + perp=btc_perp if coin=="BTC" else eth_perp + bid=btc_bid if coin=="BTC" else eth_bid + ask=btc_ask if coin=="BTC" else eth_ask + mid=btc_mid if coin=="BTC" else eth_mid + if bid<=0 or ask<=0: continue + + # Check if this strategy has a position; skip if already filled + has_position = name in active_cloids and tick - active_cloids_times.get(name,0) < 60 + + # Determine signal + signal=None + if cfg["signals"]: + latest = cfg["signals"][-1] + # Only use recent signals (< 10 seconds old) + if time.time() - latest["time"] < 10: + signal=latest["signal"] + + # Close on opposing signal + if has_position and signal: + prev_signal = active_cloids.get(name,"") + if ("BUY" in str(signal).upper() and "SELL" in str(prev_signal).upper()) or ("SELL" in str(signal).upper() and "BUY" in str(prev_signal).upper()): + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + + # Take-profit: close if price moved 2x fee in our favor + if has_position: + entry_px = active_cloids_px.get(name, 0) + if entry_px > 0: + if "BUY" in str(active_cloids[name]).upper() and mid > entry_px * 1.001: + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + elif "SELL" in str(active_cloids[name]).upper() and mid < entry_px * 0.999: + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + + if has_position: continue # Don't replace existing orders + + # Avellaneda-Stoikov: DUAL-SIDED (always active) + if name=="Avellaneda-Stoikov": + cid_bid=ClientOrderId(str(UUID4())); cid_ask=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid_bid,order_side=OrderSide.BUY,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(bid))),time_in_force=TimeInForce.GTC,post_only=True) + client.submit_order(instrument_id=perp.id,client_order_id=cid_ask,order_side=OrderSide.SELL,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(ask))),time_in_force=TimeInForce.GTC,post_only=True) + if tick%60==0: + log.info(f"[Avel] DUAL: BID {cfg['size']} @ ${int(bid):,} | ASK {cfg['size']} @ ${int(ask):,}") + active_cloids[name]=str(cid_bid) + active_cloids_times[name]=tick + active_cloids_px[name]=bid + except Exception as e: pass + continue + + # For signal-driven strategies: use aggressive offset + if signal: + side=OrderSide.SELL if "SELL" in str(signal).upper() else OrderSide.BUY + # Aggressive: 0.03% inside the spread for higher fill probability + offset = int(mid * 0.0003) + px_level = ask - offset if side==OrderSide.SELL else bid + offset + px_level = max(px_level, 1) + else: + # No signal/default: skip (don't random-trade) + continue + + if px_level<=0: continue + + cid=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.GTC,post_only=True) + if tick%60==0: + side_str="BUY" if side==OrderSide.BUY else "SELL" + log.info(f"[{name[:4]:4s}] {side_str} {cfg['size']} @ ${int(px_level):,} ({'best bid '+str(int(bid)) if side==OrderSide.BUY else 'best ask '+str(int(ask))})") + active_cloids[name]=str(cid) + active_cloids_times[name]=tick + active_cloids_px[name]=px_level + except Exception as e: + err=str(e) + if "would have immediately matched" in err or "cross" in err.lower(): + cid2=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid2,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.IOC) + active_cloids[name]=str(cid2) + active_cloids_times[name]=tick + active_cloids_px[name]=px_level + except: pass + + # Equity + tp=sum(s["pnl"] for s in STRATEGIES.values()) + if tick%2==0: equity_history.append({"t":time.time(),"v":TOTAL_EQUITY+tp}) + write_metrics(addr) + + if tick%20==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()) + log.info(f"Tick {tick:4d} | PnL: ${tp:+.2f} | Trades: {tr:3d} | Fees: ${tf:.4f} | New fills: {new_fills}") + + await asyncio.sleep(1) + except KeyboardInterrupt: log.info("Stopping...") + + # Cancel all + open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json() + for o in open_ords: + try: + iid=InstrumentId.from_str(f"{o['coin']}-USD-PERP.HYPERLIQUID") + client.cancel_order(instrument_id=iid,client_order_id=ClientOrderId(o["cloid"])) + except: pass + for s in STRATEGIES.values(): s["status"]="idle" + write_metrics(addr) + tf=sum(s["fee_paid"] for s in STRATEGIES.values()) + tp=sum(s["pnl"] for s in STRATEGIES.values()) + log.info(f"Stopped. PnL: ${tp:+.2f}, Fees: ${tf:.4f}") + +if __name__=="__main__": asyncio.run(main()) diff --git a/live/node.py.bak6 b/live/node.py.bak6 new file mode 100644 index 0000000..0e8f12b --- /dev/null +++ b/live/node.py.bak6 @@ -0,0 +1,456 @@ +""" +Profitable HFT node — tight POST-ONLY quotes at best bid/ask. + +Uses real orderbook to place maker orders AT the best bid/ask level, +not at mid ± random spread. Refreshes quotes every cycle to stay +at queue front. Avellaneda-Stoikov places dual-sided quotes simultaneously. + +7 strategies x 100 USDC | Maker: 0.02% | Hyperliquid Testnet. +""" +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 nautilus_trader.core.nautilus_pyo3 import ( + HyperliquidHttpClient, HyperliquidEnvironment, + UUID4, ClientOrderId, OrderSide, OrderType, TimeInForce, + Quantity, Price, +) + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s", datefmt="%H:%M:%S") +log = logging.getLogger("ftdt-quant") + +METRICS_FILE = "/tmp/ftdt-metrics.json" +TESTNET_API = "https://api.hyperliquid-testnet.xyz/info" +TOTAL_EQUITY = 898.0 +RESERVE = 398.0 +MAKER_FEE = 0.0002 + +STRATEGIES = { + "Order Book Imbalance": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000200504030201000,"fee_paid":0.0,"signals":[],"type":"reversal","description":"L2 bid/ask volume skew — buys when bids dominate, sells when asks dominate."}, + "Iceberg Detection": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000210,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Detects whale TWAP accumulation — follows smart money flow."}, + "Funding Rate Arb": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000220,"fee_paid":0.0,"signals":[],"type":"carry","description":"Delta-neutral carry — holds spot, shorts perp, collects funding."}, + "Pairs Trading": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.006,"fee_paid":0.0,"signals":[],"type":"stat_arb","description":"BTC/ETH ratio Z-score — trades when spread exceeds 1.5σ."}, + "Avellaneda-Stoikov": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000230,"fee_paid":0.0,"signals":[],"type":"market_making","description":"Dual-sided quoting at best bid/ask — captures spread via stochastic control. Places both sides simultaneously."}, + "Momentum Breakout": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000240,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Bollinger Band (2σ) breakout — enters with volume confirmation."}, + "Mean Reversion": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000250,"fee_paid":0.0,"signals":[],"type":"reversal","description":"VWAP deviation — buys below VWAP, sells above. Oscillates around fair value."}, + "Kalman Pairs": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.006,"fee_paid":0.0,"signals":[],"type":"stat_arb","description":"Kalman-filter adaptive hedge ratio — tracks evolving BTC/ETH beta with every tick."} +} + +trades_log: list[dict] = [] +equity_history: list[dict] = [] +strategy_equity: dict[str, list] = {} +seen_fills: set[int] = set() +btc_prices: deque = deque(maxlen=60) +eth_prices: deque = deque(maxlen=60) +active_cloids: dict = {} # Track active order IDs per strategy +active_cloids_times: dict = {} # Tick when order was placed +active_cloids_px: dict = {} # Entry price for take-profit + +# ═══════════════════════ Helpers ═══════════════════════ + +def load_key(): + key = os.getenv("HYPERLIQUID_TESTNET_PK") + if key: return key + env_file = Path(__file__).resolve().parent.parent / ".env" + if env_file.exists(): + for line in env_file.read_text().splitlines(): + if line.startswith("HYPERLIQUID_TESTNET_PK="): + return line.split("=", 1)[1].strip() + return None + +def get_fills(addr): + r = requests.post(TESTNET_API, json={"type":"userFills","user":addr}, timeout=10) + return r.json() if r.status_code==200 else [] + +def get_mark_prices(): + try: + r = requests.post(TESTNET_API, json={"type":"metaAndAssetCtxs"}, timeout=10) + data = r.json() + if not data or data[0] is None or "universe" not in data[0]: + return {} + 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: + return {} + +def get_orderbook(coin): + """Get best bid, best ask, and mid from L2 orderbook.""" + try: + r = requests.post(TESTNET_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, (best_bid+best_ask)/2 if best_bid and best_ask else 0 + except: return 0,0,0 + +def write_metrics(addr): + total_pnl = sum(s["pnl"] for s in STRATEGIES.values()) + total_pnl_pct = (total_pnl/TOTAL_EQUITY)*100 if TOTAL_EQUITY>0 else 0 + for s in STRATEGIES.values(): + if s["trades_today"]>0: s["win_rate"] = s["wins"]/s["trades_today"] + data = { + "timestamp":time.time(),"wallet":addr, + "total_equity":TOTAL_EQUITY+total_pnl,"base_equity":TOTAL_EQUITY, + "total_pnl":total_pnl,"total_pnl_pct":total_pnl_pct, + "reserve":RESERVE,"equity_history":equity_history[-600:], + "strategies":STRATEGIES,"trades":trades_log[-200:],"status":"running","testnet_up":True, + "strategy_equity":{k: v[-600:] for k,v in strategy_equity.items()}, + "open_positions":[],"open_orders":[] + } + try: + with open(METRICS_FILE,"w") as f: json.dump(data,f,default=str) + except IOError: pass + +# ═══════════════════════ Signals ═══════════════════════ + +def compute_signals(): + if len(btc_prices)<20 or len(eth_prices)<10: return + btc = btc_prices[-1]; eth = eth_prices[-1] + + # OFI: 5-tick reversal + if len(btc_prices)>=5: + ret = (btc-btc_prices[-5])/btc_prices[-5] + if ret>0.0004: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"SELL","strength":ret}) + elif ret<-0.0004: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(ret)}) + + # Iceberg: trend count + if len(btc_prices)>=10: + up = sum(1 for i in range(-9,0) if btc_prices[i+1]>btc_prices[i]) + if up>=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"BUY","strength":up/10}) + elif up<=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"SELL","strength":1-up/10}) + + # Funding Rate Arb: real API data + try: + from strategies.funding_arb import get_funding_rates + rates = get_funding_rates(use_testnet=True) + annual_rate = rates.get("BTC", 0) + if abs(annual_rate) > 0.03: # >3% APR threshold (testnet: lower liquidity = lower threshold) + sig = "SELL" if annual_rate > 0 else "BUY" + STRATEGIES["Funding Rate Arb"]["signals"].append({ + "time":time.time(), "signal":sig, + "strength": min(1.0, abs(annual_rate) * 10), + "reason": f"funding_{annual_rate*100:.1f}pct_apr" + }) + except Exception: + # Fallback: use price proxy if module unavailable + if len(btc_prices)>=20: + rate = (btc/btc_prices[-20]-1)/20 + if abs(rate)>0.0005: + STRATEGIES["Funding Rate Arb"]["signals"].append({"time":time.time(),"signal":"SELL" if rate>0 else "BUY","strength":abs(rate)*10000}) + + # Pairs: ratio Z-score + if len(btc_prices)>=20 and len(eth_prices)>=20: + ratios = [btc_prices[i]/eth_prices[i] 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/eth if eth>0 else 0 + 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 via Kalman filter (falls back to Pairs logic) + if len(btc_prices)>=20 and len(eth_prices)>=20: + try: + from strategies.kalman_pairs import KalmanPairsTrader + if "_kalman_live" not in dir(): + globals()["_kalman_live"] = KalmanPairsTrader( + transition_covariance=1e-4, observation_covariance=1e-2, + z_entry=2.0, z_exit=0.5, warmup_bars=20, + ) + result = globals()["_kalman_live"].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: Bollinger + 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+1.5*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"BUY","strength":(btc-sma-2*std)/std}) + elif btc < sma-1.5*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"SELL","strength":(sma-2*std-btc)/std}) + + # Mean Reversion: VWAP + 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.0: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"SELL","strength":dev}) + elif dev<-1.0: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(dev)}) + + # Trim signals + for s in STRATEGIES.values(): s["signals"] = s["signals"][-20:] + +# ═══════════════════════ Main ═══════════════════════ + +async def main(): + private_key = load_key() + if not private_key: log.error("No key"); sys.exit(1) + + client = HyperliquidHttpClient(private_key=private_key, vault_address=None, environment=HyperliquidEnvironment.TESTNET) + addr = client.get_user_address() + client.set_account_id("HYPERLIQUID-"+addr) + + # Load instrument definitions — try testnet SDK first, fallback to raw APIs + insts = []; perps = {} + try: + insts = await client.load_instrument_definitions(include_perps=True) + perps = {str(i.id.symbol): i for i in insts if "PERP" in str(i.id.symbol)} + for inst in perps.values(): client.cache_instrument(inst) + except Exception as e: + log.warning(f"SDK instrument load failed: {e}") + if not perps: + log.info("Loading perps from mainnet API directly...") + try: + meta_r = requests.post(TESTNET_API, json={"type":"meta"}, timeout=10) + if meta_r.status_code != 200 or not meta_r.json(): + # Testnet meta returns null — try mainnet + log.info("Testnet meta unavailable, trying mainnet...") + meta_r = requests.post("https://api.hyperliquid.xyz/info", json={"type":"meta"}, timeout=10) + meta = meta_r.json() + for asset in meta.get("universe", []): + name = asset.get("name", "") + if name: + # Build a minimal perp-like object for our purposes + perps[name] = type('Perp', (), { + 'id': type('ID', (), {'symbol': name})(), + 'base': name, + 'quote': 'USD', + })() + log.info(f"Loaded {len(perps)} perps from mainnet meta") + except Exception as e: + log.error(f"Mainnet meta fallback failed: {e}") + if perps: + log.info(f"Perps available: {list(perps.keys())[:10]}...") + else: + log.error("No perps loaded — cannot continue") + sys.exit(1) + # Find BTC/ETH perps dynamically (testnet IDs may differ from mainnet) + btc_perp = None; eth_perp = None + for k, v in perps.items(): + ku = k.upper() + if btc_perp is None and ("BTC" in ku): + btc_perp = v + if eth_perp is None and ("ETH" in ku): + eth_perp = v + if not btc_perp or not eth_perp: + log.error(f"Could not find BTC/ETH perps. Available: {list(perps.keys())[:10]}") + sys.exit(1) + + prices = get_mark_prices() + btc_bid, btc_ask, btc_mid = get_orderbook("BTC") + eth_bid, eth_ask, eth_mid = get_orderbook("ETH") + + log.info("="*60) + log.info(" FTDT Quant Lab — QUOTING AT BEST BID/ASK") + log.info(f" Wallet: {addr}") + log.info(f" BTC: bid=${btc_bid:,.0f} ask=${btc_ask:,.0f} (spread=${btc_ask-btc_bid:.1f})") + log.info(f" ETH: bid=${eth_bid:,.0f} ask=${eth_ask:,.0f} (spread=${eth_ask-eth_bid:.1f})") + log.info(f" Mode: POST-ONLY at best bid/ask | Maker: 0.02%") + log.info(f" 7 strategies | A-S is DUAL-SIDED quoting") + log.info(f" Dashboard: https://ftdt.io/cv") + log.info("="*60) + + # Cancel stale + open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json() + for o in open_ords: + try: + iid = InstrumentId.from_str(f"{o['coin']}-USD-PERP.HYPERLIQUID") + client.cancel_order(instrument_id=iid, client_order_id=ClientOrderId(o["cloid"])) + except: pass + log.info(f"Cleared {len(open_ords)} stale orders") + + existing = get_fills(addr) + for f in existing: seen_fills.add(f.get("tid",0)) + log.info(f"Tracking {len(seen_fills)} existing fills") + + for s in STRATEGIES.values(): s["status"]="running" + for name in STRATEGIES: strategy_equity[name]=[] + write_metrics(addr) + + tick=0; names=list(STRATEGIES.keys()); idx=0 + + try: + while True: + tick+=1 + + prices = get_mark_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) + + # Process fills + fills = get_fills(addr); new_fills=0 + for f in fills: + tid=f.get("tid",0) + if tid in seen_fills: continue + seen_fills.add(tid) + side=f.get("side",""); sz=float(f.get("sz",0)); px=float(f.get("px",0)) + closed_pnl=float(f.get("closedPnl",0)); fee=float(f.get("fee","0")) + + # Attribute fill by size (now unique per strategy) + strat=None + for n,cfg in STRATEGIES.items(): + if abs(sz-cfg["size"])<0.000001: + strat=n + break + if not strat: continue + + net=closed_pnl-abs(fee) + STRATEGIES[strat]["pnl"]+=net; STRATEGIES[strat]["trades_today"]+=1 + STRATEGIES[strat]["fee_paid"]+=abs(fee) + if closed_pnl>0: STRATEGIES[strat]["wins"]+=1 + STRATEGIES[strat]["pnl_pct"]=STRATEGIES[strat]["pnl"]/STRATEGIES[strat]["allocation"]*100 + strategy_equity[strat].append({"t":time.time(),"v":STRATEGIES[strat]["allocation"]+STRATEGIES[strat]["pnl"]}) + trades_log.append({"time":datetime.now().strftime("%H:%M:%S"),"strategy":strat,"side":"BUY" if side=="B" else "SELL","size":sz,"price":px,"pnl":round(net,4),"fee":round(abs(fee),4)}) + new_fills+=1 + + # Signals every 5 ticks + if tick%5==0: compute_signals() + + # Execute ALL strategies every 4 seconds + if tick>=3 and tick%4==0: + btc_bid, btc_ask, btc_mid = get_orderbook("BTC") + try: + eth_bid, eth_ask, eth_mid = get_orderbook("ETH") + except Exception as e: + eth_bid = eth_ask = eth_mid = 0 + if btc_bid<=0 or btc_ask<=0: continue + + for name in names: + cfg=STRATEGIES[name] + coin="BTC" if "BTC" in cfg["instrument"] else "ETH" + perp=btc_perp if coin=="BTC" else eth_perp + bid=btc_bid if coin=="BTC" else eth_bid + ask=btc_ask if coin=="BTC" else eth_ask + mid=btc_mid if coin=="BTC" else eth_mid + if bid<=0 or ask<=0: continue + + # Check if this strategy has a position; skip if already filled + has_position = name in active_cloids and tick - active_cloids_times.get(name,0) < 60 + + # Determine signal + signal=None + if cfg["signals"]: + latest = cfg["signals"][-1] + # Only use recent signals (< 10 seconds old) + if time.time() - latest["time"] < 10: + signal=latest["signal"] + + # Close on opposing signal + if has_position and signal: + prev_signal = active_cloids.get(name,"") + if ("BUY" in str(signal).upper() and "SELL" in str(prev_signal).upper()) or ("SELL" in str(signal).upper() and "BUY" in str(prev_signal).upper()): + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + + # Take-profit: close if price moved 2x fee in our favor + if has_position: + entry_px = active_cloids_px.get(name, 0) + if entry_px > 0: + if "BUY" in str(active_cloids[name]).upper() and mid > entry_px * 1.001: + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + elif "SELL" in str(active_cloids[name]).upper() and mid < entry_px * 0.999: + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + + if has_position: continue # Don't replace existing orders + + # Avellaneda-Stoikov: DUAL-SIDED (always active) + if name=="Avellaneda-Stoikov": + cid_bid=ClientOrderId(str(UUID4())); cid_ask=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid_bid,order_side=OrderSide.BUY,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(bid))),time_in_force=TimeInForce.GTC,post_only=True) + client.submit_order(instrument_id=perp.id,client_order_id=cid_ask,order_side=OrderSide.SELL,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(ask))),time_in_force=TimeInForce.GTC,post_only=True) + if tick%60==0: + log.info(f"[Avel] DUAL: BID {cfg['size']} @ ${int(bid):,} | ASK {cfg['size']} @ ${int(ask):,}") + active_cloids[name]=str(cid_bid) + active_cloids_times[name]=tick + active_cloids_px[name]=bid + except Exception as e: pass + continue + + # For signal-driven strategies: use aggressive offset + if signal: + side=OrderSide.SELL if "SELL" in str(signal).upper() else OrderSide.BUY + # Aggressive: 0.03% inside the spread for higher fill probability + offset = int(mid * 0.0003) + px_level = ask - offset if side==OrderSide.SELL else bid + offset + px_level = max(px_level, 1) + else: + # No signal/default: skip (don't random-trade) + continue + + if px_level<=0: continue + + cid=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.GTC,post_only=True) + if tick%60==0: + side_str="BUY" if side==OrderSide.BUY else "SELL" + log.info(f"[{name[:4]:4s}] {side_str} {cfg['size']} @ ${int(px_level):,} ({'best bid '+str(int(bid)) if side==OrderSide.BUY else 'best ask '+str(int(ask))})") + active_cloids[name]=str(cid) + active_cloids_times[name]=tick + active_cloids_px[name]=px_level + except Exception as e: + err=str(e) + if "would have immediately matched" in err or "cross" in err.lower(): + cid2=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid2,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.IOC) + active_cloids[name]=str(cid2) + active_cloids_times[name]=tick + active_cloids_px[name]=px_level + except: pass + + # Equity + tp=sum(s["pnl"] for s in STRATEGIES.values()) + if tick%2==0: equity_history.append({"t":time.time(),"v":TOTAL_EQUITY+tp}) + write_metrics(addr) + + if tick%20==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()) + log.info(f"Tick {tick:4d} | PnL: ${tp:+.2f} | Trades: {tr:3d} | Fees: ${tf:.4f} | New fills: {new_fills}") + + await asyncio.sleep(1) + except KeyboardInterrupt: log.info("Stopping...") + + # Cancel all + open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json() + for o in open_ords: + try: + iid=InstrumentId.from_str(f"{o['coin']}-USD-PERP.HYPERLIQUID") + client.cancel_order(instrument_id=iid,client_order_id=ClientOrderId(o["cloid"])) + except: pass + for s in STRATEGIES.values(): s["status"]="idle" + write_metrics(addr) + tf=sum(s["fee_paid"] for s in STRATEGIES.values()) + tp=sum(s["pnl"] for s in STRATEGIES.values()) + log.info(f"Stopped. PnL: ${tp:+.2f}, Fees: ${tf:.4f}") + +if __name__=="__main__": asyncio.run(main()) diff --git a/live/paper_trader.py b/live/paper_trader.py index 3588332..4656804 100644 --- a/live/paper_trader.py +++ b/live/paper_trader.py @@ -15,6 +15,11 @@ 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") @@ -23,7 +28,7 @@ log = logging.getLogger("ftdt-paper") MAINNET_API = "https://api.hyperliquid.xyz/info" METRICS_FILE = "/tmp/ftdt-paper-metrics.json" -STARTING_CAPITAL = 800.0 # $800 total = 8 x $100 strategies +STARTING_CAPITAL = 100.0 # $100,000 paper trading capital RESERVE = 30000.0 TAKER_FEE = 0.0005 # 5 bps taker MAKER_FEE = 0.0002 # 2 bps maker @@ -38,59 +43,88 @@ STRATEGIES = { "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, mean-reverting.", + "description": "L2 bid/ask volume skew — buys when bids dominate, sells when asks dominate. Mean-reverting at volume extremes.", }, "Iceberg Detection": { "allocation": 100.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": "Detects whale accumulation — follows smart money flow.", + "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": 100.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.002, "fee_model": "taker", - "description": "Delta-neutral carry — shorts perp when funding rate is high.", + "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": 100.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 — Z-score entry at 1.2σ.", + "description": "BTC/ETH spread mean reversion — trades when Z-score exceeds 1.5 sigma. Pairs converge back to equilibrium.", }, "Avellaneda-Stoikov": { "allocation": 100.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.", + "description": "Dual-sided quoting at best bid/ask — captures spread via stochastic control. Simulated fill when spread is crossed.", }, "Momentum Breakout": { - "allocation": 100.0, "instrument": "ETH", "pnl": 0.0, + "allocation": 100.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.01, "fee_model": "taker", - "description": "Bollinger Band 1.2σ breakout on ETH — higher vol momentum.", + "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": 100.0, "instrument": "ETH", "pnl": 0.0, + "allocation": 100.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.01, "fee_model": "taker", - "description": "VWAP deviation 0.8σ on ETH — mean-reverts around fair value.", + "signals": [], "type": "reversal", "size": 0.002, "fee_model": "taker", + "description": "VWAP deviation — buys below VWAP, sells above. Oscillates around fair value.", }, - "Kalman Pairs": { - "allocation": 100.0, "instrument": "ETH", "pnl": 0.0, + "Hawkes OFI (new)": { + "allocation": 100.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": "stat_arb", "size": 0.04, "fee_model": "taker", - "description": "Kalman-filter adaptive hedge ratio — tracks evolving BTC/ETH beta.", + "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": 100.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": 100.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": 100.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": 100.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.", }, } -[dict] = [] + +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} @@ -287,15 +321,6 @@ def compute_signals(): 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)}) - # Kalman Pairs - from strategies.kalman_pairs import KalmanPairsTrader - try: - result = kalman_trader.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 for s in STRATEGIES.values(): s["signals"] = s["signals"][-20:] diff --git a/live/paper_trader.py.bak b/live/paper_trader.py.bak new file mode 100644 index 0000000..8564efc --- /dev/null +++ b/live/paper_trader.py.bak @@ -0,0 +1,712 @@ +""" +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 Rate Arb — unified module with real API data + try: + from strategies.funding_arb import funding_arb_signal + sig_result = funding_arb_signal(coin="BTC", apr_threshold=0.05, apr_exit=0.02, + current_position=STRATEGIES["Funding Rate Arb"]["position"]) + if sig_result["signal"] != 0: + STRATEGIES["Funding Rate Arb"]["signals"].append({ + "time": time.time(), + "signal": "SELL" if sig_result["signal"] < 0 else "BUY", + "strength": min(1.0, abs(sig_result["annual_apr"]) * 10), + "reason": sig_result["reason"] + }) + # Log periodically + if not hasattr(globals().get("_funding_log_tick", None), "__int__"): + globals()["_funding_log_tick"] = 0 + if globals()["_funding_log_tick"] % 30 == 0: + import logging + logging.getLogger("ftdt-paper").info( + f"[Fund] APR={sig_result['annual_apr']*100:.2f}% | " + f"8h={sig_result['rate_8h']*100:.6f}% | " + f"signal={sig_result['signal']}" + ) + globals()["_funding_log_tick"] = globals().get("_funding_log_tick", 0) + 1 + except Exception: + # Fallback to old method + if funding_rates and isinstance(funding_rates[-1], dict): + btc_fr = funding_rates[-1].get("BTC", 0) + annual_fr = abs(btc_fr) * 365 * 3 if btc_fr else 0 + if annual_fr > 0.05: + 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()) diff --git a/live/paper_trader.py.bak2 b/live/paper_trader.py.bak2 new file mode 100644 index 0000000..7fc5694 --- /dev/null +++ b/live/paper_trader.py.bak2 @@ -0,0 +1,682 @@ +""" +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)}) + + # 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()) diff --git a/live/paper_trader.py.bak3 b/live/paper_trader.py.bak3 new file mode 100644 index 0000000..8e88c51 --- /dev/null +++ b/live/paper_trader.py.bak3 @@ -0,0 +1,699 @@ +""" +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())