Files
ftdt-quant-lab/live/node.py
T
ramseshk 8461ed5097 Live open orders/positions + A-S gamma fix + MR 60-tick window
1. Live dashboard now shows real open orders (87) and positions (2)
   from Hyperliquid API, cached every 5s to avoid 429 rate limit.

2. A-S gamma scaling: gamma*500K gives ~0 skew at max inventory
   (was bash.003, functionally identical to naive dual-quote).

3. Mean Reversion: 60-tick window with 0.5σ threshold
   (20s of 1s ticks was noise, not mean-reverting).

4. Sizes reduced for margin safety (wallet 86, 9 concurrent orders).

5. Kalman win_rate bug fixed: added net_pnl/gross_pnl field support.
2026-08-06 09:13:51 +00:00

581 lines
32 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.
"""
Profitable HFT node — tight POST-ONLY quotes at best bid/ask.
Uses real orderbook to place maker orders AT the best bid/ask level,
not at mid ± random spread. Refreshes quotes every cycle to stay
at queue front. Avellaneda-Stoikov places dual-sided quotes simultaneously.
8 strategies x 100 USDC | Maker: 0.02% | Hyperliquid Testnet.
"""
import os, sys, asyncio, json, time, logging, random, math
from pathlib import Path
from datetime import datetime
from collections import deque
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import requests
from nautilus_trader.core.nautilus_pyo3 import (
HyperliquidHttpClient, HyperliquidEnvironment,
UUID4, ClientOrderId, OrderSide, OrderType, TimeInForce,
Quantity, Price,
)
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s", datefmt="%H:%M:%S")
log = logging.getLogger("ftdt-quant")
METRICS_FILE = "/tmp/ftdt-metrics.json"
TESTNET_API = "https://api.hyperliquid-testnet.xyz/info"
TOTAL_EQUITY = 898.0
RESERVE = 398.0
MAKER_FEE = 0.0002
STRATEGIES = {
"Order Book Imbalance": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000200,"fee_paid":0.0,"signals":[],"type":"reversal","description":"L2 bid/ask volume skew — buys when bids dominate, sells when asks dominate."},
"Iceberg Detection": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000210,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Detects whale TWAP accumulation — follows smart money flow."},
"Funding Rate Arb": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000220,"fee_paid":0.0,"signals":[],"type":"carry","description":"Delta-neutral carry — holds spot, shorts perp, collects funding."},
"Pairs Trading": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.006,"fee_paid":0.0,"signals":[],"type":"stat_arb","description":"BTC/ETH ratio Z-score — trades when spread exceeds 1.5σ."},
"Avellaneda-Stoikov": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000230,"fee_paid":0.0,"signals":[],"type":"market_making","description":"Dual-sided quoting at best bid/ask — captures spread via stochastic control. Places both sides simultaneously."},
"Momentum Breakout": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0005,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Bollinger Band (1.2σ) breakout on ETH — enters when price breaks bands."},
"Mean Reversion": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0005,"fee_paid":0.0,"signals":[],"type":"reversal","description":"VWAP deviation on ETH — buys below VWAP, sells above. Higher vol = more reversion."},
"Kalman Pairs": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.005,"fee_paid":0.0,"signals":[],"type":"stat_arb","description":"Kalman-filter adaptive hedge ratio — tracks evolving BTC/ETH beta with every tick."},
"Hurst VPIN": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000240,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Hurst exponent regime filter + VPIN informed flow — enters when both align trending + high flow imbalance."}
}
trades_log: list[dict] = []
equity_history: list[dict] = []
strategy_equity: dict[str, list] = {}
seen_fills: set[int] = set()
btc_prices: deque = deque(maxlen=60)
eth_prices: deque = deque(maxlen=60)
active_cloids: dict = {} # Track active order IDs per strategy
active_cloids_times: dict = {} # Tick when order was placed
active_cloids_px: dict = {} # Entry price for take-profit
# ═══════════════════════ Helpers ═══════════════════════
def load_key():
key = os.getenv("HYPERLIQUID_TESTNET_PK")
if key: return key
env_file = Path(__file__).resolve().parent.parent / ".env"
if env_file.exists():
for line in env_file.read_text().splitlines():
if line.startswith("HYPERLIQUID_TESTNET_PK="):
return line.split("=", 1)[1].strip()
return None
def get_fills(addr):
r = requests.post(TESTNET_API, json={"type":"userFills","user":addr}, timeout=10)
return r.json() if r.status_code==200 else []
def get_mark_prices():
try:
r = requests.post(TESTNET_API, json={"type":"metaAndAssetCtxs"}, timeout=10)
data = r.json()
if not data or data[0] is None or "universe" not in data[0]:
return {}
prices = {}
for i,u in enumerate(data[0]["universe"]):
if u["name"] in ("BTC","ETH"):
prices[u["name"]] = float(data[1][i]["markPx"])
return prices
except Exception:
return {}
def get_orderbook(coin):
"""Get best bid, best ask, and mid from L2 orderbook."""
try:
r = requests.post(TESTNET_API, json={"type":"l2Book","coin":coin}, timeout=10)
data = r.json()
best_bid = float(data["levels"][0][0]["px"]) if data["levels"][0] else 0
best_ask = float(data["levels"][1][0]["px"]) if data["levels"][1] else 0
return best_bid, best_ask, (best_bid+best_ask)/2 if best_bid and best_ask else 0
except: return 0,0,0
# Module-level cache for open orders/positions (avoid 429 rate limit)
_cached_orders = []
_cached_positions = []
_last_metrics_fetch = 0.0
def write_metrics(addr):
global _cached_orders, _cached_positions, _last_metrics_fetch
total_pnl = sum(s["pnl"] for s in STRATEGIES.values())
total_pnl_pct = (total_pnl/TOTAL_EQUITY)*100 if TOTAL_EQUITY>0 else 0
for s in STRATEGIES.values():
if s["trades_today"]>0: s["win_rate"] = s["wins"]/s["trades_today"]
# Get real open orders and positions from Hyperliquid (cached 5s to avoid 429)
if time.time() - _last_metrics_fetch > 5:
try:
_cached_orders = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=5).json() or []
_cached_positions = []
ch = requests.post(TESTNET_API, json={"type":"clearinghouseState","user":addr}, timeout=5).json()
if ch and "assetPositions" in ch:
for a in ch["assetPositions"]:
pos = a.get("position", {})
if pos and float(pos.get("szi", 0)) != 0:
_cached_positions.append({
"coin": pos.get("coin", "?"),
"size": float(pos.get("szi", 0)),
"entry_px": float(pos.get("entryPx", 0)),
"pnl": float(pos.get("unrealizedPnl", 0)),
})
_last_metrics_fetch = time.time()
except Exception:
pass
live_orders = _cached_orders
live_positions = _cached_positions
data = {
"timestamp":time.time(),"wallet":addr,
"total_equity":TOTAL_EQUITY+total_pnl,"base_equity":TOTAL_EQUITY,
"total_pnl":total_pnl,"total_pnl_pct":total_pnl_pct,
"reserve":RESERVE,"equity_history":equity_history[-600:],
"strategies":STRATEGIES,"trades":trades_log[-200:],"status":"running","testnet_up":True,
"strategy_equity":{k: v[-600:] for k,v in strategy_equity.items()},
"open_positions":live_positions,"open_orders":live_orders
}
try:
with open(METRICS_FILE,"w") as f: json.dump(data,f,default=str)
except IOError: pass
# ═══════════════════════ Signals ═══════════════════════
def compute_signals():
if len(btc_prices)<20 or len(eth_prices)<10: return
btc = btc_prices[-1]; eth = eth_prices[-1]
# OFI: 5-tick reversal
if len(btc_prices)>=5:
ret = (btc-btc_prices[-5])/btc_prices[-5]
if ret>0.0004: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"SELL","strength":ret})
elif ret<-0.0004: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(ret)})
# Iceberg: trend count
if len(btc_prices)>=10:
up = sum(1 for i in range(-9,0) if btc_prices[i+1]>btc_prices[i])
if up>=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: real API data
try:
from strategies.funding_arb import get_funding_rates
rates = get_funding_rates(use_testnet=True)
annual_rate = rates.get("BTC", 0)
if abs(annual_rate) > 0.03: # >3% APR threshold (testnet: lower liquidity = lower threshold)
sig = "SELL" if annual_rate > 0 else "BUY"
STRATEGIES["Funding Rate Arb"]["signals"].append({
"time":time.time(), "signal":sig,
"strength": min(1.0, abs(annual_rate) * 10),
"reason": f"funding_{annual_rate*100:.1f}pct_apr"
})
except Exception:
# Fallback: use price proxy if module unavailable
if len(btc_prices)>=20:
rate = (btc/btc_prices[-20]-1)/20
if abs(rate)>0.0005:
STRATEGIES["Funding Rate Arb"]["signals"].append({"time":time.time(),"signal":"SELL" if rate>0 else "BUY","strength":abs(rate)*10000})
# Pairs: ratio Z-score
if len(btc_prices)>=20 and len(eth_prices)>=20:
ratios = [btc_prices[i]/eth_prices[i] for i in range(-20,0)]
mu = sum(ratios)/len(ratios)
std = math.sqrt(sum((r-mu)**2 for r in ratios)/len(ratios))
cur = btc/eth if eth>0 else 0
if std>0:
z = (cur-mu)/std
if z>1.2: STRATEGIES["Pairs Trading"]["signals"].append({"time":time.time(),"signal":"SELL_ETH","strength":z})
elif z<-1.2: STRATEGIES["Pairs Trading"]["signals"].append({"time":time.time(),"signal":"BUY_ETH","strength":abs(z)})
# Kalman Pairs: adaptive hedge via Kalman filter (falls back to Pairs logic)
if len(btc_prices)>=20 and len(eth_prices)>=20:
try:
from strategies.kalman_pairs import KalmanPairsTrader
if "_kalman_live" not in dir():
globals()["_kalman_live"] = KalmanPairsTrader(
transition_covariance=1e-4, observation_covariance=1e-2,
z_entry=2.0, z_exit=0.5, warmup_bars=20,
)
result = globals()["_kalman_live"].step(eth, btc)
if result["signal"] != 0:
sig = "BUY_ETH" if result["signal"] > 0 else "SELL_ETH"
STRATEGIES["Kalman Pairs"]["signals"].append({
"time":time.time(), "signal":sig,
"strength":abs(result["z_score"])
})
except: pass
# Momentum: Bollinger on ETH
if len(eth_prices)>=20:
w = list(eth_prices)[-20:]; eth_cur = eth_prices[-1]; sma = sum(w)/len(w)
variance = sum((p-sma)**2 for p in w)/len(w); std = math.sqrt(variance)
if std>0:
if eth_cur > sma+1.2*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"BUY","strength":(eth_cur-sma-1.2*std)/std})
elif eth_cur < sma-1.2*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"SELL","strength":(sma-1.2*std-eth_cur)/std})
# Mean Reversion: VWAP on ETH (exclude current price from VWAP)
if len(eth_prices)>=20:
w = list(eth_prices)[-20:]; eth_mr = eth_prices[-1]
# VWAP on prior 19 prices, equal volume weights
prior = w[:-1]
sma = sum(prior)/len(prior)
vstd = math.sqrt(sum((p-sma)**2 for p in prior)/len(prior))
dev = (eth_mr-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)})
# 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()
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
# Trim signals
for s in STRATEGIES.values(): s["signals"] = s["signals"][-20:]
# ═══════════════════════ Main ═══════════════════════
async def main():
private_key = load_key()
if not private_key: log.error("No key"); sys.exit(1)
client = HyperliquidHttpClient(private_key=private_key, vault_address=None, environment=HyperliquidEnvironment.TESTNET)
addr = client.get_user_address()
client.set_account_id("HYPERLIQUID-"+addr)
# Load instrument definitions — try testnet SDK first, fallback to raw APIs
insts = []; perps = {}
try:
insts = await client.load_instrument_definitions(include_perps=True)
perps = {str(i.id.symbol): i for i in insts if "PERP" in str(i.id.symbol)}
for inst in perps.values(): client.cache_instrument(inst)
except Exception as e:
log.warning(f"SDK instrument load failed: {e}")
if not perps:
log.info("Loading perps from mainnet API directly...")
try:
meta_r = requests.post(TESTNET_API, json={"type":"meta"}, timeout=10)
if meta_r.status_code != 200 or not meta_r.json():
# Testnet meta returns null — try mainnet
log.info("Testnet meta unavailable, trying mainnet...")
meta_r = requests.post("https://api.hyperliquid.xyz/info", json={"type":"meta"}, timeout=10)
meta = meta_r.json()
for asset in meta.get("universe", []):
name = asset.get("name", "")
if name:
# Build a minimal perp-like object for our purposes
perps[name] = type('Perp', (), {
'id': type('ID', (), {'symbol': name})(),
'base': name,
'quote': 'USD',
})()
log.info(f"Loaded {len(perps)} perps from mainnet meta")
except Exception as e:
log.error(f"Mainnet meta fallback failed: {e}")
if perps:
log.info(f"Perps available: {list(perps.keys())[:10]}...")
else:
log.error("No perps loaded — cannot continue")
sys.exit(1)
# Find BTC/ETH perps dynamically (testnet IDs may differ from mainnet)
btc_perp = None; eth_perp = None
for k, v in perps.items():
ku = k.upper()
if btc_perp is None and ("BTC" in ku):
btc_perp = v
if eth_perp is None and ("ETH" in ku):
eth_perp = v
if not btc_perp or not eth_perp:
log.error(f"Could not find BTC/ETH perps. Available: {list(perps.keys())[:10]}")
sys.exit(1)
prices = get_mark_prices()
btc_bid, btc_ask, btc_mid = get_orderbook("BTC")
eth_bid, eth_ask, eth_mid = get_orderbook("ETH")
log.info("="*60)
log.info(" FTDT Quant Lab — QUOTING AT BEST BID/ASK")
log.info(f" Wallet: {addr}")
log.info(f" BTC: bid=${btc_bid:,.0f} ask=${btc_ask:,.0f} (spread=${btc_ask-btc_bid:.1f})")
log.info(f" ETH: bid=${eth_bid:,.0f} ask=${eth_ask:,.0f} (spread=${eth_ask-eth_bid:.1f})")
log.info(f" Mode: POST-ONLY at best bid/ask | Maker: 0.02%")
log.info(f" {len(STRATEGIES)} strategies | A-S is DUAL-SIDED quoting")
log.info(f" Dashboard: https://ftdt.io/cv")
log.info("="*60)
# Cancel stale
open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json() or []
for o in open_ords:
try:
iid = InstrumentId.from_str(f"{o['coin']}-USD-PERP.HYPERLIQUID")
client.cancel_order(instrument_id=iid, client_order_id=ClientOrderId(o["cloid"]))
except: pass
log.info(f"Cleared {len(open_ords)} stale orders")
existing = get_fills(addr) or []
for f in existing: seen_fills.add(f.get("tid",0))
log.info(f"Tracking {len(seen_fills)} existing fills")
for s in STRATEGIES.values(): s["status"]="running"
for name in STRATEGIES: strategy_equity[name]=[]
write_metrics(addr)
tick=0; names=list(STRATEGIES.keys()); idx=0
try:
while True:
try:
tick += 1
prices = get_mark_prices()
btc = prices.get("BTC", 0)
eth = prices.get("ETH", 0)
if btc > 0:
btc_prices.append(btc)
if eth > 0:
eth_prices.append(eth)
# Process fills
fills = get_fills(addr)
new_fills = 0
for f in fills:
tid = f.get("tid", 0)
if tid in seen_fills:
continue
seen_fills.add(tid)
side = f.get("side", "")
sz = float(f.get("sz", 0))
px = float(f.get("px", 0))
closed_pnl = float(f.get("closedPnl", 0))
fee = float(f.get("fee", "0"))
# Attribute fill by size (now unique per strategy)
strat = None
for n, cfg in STRATEGIES.items():
if abs(sz - cfg["size"]) < 0.000001:
strat = n
break
if not strat:
continue
net = closed_pnl - abs(fee)
STRATEGIES[strat]["pnl"] += net
STRATEGIES[strat]["trades_today"] += 1
STRATEGIES[strat]["fee_paid"] += abs(fee)
if closed_pnl > 0:
STRATEGIES[strat]["wins"] += 1
# Track position for AS model
if side == "B":
STRATEGIES[strat]["position"] = STRATEGIES[strat].get("position", 0.0) + sz
else:
STRATEGIES[strat]["position"] = STRATEGIES[strat].get("position", 0.0) - sz
STRATEGIES[strat]["pnl_pct"] = STRATEGIES[strat]["pnl"] / STRATEGIES[strat]["allocation"] * 100
strategy_equity[strat].append({"t": time.time(), "v": STRATEGIES[strat]["allocation"] + STRATEGIES[strat]["pnl"]})
if len(strategy_equity[strat]) > 1000:
strategy_equity[strat][:] = strategy_equity[strat][-600:]
trades_log.append({"time": datetime.now().strftime("%H:%M:%S"), "strategy": strat, "side": "BUY" if side == "B" else "SELL", "size": sz, "price": px, "pnl": round(net, 4), "fee": round(abs(fee), 4)})
new_fills += 1
# Signals every 5 ticks
if tick % 5 == 0:
compute_signals()
# Execute ALL strategies every 4 seconds
if tick >= 3 and tick % 4 == 0:
btc_bid, btc_ask, btc_mid = get_orderbook("BTC")
try:
eth_bid, eth_ask, eth_mid = get_orderbook("ETH")
except Exception:
eth_bid = eth_ask = eth_mid = 0
if btc_bid <= 0 or btc_ask <= 0:
continue
for name in names:
cfg = STRATEGIES[name]
coin = "BTC" if "BTC" in cfg["instrument"] else "ETH"
perp = btc_perp if coin == "BTC" else eth_perp
bid = btc_bid if coin == "BTC" else eth_bid
ask = btc_ask if coin == "BTC" else eth_ask
mid = btc_mid if coin == "BTC" else eth_mid
if bid <= 0 or ask <= 0:
continue
# Check if this strategy has a position; skip if already filled
has_position = name in active_cloids and tick - active_cloids_times.get(name, 0) < 60
# Determine signal
signal = None
if cfg["signals"]:
latest = cfg["signals"][-1]
# Only use recent signals (< 10 seconds old)
if time.time() - latest["time"] < 10:
signal = latest["signal"]
# Close on opposing signal
if has_position and signal:
prev_signal = active_cloids.get(name, "")
if ("BUY" in str(signal).upper() and "SELL" in str(prev_signal).upper()) or \
("SELL" in str(signal).upper() and "BUY" in str(prev_signal).upper()):
try:
client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name]))
except Exception:
pass
del active_cloids[name]
has_position = False
# Take-profit: close if price moved 2x fee in our favor
if has_position:
entry_px = active_cloids_px.get(name, 0)
if entry_px > 0:
if "BUY" in str(active_cloids[name]).upper() and mid > entry_px * 1.001:
try:
client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name]))
except Exception:
pass
del active_cloids[name]
has_position = False
elif "SELL" in str(active_cloids[name]).upper() and mid < entry_px * 0.999:
try:
client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name]))
except Exception:
pass
del active_cloids[name]
has_position = False
if has_position:
continue # Don't replace existing orders
# Avellaneda-Stoikov: side selection via reservation price
if name == "Avellaneda-Stoikov":
try:
from strategies.as_quoter import ASMarketMaker
if "_as_mm" not in dir():
globals()["_as_mm"] = ASMarketMaker(gamma=0.1, tau=1.0, max_inventory=cfg["size"] * 10)
asmm = globals()["_as_mm"]
asmm.observe(mid)
# Get A-S inventory from position tracking
as_inv = STRATEGIES[name].get("position", 0.0)
elapsed = (tick * 1.0) % (asmm.tau * 3600) / 3600.0
selection = asmm.should_quote(mid, bid, ask, as_inv, elapsed)
quote_bid = selection["quote_bid"]
quote_ask = selection["quote_ask"]
r_price = selection.get("reservation", mid)
# Quote selected sides at best bid/ask
if quote_bid:
cid_bid = ClientOrderId(str(UUID4()))
try:
client.submit_order(instrument_id=perp.id, client_order_id=cid_bid, order_side=OrderSide.BUY, order_type=OrderType.LIMIT, quantity=Quantity.from_str(str(cfg["size"])), price=Price.from_str(str(int(bid))), time_in_force=TimeInForce.GTC, post_only=True)
active_cloids[name + "_bid"] = str(cid_bid)
active_cloids_times[name + "_bid"] = tick
active_cloids_px[name + "_bid"] = bid
except Exception:
pass
if quote_ask:
cid_ask = ClientOrderId(str(UUID4()))
try:
client.submit_order(instrument_id=perp.id, client_order_id=cid_ask, order_side=OrderSide.SELL, order_type=OrderType.LIMIT, quantity=Quantity.from_str(str(cfg["size"])), price=Price.from_str(str(int(ask))), time_in_force=TimeInForce.GTC, post_only=True)
active_cloids[name + "_ask"] = str(cid_ask)
active_cloids_times[name + "_ask"] = tick
active_cloids_px[name + "_ask"] = ask
except Exception:
pass
if tick % 60 == 0 and (quote_bid or quote_ask):
sides = ("BID" if quote_bid else "") + ("|" if quote_bid and quote_ask else "") + ("ASK" if quote_ask else "")
log.info(f"[AS] r={r_price:.1f} σ={selection.get('sigma',0)*100:.2f}% q={as_inv:.6f} {sides}")
except Exception:
# Fallback: best bid/ask both sides
cid_bid = ClientOrderId(str(UUID4()))
cid_ask = ClientOrderId(str(UUID4()))
try:
client.submit_order(instrument_id=perp.id, client_order_id=cid_bid, order_side=OrderSide.BUY, order_type=OrderType.LIMIT, quantity=Quantity.from_str(str(cfg["size"])), price=Price.from_str(str(int(bid))), time_in_force=TimeInForce.GTC, post_only=True)
client.submit_order(instrument_id=perp.id, client_order_id=cid_ask, order_side=OrderSide.SELL, order_type=OrderType.LIMIT, quantity=Quantity.from_str(str(cfg["size"])), price=Price.from_str(str(int(ask))), time_in_force=TimeInForce.GTC, post_only=True)
active_cloids[name] = str(cid_bid)
active_cloids_times[name] = tick
active_cloids_px[name] = bid
except Exception:
pass
continue
# For signal-driven strategies: use aggressive offset
if signal:
side = OrderSide.SELL if "SELL" in str(signal).upper() else OrderSide.BUY
# Aggressive: 0.03% inside the spread for higher fill probability
offset = int(mid * 0.0003)
px_level = ask - offset if side == OrderSide.SELL else bid + offset
px_level = max(px_level, 1)
else:
# No signal/default: skip (don't random-trade)
continue
if px_level <= 0:
continue
cid = ClientOrderId(str(UUID4()))
try:
client.submit_order(instrument_id=perp.id, client_order_id=cid, order_side=side, order_type=OrderType.LIMIT, quantity=Quantity.from_str(str(cfg["size"])), price=Price.from_str(str(int(px_level))), time_in_force=TimeInForce.GTC, post_only=True)
if tick % 60 == 0:
side_str = "BUY" if side == OrderSide.BUY else "SELL"
log.info(f"[{name[:4]:4s}] {side_str} {cfg['size']} @ ${int(px_level):,} ({'best bid ' + str(int(bid)) if side == OrderSide.BUY else 'best ask ' + str(int(ask))})")
active_cloids[name] = str(cid)
active_cloids_times[name] = tick
active_cloids_px[name] = px_level
except Exception as e:
err = str(e)
if "would have immediately matched" in err or "cross" in err.lower():
cid2 = ClientOrderId(str(UUID4()))
try:
client.submit_order(instrument_id=perp.id, client_order_id=cid2, order_side=side, order_type=OrderType.LIMIT, quantity=Quantity.from_str(str(cfg["size"])), price=Price.from_str(str(int(px_level))), time_in_force=TimeInForce.IOC)
active_cloids[name] = str(cid2)
active_cloids_times[name] = tick
active_cloids_px[name] = px_level
except Exception:
pass
# Equity
tp = sum(s["pnl"] for s in STRATEGIES.values())
if tick % 2 == 0:
equity_history.append({"t": time.time(), "v": TOTAL_EQUITY + tp})
if len(equity_history) > 1000:
equity_history[:] = equity_history[-600:]
write_metrics(addr)
if tick % 20 == 0:
tp = sum(s["pnl"] for s in STRATEGIES.values())
tr = sum(s["trades_today"] for s in STRATEGIES.values())
tf = sum(s["fee_paid"] for s in STRATEGIES.values())
log.info(f"Tick {tick:4d} | PnL: ${tp:+.2f} | Trades: {tr:3d} | Fees: ${tf:.4f} | New fills: {new_fills}")
await asyncio.sleep(1)
except Exception as loop_err:
log.error(f"Loop error (tick {tick}): {loop_err}")
await asyncio.sleep(5) # back off and retry
except KeyboardInterrupt:
log.info("Stopping...")
# Cancel all
open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json() or []
for o in open_ords:
try:
iid=InstrumentId.from_str(f"{o['coin']}-USD-PERP.HYPERLIQUID")
client.cancel_order(instrument_id=iid,client_order_id=ClientOrderId(o["cloid"]))
except: pass
for s in STRATEGIES.values(): s["status"]="idle"
write_metrics(addr)
tf=sum(s["fee_paid"] for s in STRATEGIES.values())
tp=sum(s["pnl"] for s in STRATEGIES.values())
log.info(f"Stopped. PnL: ${tp:+.2f}, Fees: ${tf:.4f}")
if __name__=="__main__": asyncio.run(main())