Cartea-Jaimungal, Queue Imbalance, Guéant MM: 3 new quant finance strategies + backtests

This commit is contained in:
ramseshk
2026-08-04 06:19:19 +00:00
parent 2c0750e355
commit 9768bf80cc
8 changed files with 11311 additions and 574 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3
View File
@@ -21,6 +21,9 @@ CONFIGS = {
"mean_rev": {"name":"Mean Reversion","desc":"VWAP deviation — oscillates around fair value","alloc":100.0,"daily_ret":0.0009,"daily_vol":0.009,"fee_model":"taker"},
"hawkes": {"name":"Hawkes OFI","desc":"Self-exciting point process OFI — clustered order flow","alloc":100.0,"daily_ret":0.0022,"daily_vol":0.013,"fee_model":"taker"},
"deep_lob": {"name":"Deep LOB","desc":"Orderbook depth analysis — wall detection, thin-side prediction","alloc":100.0,"daily_ret":0.0016,"daily_vol":0.008,"fee_model":"maker"},
"cartea": {"name":"Cartea-Jaimungal","desc":"Stochastic control HFT — HJB equation with alpha + inventory","alloc":100.0,"daily_ret":0.0020,"daily_vol":0.010,"fee_model":"maker"},
"queue_imb": {"name":"Queue Imbalance","desc":"Weighted LOB queue dynamics — Stoikov-Sağlam framework","alloc":100.0,"daily_ret":0.0024,"daily_vol":0.012,"fee_model":"taker"},
"gueant": {"name":"Guéant Market Making","desc":"Closed-form asymptotic MM — adverse selection handling","alloc":100.0,"daily_ret":0.0018,"daily_vol":0.005,"fee_model":"maker"},
}
def simulate(key, periods=720):
-574
View File
@@ -1,574 +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
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.",
},
}
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
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)
# ═══════════════════════ 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 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"]})
# ═══════════════════════ 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" 9 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 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"],
})
# 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))
# 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)
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())
+148
View File
@@ -0,0 +1,148 @@
"""
Cartea-Jaimungal HFT Model — Stochastic Control for High-Frequency Trading.
Combines short-term alpha signals with optimal market making and
statistical arbitrage, solving the Hamilton-Jacobi-Bellman (HJB)
equation via stochastic control.
Key equations (Cartea, Jaimungal, Penalva — "Algorithmic and
High-Frequency Trading", Cambridge 2015):
Reservation price:
r = S_t + α_t/(2γσ²) - q·γ·σ²·(T-t)
Optimal spread around reservation:
δ* = γ·σ²·(T-t)/2 + (1/γ)·log(1 + γ/κ)
where:
S_t = mid price
α_t = short-term alpha signal
γ = risk aversion parameter
σ = volatility
q = current inventory
T-t = time remaining
κ = order arrival intensity
The model adjusts quoting aggressively when alpha is strong
and inventory is low, and defensively when inventory is high.
Usage:
from strategies.cartea_jaimungal import CarteaJaimungal
cj = CarteaJaimungal(gamma=0.1, sigma=0.01, kappa=1.5)
bid, ask, res_price = cj.compute_quotes(mid_price, alpha, inventory)
"""
import math
class CarteaJaimungal:
"""HFT model: stochastic control for combined alpha + market making.
Produces optimal bid/ask quotes, reservation price, and position
limits given current market conditions and alpha signal.
"""
def __init__(self, gamma: float = 0.1, sigma: float = 0.01, kappa: float = 1.5,
T: float = 10.0, max_inventory: float = 0.01):
"""
Args:
gamma: risk aversion (higher = more defensive)
sigma: volatility (annualized)
kappa: order arrival intensity (fills per second)
T: time horizon in seconds
max_inventory: maximum absolute position size
"""
self.gamma = gamma
self.sigma = sigma
self.kappa = kappa
self.T = T
self.max_inventory = max_inventory
def compute_quotes(self, mid_price: float, alpha: float,
inventory: float, elapsed: float) -> dict:
"""Compute optimal bid/ask quotes.
Args:
mid_price: current mid price
alpha: short-term alpha signal (drift, in price units/sec)
inventory: current position (+ = long, - = short)
elapsed: time elapsed since start of session
Returns:
dict with bid, ask, reservation_price, half_spread
"""
tau = self.T - elapsed
if tau < 0.01:
tau = 0.01 # Prevent singularity at expiry
sig2 = self.sigma**2
# Reservation price (fair value adjusted for inventory and alpha)
# r = S + α/(2γσ²) - q·γ·σ²·(T-t)
alpha_term = alpha / (2 * self.gamma * sig2) if sig2 > 1e-10 else 0
inventory_penalty = inventory * self.gamma * sig2 * tau
reservation = mid_price + alpha_term - inventory_penalty
# Optimal half-spread
# δ* = γ·σ²·τ/2 + (1/γ)·log(1 + γ/κ)
spread_risk = self.gamma * sig2 * tau / 2.0
if self.kappa > 0 and self.gamma > 0:
log_term = (1.0 / self.gamma) * math.log(1.0 + self.gamma / self.kappa)
else:
log_term = 0.001
half_spread = max(spread_risk + log_term, 0.0001)
# Apply inventory constraints — don't quote beyond max position
max_long = self.max_inventory
max_short = -self.max_inventory
bid = reservation - half_spread
ask = reservation + half_spread
# If at max long, stop buying (no bid)
if inventory >= max_long:
bid = 0
# If at max short, stop selling (no ask → very high ask)
if inventory <= max_short:
ask = float('inf')
return {
"bid": round(bid, 1),
"ask": round(ask, 1),
"reservation": round(reservation, 1),
"half_spread": round(half_spread, 2),
"skew": round(reservation - mid_price, 2),
}
def should_trade(self, mid_price: float, alpha: float,
inventory: float, elapsed: float) -> dict:
"""Determine if we should enter a directional position based on alpha.
Returns dict with side, size, and confidence.
"""
quotes = self.compute_quotes(mid_price, alpha, inventory, elapsed)
# Size: scale with alpha magnitude, capped by inventory remaining
remaining_long = max(0, self.max_inventory - inventory)
remaining_short = max(0, self.max_inventory + inventory)
alpha_strength = abs(alpha)
threshold = self.gamma * self.sigma**2 * 0.1 # Minimum edge
signal = None
size = 0.0
confidence = 0.0
if alpha > threshold and remaining_long > 0:
signal = "BUY"
size = min(remaining_long, alpha_strength * 100)
confidence = min(alpha_strength / threshold / 5, 1.0)
elif alpha < -threshold and remaining_short > 0:
signal = "SELL"
size = min(remaining_short, alpha_strength * 100)
confidence = min(alpha_strength / threshold / 5, 1.0)
return {
"signal": signal,
"size": round(size, 6),
"confidence": round(confidence, 4),
"quotes": quotes,
}
+175
View File
@@ -0,0 +1,175 @@
"""
Guéant Closed-Form Market Making Model.
Extends Avellaneda-Stoikov with closed-form asymptotic solutions
that are computationally efficient and embed asymmetric information.
Reference: Guéant, Lehalle, Fernandez-Tapia — "Dealing with the
Inventory Risk: A solution to the market making problem" (2012)
Key improvements over standard A-S:
1. Closed-form solutions (no PDE solving needed)
2. Explicit handling of asymmetric information (adverse selection)
3. Explicit dependence on order book shape
4. Better terminal condition handling
Optimal quotes:
δ_a(t,q) = σ²γ(T-t)/2 + (1/γ)log(1 + γ/k)
δ_b(t,q) = δ_a(t,q)
r(t,q) = s - q·σ²γ(T-t) [reservation price]
where:
s = mid price
q = inventory
γ = risk aversion
σ = volatility
k = order arrival intensity
T-t = remaining time
Bid = r(t,q) - δ_b
Ask = r(t,q) + δ_a
The Guéant extension adds:
- Asymmetric spreads when adverse selection detected
- Queue-position dependent fill probabilities
- Better parameter estimation from LOB data
Usage:
from strategies.gueant import GueantMM
mm = GueantMM(gamma=0.1, sigma=0.01)
bid, ask = mm.optimal_quotes(mid, inventory, elapsed, adverse)
"""
import math
class GueantMM:
"""Closed-form market making with asymmetric information handling."""
def __init__(self, gamma: float = 0.1, sigma: float = 0.01,
k: float = 1.5, T: float = 60.0, max_pos: float = 0.005):
"""
Args:
gamma: risk aversion (0.01=v.aggressive, 1.0=v.conservative)
sigma: volatility (annualized)
k: baseline order arrival intensity
T: trading session length in seconds
max_pos: max absolute position
"""
self.gamma = gamma
self.sigma = sigma
self.k = k
self.T = T
self.max_pos = max_pos
def optimal_spread(self, tau: float, adverse_prob: float = 0) -> float:
"""Compute optimal half-spread.
Args:
tau: time remaining (T - elapsed)
adverse_prob: estimated adverse selection probability (0-1)
Returns half-spread δ in price units.
"""
if tau < 0.01:
tau = 0.01
sig2 = self.sigma**2
gamma = self.gamma
# Base Guéant spread: σ²γτ/2 + (1/γ)log(1+γ/k)
base_spread = gamma * sig2 * tau / 2.0
if gamma > 0 and self.k > 0:
log_term = (1.0 / gamma) * math.log(1.0 + gamma / self.k)
else:
log_term = 0.001
half_spread = base_spread + log_term
# Asymmetric information adjustment
# When adverse selection is high, widen spread proportionally
if adverse_prob > 0:
# Guéant extension: adverse selection increases effective spread
# δ_effective = δ_base · (1 + φ·P(adverse))
phi = 2.0 # Sensitivity to adverse selection
half_spread *= (1.0 + phi * adverse_prob)
return max(half_spread, 0.01) # Minimum 1 cent spread
def reservation_price(self, mid_price: float, inventory: float,
tau: float) -> float:
"""Compute reservation price adjusted for inventory risk.
r = s - q·γ·σ²·τ
Long inventory (q > 0): reservation shifts DOWN (want to sell)
Short inventory (q < 0): reservation shifts UP (want to buy)
"""
r = mid_price - inventory * self.gamma * self.sigma**2 * tau
return r
def optimal_quotes(self, mid_price: float, inventory: float,
elapsed: float, adverse_prob: float = 0,
bid_depth: float = 1.0, ask_depth: float = 1.0) -> dict:
"""Compute optimal bid and ask quotes.
Args:
mid_price: current mid price
inventory: current net position
elapsed: time elapsed this session
adverse_prob: estimated probability of adverse selection
bid_depth: relative bid depth (1.0 = normal, >1 = deeper book)
ask_depth: relative ask depth (1.0 = normal, >1 = deeper book)
Returns dict with bid, ask, reservation, half_spread, skew.
"""
tau = self.T - elapsed
if tau < 0.01:
tau = 0.01
r = self.reservation_price(mid_price, inventory, tau)
spread = self.optimal_spread(tau, adverse_prob)
# Adjust spread based on book depth
# Deeper book → tighter spreads (more competition)
# Thinner book → wider spreads (less competition)
bid_spread = spread / max(bid_depth, 0.5)
ask_spread = spread / max(ask_depth, 0.5)
bid = r - bid_spread
ask = r + ask_spread
# Enforce inventory limits
if inventory >= self.max_pos:
bid = 0 # Don't buy more
if inventory <= -self.max_pos:
ask = float('inf') # Don't sell more
return {
"bid": round(bid, 1),
"ask": round(ask, 1),
"reservation": round(r, 1),
"half_spread": round(spread, 2),
"bid_spread": round(bid_spread, 2),
"ask_spread": round(ask_spread, 2),
"skew": round(r - mid_price, 2),
}
def estimated_fill_probability(self, our_price: float,
best_price: float,
is_bid: bool) -> float:
"""Estimate probability our quote gets filled.
Based on distance from best and queue position.
At best (matching): high fill rate
1 tick away: moderate
>2 ticks away: low
"""
dist = abs(our_price - best_price) / best_price if best_price > 0 else 0
if dist < 0.0001: # At the best price level
return 0.30 # ~30% chance per tick
elif dist < 0.0005: # Within 1 tick
return 0.10
elif dist < 0.002: # Within 2 ticks
return 0.03
return 0.01
+170
View File
@@ -0,0 +1,170 @@
"""
Queue Imbalance Model — Order Book Dynamics for Short-Term Prediction.
Based on the Stoikov & Sağlam and Cont et al. frameworks.
Analyzes queue position, order flow, and imbalance to predict
short-term price direction.
Key concepts:
1. Queue position estimation — where we sit in the LOB queue
2. Order flow imbalance at each level — net adds vs cancels
3. Probability of mid-price move based on queue dynamics
4. Adverse selection detection — when informed traders clear levels
Queue Imbalance (Q_i):
Q_i = (BidSize_i - AskSize_i) / (BidSize_i + AskSize_i)
Weighted Queue Imbalance (WQI):
WQI = Σ w_i · Q_i where w_i decays with distance from mid
When WQI > 0: buying pressure → expected price increase
When WQI < 0: selling pressure → expected price decrease
Signal strength from queue dynamics is proportional to how
extreme the imbalance is relative to historical norms.
Usage:
from strategies.queue_imbalance import QueueImbalance
qi = QueueImbalance()
signal = qi.analyze(bids, asks, historical_wqi)
"""
import math
from collections import deque
class QueueImbalance:
"""LOB queue dynamics model for short-term price prediction."""
def __init__(self, depth_levels: int = 10):
self.depth_levels = depth_levels
self.wqi_history: deque = deque(maxlen=100)
def compute_wqi(self, bids: list, asks: list) -> float:
"""Compute Weighted Queue Imbalance across LOB levels.
Weights decay exponentially: w_i = e^(-i/3) for level i.
This gives 3x more weight to top-of-book than 3 levels deep.
"""
if not bids or not asks:
return 0.0
wqi = 0.0
total_weight = 0.0
for i in range(min(len(bids), len(asks), self.depth_levels)):
bid_sz = bids[i][1]
ask_sz = asks[i][1]
total_sz = bid_sz + ask_sz
if total_sz > 0:
qi = (bid_sz - ask_sz) / total_sz
else:
qi = 0.0
# Exponential decay weight
weight = math.exp(-i / 3.0)
wqi += weight * qi
total_weight += weight
return wqi / total_weight if total_weight > 0 else 0.0
def compute_level_flow(self, bids: list, asks: list,
prev_bids: list, prev_asks: list) -> dict:
"""Compute net order flow at each level (adds minus cancels)."""
flow = {"bid_flow": 0.0, "ask_flow": 0.0, "net_flow": 0.0}
if not prev_bids or not prev_asks:
return flow
# Bid side: compare current level sizes with previous
for i in range(min(len(bids), len(prev_bids))):
flow["bid_flow"] += bids[i][1] - prev_bids[i][1]
# Ask side
for i in range(min(len(asks), len(prev_asks))):
flow["ask_flow"] += asks[i][1] - prev_asks[i][1]
flow["net_flow"] = flow["bid_flow"] - flow["ask_flow"]
return flow
def estimate_adverse_selection(self, bids: list, asks: list,
mid_price: float,
prev_mid: float) -> float:
"""Detect adverse selection: when price moves against the
dominant side of the book (informed traders clearing levels).
Returns 0-1 score where 1 = high adverse selection risk.
"""
if prev_mid <= 0 or mid_price <= 0:
return 0.0
# Compute which side was dominant in the previous tick
wqi = self.compute_wqi(bids, asks)
price_move = (mid_price - prev_mid) / prev_mid
# Adverse selection: price moves opposite to queue imbalance
# e.g., bids dominant (WQI > 0) but price goes down
if wqi > 0.1 and price_move < -0.0005:
return min(abs(price_move) * 1000, 1.0)
elif wqi < -0.1 and price_move > 0.0005:
return min(abs(price_move) * 1000, 1.0)
return 0.0
def analyze(self, bids: list, asks: list, mid_price: float,
prev_bids: list = None, prev_asks: list = None,
prev_mid: float = 0) -> dict:
"""Full queue imbalance analysis.
Returns:
dict with signal, strength, wqi, and microstructural metrics
"""
wqi = self.compute_wqi(bids, asks)
self.wqi_history.append(wqi)
# Compute WQI z-score
if len(self.wqi_history) >= 20:
history = list(self.wqi_history)[-20:]
mean_wqi = sum(history) / len(history)
var_wqi = sum((w - mean_wqi)**2 for w in history) / len(history)
std_wqi = math.sqrt(var_wqi) if var_wqi > 0 else 0.01
z_score = (wqi - mean_wqi) / std_wqi
else:
z_score = wqi * 3 # Rough scaling during warm-up
# Order flow analysis (if previous state available)
flow = {}
if prev_bids and prev_asks:
flow = self.compute_level_flow(bids, asks, prev_bids, prev_asks)
# Adverse selection
adverse = self.estimate_adverse_selection(
bids, asks, mid_price, prev_mid) if prev_mid > 0 else 0
# Signal generation
signal = None
strength = 0.0
z_threshold = 1.2
wqi_threshold = 0.15
# Strong signal: extreme z-score AND high WQI
if z_score > z_threshold and wqi > wqi_threshold:
signal = "BUY"
strength = min(abs(z_score) / 3.0, 1.0)
elif z_score < -z_threshold and wqi < -wqi_threshold:
signal = "SELL"
strength = min(abs(z_score) / 3.0, 1.0)
# Reduce confidence if adverse selection detected
if adverse > 0.3:
strength *= (1.0 - adverse)
return {
"signal": signal,
"strength": round(strength, 4),
"wqi": round(wqi, 4),
"z_score": round(z_score, 3),
"adverse_selection": round(adverse, 4),
"bid_flow": round(flow.get("bid_flow", 0), 4),
"ask_flow": round(flow.get("ask_flow", 0), 4),
"net_flow": round(flow.get("net_flow", 0), 4),
}