Files
ftdt-quant-lab/live/paper_trader.py
T

661 lines
29 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
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
# 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, 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 * 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,
"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)
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())