Files
ramseshk 9be02b47f9 Clean architecture: Paper=Mainnet, Live=Testnet, Historical=Mainnet
- Live node: testnet-only API for prices/orderbook/instruments
  (removed mainnet fallback, added resilience wrappers)
- Paper trader: mainnet-only API — simulates with real Hyperliquid data
  $120K paper capital, 12 strategies, mainnet mark prices
- Historical backtests: mainnet candle API (unchanged, already correct)
- All three tiers: strategy_equity tracking, dynamic perp lookup,
  win_rate fix (pnl_net/pnl_gross), CSS contrast improvement
2026-08-05 03:02:50 +00:00

385 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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())