Repo cleanup: README with full stack summary + .gitignore + remove stale backups
This commit is contained in:
+23
-20
@@ -1,28 +1,31 @@
|
|||||||
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
dist/
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
|
||||||
|
# Next.js / Dashboard
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Environment
|
||||||
.env
|
.env
|
||||||
*.pem
|
*.env.local
|
||||||
*_pk
|
|
||||||
data/
|
# IDE
|
||||||
*.parquet
|
|
||||||
.ipynb_checkpoints/
|
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
.DS_Store
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
# Next.js build output (deployed to static dir at runtime, not tracked)
|
# Runtime artifacts
|
||||||
dashboard/static/_next/
|
/tmp/
|
||||||
dashboard/static/404.html
|
*.log
|
||||||
dashboard/static/404/
|
metrics.json
|
||||||
dashboard/static/__next.*
|
paper_metrics.json
|
||||||
dashboard/static/favicon.ico
|
|
||||||
dashboard/static/file.svg
|
# OS
|
||||||
dashboard/static/globe.svg
|
.DS_Store
|
||||||
dashboard/static/index.txt
|
Thumbs.db
|
||||||
dashboard/static/next.svg
|
|
||||||
dashboard/static/vercel.svg
|
|
||||||
dashboard/static/window.svg
|
|
||||||
dashboard/static/_not-found/
|
|
||||||
|
|||||||
@@ -1,59 +1,168 @@
|
|||||||
# FTDT Quant Lab — Quantitative Trading Strategies
|
# FTDT Quant Lab
|
||||||
|
|
||||||
A collection of quantitative trading strategies running on
|
Production multi-strategy quant trading system running on Hyperliquid.
|
||||||
**Hyperliquid Testnet** via **Nautilus Trader**. Built as part of
|
Live testnet node, paper trading simulator, historical backtesting, and real-time dashboard.
|
||||||
my professional portfolio to demonstrate algorithmic trading,
|
|
||||||
market microstructure, and risk management skills.
|
|
||||||
|
|
||||||
## What's inside
|
**Live:** https://ftdt.io/cv
|
||||||
|
|
||||||
Five strategies, from simple to advanced:
|
---
|
||||||
|
|
||||||
| # | Strategy | Concept |
|
## Stack
|
||||||
|---|----------|---------|
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
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
|
## Repository Structure
|
||||||
export HYPERLIQUID_TESTNET_PK=0x...
|
|
||||||
|
|
||||||
# Run live (testnet only)
|
|
||||||
python live/node.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project layout
|
|
||||||
|
|
||||||
```
|
```
|
||||||
ftdt-quant-lab/
|
ftdt-quant-lab/
|
||||||
├── config/ # Per-strategy YAML configuration
|
├── live/
|
||||||
├── strategies/ # Strategy implementations
|
│ ├── node.py # Live trading node — testnet, 9 strategies
|
||||||
├── common/ # Risk manager, portfolio tracker, metrics
|
│ └── paper_trader.py # Paper trading — mainnet data, 10 strategies
|
||||||
├── backtests/ # Historical backtest runners
|
├── strategies/
|
||||||
├── live/ # Live trading node (Hyperliquid Testnet)
|
│ ├── orderbook_imbalance.py # L2 bid/ask volume skew (OBI)
|
||||||
├── docs/ # Documentation and strategy writeups
|
│ ├── iceberg_detection.py # Whale TWAP accumulation detection
|
||||||
└── notebooks/ # Analysis notebooks
|
│ ├── 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
|
| # | Strategy | Type | Asset | Size | PnL | Trades | Win |
|
||||||
are not financial advice and have no alpha guarantee. Never run
|
|---|----------|------|-------|------|-----|--------|-----|
|
||||||
them on mainnet without thorough backtesting and your own due diligence.
|
| 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*
|
||||||
|
|||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
Reference in New Issue
Block a user