""" 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())