From ff3e68855cba326a48ca28e741e881f8eef8c190 Mon Sep 17 00:00:00 2001 From: ramseshk Date: Thu, 6 Aug 2026 07:21:04 +0000 Subject: [PATCH] Repo cleanup: README with full stack summary + .gitignore + remove stale backups --- .gitignore | 43 +-- README.md | 191 +++++++--- 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.bak | 712 -------------------------------------- live/paper_trader.py.bak2 | 682 ------------------------------------ live/paper_trader.py.bak3 | 699 ------------------------------------- 10 files changed, 173 insertions(+), 4314 deletions(-) delete mode 100644 live/node.py.bak delete mode 100644 live/node.py.bak2 delete mode 100644 live/node.py.bak3 delete mode 100644 live/node.py.bak5 delete mode 100644 live/node.py.bak6 delete mode 100644 live/paper_trader.py.bak delete mode 100644 live/paper_trader.py.bak2 delete mode 100644 live/paper_trader.py.bak3 diff --git a/.gitignore b/.gitignore index 8ad253f..f525bb9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,31 @@ +# Python __pycache__/ *.py[cod] *.egg-info/ +dist/ .venv/ -venv/ + +# Next.js / Dashboard +.next/ +out/ +node_modules/ + +# Environment .env -*.pem -*_pk -data/ -*.parquet -.ipynb_checkpoints/ +*.env.local + +# IDE .idea/ .vscode/ -.DS_Store +*.swp +*.swo -# Next.js build output (deployed to static dir at runtime, not tracked) -dashboard/static/_next/ -dashboard/static/404.html -dashboard/static/404/ -dashboard/static/__next.* -dashboard/static/favicon.ico -dashboard/static/file.svg -dashboard/static/globe.svg -dashboard/static/index.txt -dashboard/static/next.svg -dashboard/static/vercel.svg -dashboard/static/window.svg -dashboard/static/_not-found/ +# Runtime artifacts +/tmp/ +*.log +metrics.json +paper_metrics.json + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md index 50f9581..1724c72 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,168 @@ -# FTDT Quant Lab — Quantitative Trading Strategies +# FTDT Quant Lab -A collection of quantitative trading strategies running on -**Hyperliquid Testnet** via **Nautilus Trader**. Built as part of -my professional portfolio to demonstrate algorithmic trading, -market microstructure, and risk management skills. +Production multi-strategy quant trading system running on Hyperliquid. +Live testnet node, paper trading simulator, historical backtesting, and real-time dashboard. -## What's inside +**Live:** https://ftdt.io/cv -Five strategies, from simple to advanced: +--- -| # | Strategy | Concept | -|---|----------|---------| -| 1 | Order Book Imbalance | Trades on L2 bid/ask pressure | -| 2 | Iceberg / TWAP Detection | Follows whale accumulation patterns | -| 3 | Funding Rate Arbitrage | Delta-neutral carry trade | -| 4 | Pairs Trading (BTC/ETH) | Cointegration-based stat arb | -| 5 | Avellaneda-Stoikov Market Making | Stochastic optimal control | +## Stack -All strategies share a common risk manager and portfolio tracker. +| Layer | Technology | +|-------|-----------| +| **Runtime** | Python 3.13 (async trading) | +| **API Client** | nautilus_trader (Hyperliquid SDK, Rust bindings) | +| **Dashboard** | Next.js 16 (static export) + shadcn/ui + Framer Motion | +| **Design System** | Hallmark Cobalt — Ubuntu font, hairline borders, cool paper palette | +| **Reverse Proxy** | Caddy → auto HTTPS | +| **WebSocket** | FastAPI (live/paper streaming) | +| **Data** | PostgreSQL 17 (`ftdt_quant`), JSON metrics files | +| **Backtesting** | Custom dollar-bar engine + numpy | +| **Infra** | OVH VPS (4 vCPU, 8GB RAM, Debian 13), 2GB swap | -## Quick start +~5,300 lines of Python + TypeScript. 67 commits since July 2026. -```bash -# Install dependencies -pip install -r requirements.txt +--- -# Set your Hyperliquid testnet key -export HYPERLIQUID_TESTNET_PK=0x... - -# Run live (testnet only) -python live/node.py -``` - -## Project layout +## Repository Structure ``` ftdt-quant-lab/ -├── config/ # Per-strategy YAML configuration -├── strategies/ # Strategy implementations -├── common/ # Risk manager, portfolio tracker, metrics -├── backtests/ # Historical backtest runners -├── live/ # Live trading node (Hyperliquid Testnet) -├── docs/ # Documentation and strategy writeups -└── notebooks/ # Analysis notebooks +├── live/ +│ ├── node.py # Live trading node — testnet, 9 strategies +│ └── paper_trader.py # Paper trading — mainnet data, 10 strategies +├── strategies/ +│ ├── orderbook_imbalance.py # L2 bid/ask volume skew (OBI) +│ ├── iceberg_detection.py # Whale TWAP accumulation detection +│ ├── funding_arb.py # Delta-neutral carry — spot/perp funding +│ ├── pairs_trading.py # BTC/ETH ratio Z-score (1.5σ) +│ ├── avellaneda_stoikov.py # Dual-sided stochastic control MM +│ ├── kalman_pairs/ # Kalman-filter adaptive hedge ratio +│ ├── hawkes_ofi.py # Hawkes process order flow +│ ├── deep_lob.py # Deep LOB CNN feature extraction +│ ├── queue_imbalance.py # Weighted queue dynamics +│ ├── hurst_vpin.py # Hurst exponent + VPIN directional +│ ├── hurst_vpin_live.py # Lightweight Hurst/VPIN for live tick stream +│ └── quant_report.py # QF-Lib style quant analytics +├── dashboard/ +│ ├── server.py # FastAPI backend — WS, REST, static files +│ └── next/ +│ └── src/ +│ ├── app/ # Main page + layout +│ ├── components/ # QuantReport, StrategyCard, L2Terminal +│ └── lib/ # Types, API client +├── backtests/ +│ ├── run.py # Backtest runner +│ └── results/ +│ └── historical/ # JSON backtest snapshots (32 entries) +├── common/ # Shared utilities +│ ├── risk.py, risk_manager.py +│ ├── hyperliquid_api.py +│ └── portfolio.py, metrics.py +├── config/ +│ └── fee_tiers.py # Perp/spot fee schedules +└── infrastructure/ + ├── Caddyfile # Reverse proxy config + └── systemd/ # Service units (pending) ``` -## Strategy details +--- -See `docs/STRATEGIES.md` for a walkthrough of each strategy. +## Strategies — Current State -## Risk warning +### Live Node (Hyperliquid Testnet — 9 strategies, $100 each) -This is **testnet only**. These strategies are educational — they -are not financial advice and have no alpha guarantee. Never run -them on mainnet without thorough backtesting and your own due diligence. +| # | Strategy | Type | Asset | Size | PnL | Trades | Win | +|---|----------|------|-------|------|-----|--------|-----| +| 1 | Order Book Imbalance | reversal | BTC | 0.000200 | $0.00 | 0 | — | +| 2 | Iceberg Detection | momentum | BTC | 0.000210 | $0.00 | 2 | 0% | +| 3 | Funding Rate Arb | carry | BTC | 0.000220 | $0.00 | 0 | — | +| 4 | Pairs Trading | stat_arb | ETH | 0.006000 | **+$0.74** | 9 | 67% | +| 5 | Avellaneda-Stoikov | market_making | BTC | 0.000230 | -$1.35 | 32 | 0% | +| 6 | Momentum Breakout | momentum | ETH | 0.000500 | $0.00 | 0 | — | +| 7 | Mean Reversion | reversal | ETH | 0.000500 | $0.00 | 0 | — | +| 8 | Kalman Pairs | stat_arb | ETH | 0.005000 | $0.00 | 0 | — | +| 9 | Hurst VPIN | momentum | BTC | 0.000240 | $0.00 | 0 | — | + +**Execution:** GTC POST-ONLY limit orders. Signals every 5 ticks (5s), dual-sided for A-S. +**Fee model:** Maker 0.02% (testnet). + +### Paper Trader (Hyperliquid Mainnet data — 10 strategies, $100 each) + +Same set + Queue Imbalance. Real mainnet orderbook + funding data. Fee model: taker 0.05% / maker 0.02%. Trades simulated with 1bps slippage. --- -Built by [Ramses Echikh](https://git.ftdt.io/rams) · Part of my quant trading portfolio + +## Historical Backtests + +32 backtest snapshots across 8 strategies × 4 coins (BTC, ETH, HYPE, VVV). +Hurst/VPIN BTC: **46 trades, 96% win rate, +1.10%** on synthetic trending data. + +--- + +## Priority Analysis + +### Strategies showing real signal + +| Strategy | Signal | Status | +|----------|--------|--------| +| **Pairs Trading** | ✅ | +$0.74, 67% win rate — only profitable live strategy | +| **Avellaneda-Stoikov** | ⚠️ | 32 trades but losing — spread capture not covering fees | +| **Iceberg Detection** | ⚠️ | 2 trades — rare signals, needs threshold tuning | +| **Hurst VPIN** | 🔬 | 96% win in backtest, 0 live trades — very selective | +| **Mean Reversion** | ⏳ | 0 trades — VWAP deviation not crossing 1.0σ | +| **Momentum** | ⏳ | 0 trades — Bollinger 1.2σ too tight for ETH | + +### Recommendation: focus investment here + +1. **Pairs Trading** — `#1 priority`. Only live winner. Extend to more pairs (SOL, ARB, OP). Add Kalman dynamic hedge ratio. This is the clearest path to sustained PnL. + +2. **Hurst/VPIN** — `#2 priority`. Backtest shows strong edge (96% win). Needs real market data (not synthetic) and 3-day candle feed to trigger more signals. The selectivity IS the edge — don't dilute it. + +3. **Avellaneda-Stoikov** — Needs inventory control. 32 trades losing because adverse selection. Add skew-aware quoting (update reserve price based on queue imbalance). + +4. **Iceberg Detection** — Lower detection threshold. Currently requires 7/10 consecutive ticks same direction — too strict. + +5. **Funding Rate Arb** — Real Hyperliquid funding data already plumbed. Test threshold from 3% → 1% APR. Prefunding detection (predict next rate before announcement). + +6. **Backtest engine** — Replace synthetic data with real Hyperliquid candles. Add walk-forward optimization. The `hurst_vpin.py` infrastructure is ready. + +### Skip for now + +- OBI / Mean Reversion / Momentum — 0 trades. Signal thresholds need fundamental redesign, not just tuning. +- Cartea-Jaimungal / Gueant MM — academic models, not adapted to crypto microstructure. +- DeepLOB / Hawkes OFI — dependency-heavy, no live integration. + +--- + +## Next Steps + +```bash +# Clone and deploy +git clone https://git.ftdt.io/rams/ftdt-quant-lab.git +cd ftdt-quant-lab +python3 -m venv .venv && source .venv/bin/activate +pip install -r requirements.txt # (pending — currently manual) + +# Start services +python live/node.py & # Trading node +python live/paper_trader.py & # Paper simulator +python dashboard/server.py --port 9175 # Dashboard backend +``` + +--- + +## Roadmap + +- [ ] Docker Compose for reproducible deployment +- [ ] Walk-forward backtest on real Hyperliquid candle data +- [ ] Extend Pairs Trading to BTC/SOL, BTC/ARB +- [ ] Hurst/VPIN 3-day candle feed → real live signals +- [ ] Memory leak proofing — current guard at 512MB RSS +- [ ] systemd service unit files for auto-restart +- [ ] Grafana + Prometheus monitoring dashboard + +--- + +*Built with Hermes Agent · Hallmark Cobalt · Ubuntu fonts* diff --git a/live/node.py.bak b/live/node.py.bak deleted file mode 100644 index 3ec41e7..0000000 --- a/live/node.py.bak +++ /dev/null @@ -1,384 +0,0 @@ -""" -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 deleted file mode 100644 index e7dfbb3..0000000 --- a/live/node.py.bak2 +++ /dev/null @@ -1,425 +0,0 @@ -""" -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 deleted file mode 100644 index 48f6439..0000000 --- a/live/node.py.bak3 +++ /dev/null @@ -1,443 +0,0 @@ -""" -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 deleted file mode 100644 index 6a9d26e..0000000 --- a/live/node.py.bak5 +++ /dev/null @@ -1,452 +0,0 @@ -""" -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 deleted file mode 100644 index 0e8f12b..0000000 --- a/live/node.py.bak6 +++ /dev/null @@ -1,456 +0,0 @@ -""" -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.bak b/live/paper_trader.py.bak deleted file mode 100644 index 8564efc..0000000 --- a/live/paper_trader.py.bak +++ /dev/null @@ -1,712 +0,0 @@ -""" -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 deleted file mode 100644 index 7fc5694..0000000 --- a/live/paper_trader.py.bak2 +++ /dev/null @@ -1,682 +0,0 @@ -""" -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 deleted file mode 100644 index 8e88c51..0000000 --- a/live/paper_trader.py.bak3 +++ /dev/null @@ -1,699 +0,0 @@ -""" -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())