Files
ftdt-quant-lab/live/paper_trader.py
T
ramseshk acf3a556ec Regime-switching Avellaneda-Stoikov + Advanced Strategies research doc
Implemented regime detection in paper trader:
- Rolling 30-tick volatility classifies market as LOW_VOL/NORMAL/HIGH_VOL
- A-S fill probability adapts: 25% (low vol), 15% (normal), 8% (high vol)
- HIGH_VOL with spreads >$30: skip trading (adverse selection protection)
- Regime shown on dashboard header with color-coded badge

Added docs/ADVANCED_STRATEGIES.md — comprehensive research covering:
  1. Deep Learning LOB Prediction (Transformers/TLOB)
  2. Latency Arbitrage in Fragmented Markets
  3. Hawkes Process OFI Modeling
  4. Cross-Chain MEV Arbitrage
  5. Institutional Capital Flow Arbitrage (ETF flows)
  6. Hybrid Transformer + Hawkes Fusion
  7. Implementation Roadmap (Phase 1-4)

All strategies referenced with papers from arXiv, SSRN, and empirical studies.
2026-08-04 04:54:26 +00:00

513 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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
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 (realistic for paper fills)
SLIPPAGE_BPS = 1.0 # 1 bps slippage
# ═══════════════════════ 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,
"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,
"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,
"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,
"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,
"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,
"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,
"description": "VWAP deviation — buys below VWAP, sells above. Oscillates around fair value.",
},
}
trades_log: list[dict] = []
equity_history: list[dict] = []
strategy_equity: dict = {name: deque(maxlen=300) 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
# ═══════════════════════ 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
# OFI
if len(btc_prices) >= 5:
ret = (btc - btc_prices[-5]) / btc_prices[-5]
if ret > 0.0005:
STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"SELL","strength":ret})
elif ret < -0.0005:
STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(ret)})
# 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:
btc_fr = funding_rates[-1].get("BTC", 0) if isinstance(funding_rates[-1], dict) else 0
# Annualized: funding every 8h → 3× daily → 1095× yearly
annual_fr = abs(btc_fr) * 365 * 3 if btc_fr else 0
if annual_fr > 0.05: # >5% APR
STRATEGIES["Funding Rate Arb"]["signals"].append(
{"time":time.time(),"signal":"SELL" if btc_fr > 0 else "BUY",
"strength":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):
"""Simulate a trade fill at market price with fees."""
cfg = STRATEGIES[name]
sz = cfg["size"]
notional = sz * price
fee = notional * TAKER_FEE
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"]})
# ═══════════════════════ 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 * TAKER_FEE
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,
}
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" 7 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:
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
btc_bid, btc_ask = get_mainnet_orderbook("BTC")
# Avellaneda-Stoikov: simulate spread capture
simulate_avellaneda(btc_bid, btc_ask)
# Process next strategy's signals
name = strategy_names[idx % 7]
idx += 1
cfg = STRATEGIES[name]
if name == "Avellaneda-Stoikov":
continue # Already handled above
# Check for signals
if not cfg["signals"]:
continue
sig = cfg["signals"][-1]
signal_str = str(sig["signal"])
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)
log.info(f"[{name[:4]:4s}] PAPER BUY {cfg['size']} {coin} @ ${px:,.1f} | PnL: ${cfg['pnl']:+.2f}")
elif "SELL" in signal_str.upper():
simulate_fill(name, "SELL", coin, px)
log.info(f"[{name[:4]:4s}] PAPER SELL {cfg['size']} {coin} @ ${px:,.1f} | PnL: ${cfg['pnl']:+.2f}")
# 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())