09cb0d42b5
- live/paper_trader.py: replaced random 5% fill probability in simulate_avellaneda() with QueueAwareFillModel — fills only when aggressor volume exceeds depth ahead, regime-adaptive quote placement (tight in LOW_VOL, wide in HIGH_VOL). Integrated WQI Predictor and Funding Rate Arb as new strategies with signal generation. Dashboard metrics now include WQI summaries, funding arb status, and fill model throughput stats (fill rate, fills vs skips). 14 strategies total. - scripts/kill_switch.py: emergency kill switch — flattens all positions, cancels all open orders, verifies account is flat. Supports --dry-run, --mainnet, retry logic, L1 action signing. Reads private key from HL_PRIVATE_KEY env or ~/.hl/key. - infrastructure/systemd/: three service unit files for production deployment: ftdt-collector (data collection), ftdt-paper (trading node v2), ftdt-dashboard (FastAPI backend). Includes memory/cpu limits, auto-restart, log rotation. 321 tests passing.
812 lines
37 KiB
Python
812 lines
37 KiB
Python
"""
|
||
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
|
||
from strategies.wqi_predictor import WQIPredictor
|
||
from strategies.funding_arb_strategy import FundingArb
|
||
from sim.fills import QueueAwareFillModel
|
||
|
||
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.000800, "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.000850, "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.000900, "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.027500, "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.000950, "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.020000, "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.022500, "fee_model": "taker",
|
||
"description": "VWAP deviation — buys below VWAP, sells above. Oscillates around fair value.",
|
||
},
|
||
"Hurst VPIN": {
|
||
"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": "Hurst exponent regime filter + VPIN informed flow. Enters when both align trending + high flow imbalance.",
|
||
},
|
||
"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.",
|
||
},
|
||
"WQI Predictor": {
|
||
"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": "wqi", "size": 0.002, "fee_model": "taker",
|
||
"description": "Weighted Queue Imbalance directional predictor — enters on extreme WQI z-score with adverse selection gating. Exits on timeout, reversal, or stop-loss.",
|
||
},
|
||
"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": "funding_arb", "size": 0.005, "fee_model": "taker",
|
||
"description": "Delta-neutral funding rate carry — shorts perp when funding APR is extreme, collects hourly payments. Exits when rate fades, flips, or max hold reached.",
|
||
},
|
||
}
|
||
|
||
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
|
||
fill_model = QueueAwareFillModel()
|
||
wqi_predictors = {coin: WQIPredictor(z_entry=2.0, max_hold_seconds=30,
|
||
stop_loss_bps=5.0, take_profit_bps=10.0,
|
||
size=0.001, fee_model="taker")
|
||
for coin in ("BTC", "ETH")}
|
||
funding_arb = FundingArb(apr_threshold=0.30, apr_exit=0.10, size=0.001,
|
||
max_hold_hours=48.0, taker_fee_pct=TAKER_FEE)
|
||
|
||
# ═══════════════════════ 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)
|
||
|
||
# Hurst/VPIN — feed BTC price into dollar bars
|
||
if len(btc_prices) >= 3:
|
||
try:
|
||
from strategies.hurst_vpin_live import HurstVPINLive
|
||
if "_hv_live" not in dir():
|
||
globals()["_hv_live"] = HurstVPINLive(
|
||
threshold=50000.0, hurst_entry=0.55, vpin_threshold=0.25
|
||
)
|
||
hv_signal = globals()["_hv_live"].feed_price(btc)
|
||
if hv_signal:
|
||
STRATEGIES["Hurst VPIN"]["signals"].append({
|
||
"time": time.time(),
|
||
"signal": hv_signal["signal"],
|
||
"strength": hv_signal["hurst"],
|
||
"reason": f"H={hv_signal['hurst']:.2f}_V={hv_signal['vpin']:.2f}"
|
||
})
|
||
except: pass
|
||
|
||
# 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: SMA deviation on ETH (prior 19, exclude current)
|
||
if len(eth_prices) >= 20:
|
||
w = list(eth_prices)[-20:]
|
||
eth_now = eth_prices[-1]
|
||
prior = w[:-1]
|
||
sma = sum(prior) / len(prior)
|
||
vstd = math.sqrt(sum((p-sma)**2 for p in prior) / len(prior))
|
||
dev = (eth_now - sma) / 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)})
|
||
|
||
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":0.025000,
|
||
"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, bid_depth=2.0, ask_depth=2.0):
|
||
"""Avellaneda-Stoikov: queue-aware spread capture with regime gating.
|
||
|
||
Uses QueueAwareFillModel instead of random probabilities.
|
||
"""
|
||
cfg = STRATEGIES["Avellaneda-Stoikov"]
|
||
if btc_bid <= 0 or btc_ask <= 0:
|
||
return
|
||
|
||
regime = current_regime
|
||
spread = btc_ask - btc_bid
|
||
|
||
if regime == "HIGH_VOL" and spread > 30:
|
||
return
|
||
|
||
mid = (btc_bid + btc_ask) / 2
|
||
sz = cfg["size"]
|
||
quote_bid = btc_bid
|
||
quote_ask = btc_ask
|
||
|
||
if regime == "LOW_VOL":
|
||
quote_bid = btc_bid + spread * 0.05
|
||
quote_ask = btc_ask - spread * 0.05
|
||
elif regime == "HIGH_VOL":
|
||
quote_bid = btc_bid - spread * 0.1
|
||
quote_ask = btc_ask + spread * 0.1
|
||
|
||
depth = max(bid_depth, ask_depth, 1.0)
|
||
depth_ahead = depth * 0.5
|
||
|
||
bid_fill = fill_model.check_fill(
|
||
aggressor_side="sell", agg_size=depth * 0.3, agg_price=max(quote_bid, 1),
|
||
our_price=quote_bid, our_size=sz, depth_ahead=depth_ahead,
|
||
)
|
||
if bid_fill["filled"] and cfg["position"] <= 0:
|
||
fee = sz * bid_fill["fill_size"] * quote_bid * MAKER_FEE
|
||
cfg["fee_paid"] += fee
|
||
spread_profit = bid_fill["fill_size"] * (btc_ask - quote_bid) / 2
|
||
if cfg["position"] < 0:
|
||
close_pnl = abs(cfg["position"]) * (cfg["entry_price"] - quote_bid)
|
||
cfg["pnl"] += close_pnl
|
||
if close_pnl > 0:
|
||
cfg["wins"] += 1
|
||
cfg["entry_price"] = quote_bid
|
||
cfg["position"] = sz
|
||
cfg["pnl"] += spread_profit - 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"]}
|
||
)
|
||
return
|
||
|
||
ask_fill = fill_model.check_fill(
|
||
aggressor_side="buy", agg_size=depth * 0.3, agg_price=min(quote_ask, mid * 2),
|
||
our_price=quote_ask, our_size=sz, depth_ahead=depth_ahead,
|
||
)
|
||
if ask_fill["filled"] and cfg["position"] >= 0:
|
||
fee = sz * ask_fill["fill_size"] * quote_ask * MAKER_FEE
|
||
cfg["fee_paid"] += fee
|
||
spread_profit = ask_fill["fill_size"] * (quote_ask - btc_bid) / 2
|
||
if cfg["position"] > 0:
|
||
close_pnl = cfg["position"] * (quote_ask - cfg["entry_price"])
|
||
cfg["pnl"] += close_pnl
|
||
if close_pnl > 0:
|
||
cfg["wins"] += 1
|
||
cfg["entry_price"] = quote_ask
|
||
cfg["position"] = -sz
|
||
cfg["pnl"] += spread_profit - 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:],
|
||
"wqi": {c: wqi_predictors[c].summary() for c in wqi_predictors},
|
||
"funding_arb": funding_arb.summary(),
|
||
"fill_model": {
|
||
"fill_rate": round(fill_model.fill_rate(), 4),
|
||
"fills": fill_model.fill_count,
|
||
"skips": fill_model.skip_count,
|
||
},
|
||
"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 with queue-aware fills
|
||
bid_depth = sum(sz for _, sz in bids[:10]) if bids else 2.0
|
||
ask_depth = sum(sz for _, sz in asks[:10]) if asks else 2.0
|
||
simulate_avellaneda(btc_bid, btc_ask, bid_depth, ask_depth)
|
||
|
||
# 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)
|
||
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,
|
||
})
|
||
|
||
# WQI Predictor: directional signal from queue imbalance
|
||
if bids and asks and btc > 0:
|
||
try:
|
||
wqi_signal = wqi_predictors.get("BTC")
|
||
if wqi_signal:
|
||
sig = wqi_signal.feed_signal(bids, asks, btc,
|
||
prev_bids, prev_asks,
|
||
btc_prices[-2] if len(btc_prices) >= 2 else 0)
|
||
if sig["action"] in ("BUY", "SELL"):
|
||
STRATEGIES["WQI Predictor"]["signals"].append({
|
||
"time": time.time(),
|
||
"signal": sig["action"],
|
||
"strength": abs(sig["z_score"]) / 3.0,
|
||
"reason": f"z={sig['z_score']:.2f}_wqi={sig['wqi']:.3f}",
|
||
})
|
||
except Exception:
|
||
pass
|
||
|
||
# Funding Rate Arb: check funding signal
|
||
if funding_rates and isinstance(funding_rates[-1], dict):
|
||
fr = funding_rates[-1].get("BTC", 0)
|
||
if fr != 0:
|
||
annual_apr = abs(fr) * 1095
|
||
arb_signal = funding_arb.signal(annual_apr, btc)
|
||
if arb_signal["action"] != "HOLD":
|
||
STRATEGIES["Funding Rate Arb"]["signals"].append({
|
||
"time": time.time(),
|
||
"signal": arb_signal["action"],
|
||
"strength": min(1.0, annual_apr),
|
||
"reason": arb_signal.get("reason", ""),
|
||
})
|
||
|
||
# Process next strategy's signals (round-robin all 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())
|