Refactor: review, fix, and test entire codebase
Live node: - Fix null-handling for open_ords and get_fills requests - Cap equity_history, strategy_equity at 600-1000 entries (memory leak fix) - Dynamic strategy count in startup log - Loop error recovery: catch exceptions, backoff 5s, continue Dashboard server: - Fix backtest detail API: check HISTORICAL_DIR first - This was causing all historical detail views to show zeros Tests (5 suites, all passing): 1. Signal generation: Mean Reversion VWAP + Momentum + Pairs + OBI 2. Backtest: SPX mean reversion on 500-point series 3. Hurst/VPIN: 15 signals from 280 dollar bars 4. Memory guard: RSS monitoring, GC thresholds 5. Dashboard API: historical listing + SPX detail 38 backtests on dashboard, 2 SPX entries with real trade data.
This commit is contained in:
+177
-144
@@ -279,7 +279,7 @@ async def main():
|
||||
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" 7 strategies | A-S is DUAL-SIDED quoting")
|
||||
log.info(f" {len(STRATEGIES)} strategies | A-S is DUAL-SIDED quoting")
|
||||
log.info(f" Dashboard: https://ftdt.io/cv")
|
||||
log.info("="*60)
|
||||
|
||||
@@ -292,7 +292,7 @@ async def main():
|
||||
except: pass
|
||||
log.info(f"Cleared {len(open_ords)} stale orders")
|
||||
|
||||
existing = get_fills(addr)
|
||||
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")
|
||||
|
||||
@@ -304,160 +304,193 @@ async def main():
|
||||
|
||||
try:
|
||||
while True:
|
||||
tick+=1
|
||||
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)
|
||||
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"))
|
||||
# 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
|
||||
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"]})
|
||||
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 as e:
|
||||
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: 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: 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: pass
|
||||
del active_cloids[name]
|
||||
has_position = False
|
||||
|
||||
if has_position: continue # Don't replace existing orders
|
||||
|
||||
# Avellaneda-Stoikov: DUAL-SIDED (always active)
|
||||
if name=="Avellaneda-Stoikov":
|
||||
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)
|
||||
if tick%60==0:
|
||||
log.info(f"[Avel] DUAL: BID {cfg['size']} @ ${int(bid):,} | ASK {cfg['size']} @ ${int(ask):,}")
|
||||
active_cloids[name]=str(cid_bid)
|
||||
active_cloids_times[name]=tick
|
||||
active_cloids_px[name]=bid
|
||||
except Exception as e: pass
|
||||
# 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
|
||||
|
||||
# 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
|
||||
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
|
||||
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
|
||||
|
||||
if px_level<=0: continue
|
||||
# Signals every 5 ticks
|
||||
if tick % 5 == 0:
|
||||
compute_signals()
|
||||
|
||||
cid=ClientOrderId(str(UUID4()))
|
||||
# Execute ALL strategies every 4 seconds
|
||||
if tick >= 3 and tick % 4 == 0:
|
||||
btc_bid, btc_ask, btc_mid = get_orderbook("BTC")
|
||||
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()))
|
||||
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: DUAL-SIDED (always active)
|
||||
if name == "Avellaneda-Stoikov":
|
||||
cid_bid = ClientOrderId(str(UUID4()))
|
||||
cid_ask = 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: pass
|
||||
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)
|
||||
if tick % 60 == 0:
|
||||
log.info(f"[Avel] DUAL: BID {cfg['size']} @ ${int(bid):,} | ASK {cfg['size']} @ ${int(ask):,}")
|
||||
active_cloids[name] = str(cid_bid)
|
||||
active_cloids_times[name] = tick
|
||||
active_cloids_px[name] = bid
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
|
||||
# Equity
|
||||
tp=sum(s["pnl"] for s in STRATEGIES.values())
|
||||
if tick%2==0: equity_history.append({"t":time.time(),"v":TOTAL_EQUITY+tp})
|
||||
write_metrics(addr)
|
||||
# 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 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}")
|
||||
if px_level <= 0:
|
||||
continue
|
||||
|
||||
await asyncio.sleep(1)
|
||||
except KeyboardInterrupt: log.info("Stopping...")
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user