Fix dashboard backtest detail, deterministic backtest seeds, paper trader fees, live node crash guard

Backtest detail: openDetail() now fetches full backtest JSON from the API
instead of showing "Full trade data not in summary". Renders equity curve
chart + full trade history table with 100 rows.

Backtest reproducibility: replaced hash(key) with fixed per-strategy seeds.
Python's hash() is randomized per process (PYTHONHASHSEED), causing wildly
different results for same strategy across runs. Now deterministic.

Server: added total_trades and sortino to /api/backtests summary response.

Paper trader: fixed Avellaneda-Stoikov simulate using TAKER_FEE instead of
MAKER_FEE. Lowered OBI signal threshold from 5bps to 1.5bps for flat markets.

Live node: added None-guard in get_mark_prices — Hyperliquid testnet API
sometimes returns null, crashing the node. Wrapped in try/except.
This commit is contained in:
ramseshk
2026-08-04 07:07:15 +00:00
parent 1c83fd378e
commit e4de21192a
17 changed files with 43315 additions and 16 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
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
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
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
+5 -1
View File
@@ -27,7 +27,11 @@ CONFIGS = {
}
def simulate(key, periods=720):
random.seed(hash(key)%2**32)
# Deterministic seed per strategy (hash() is randomized per Python process)
_fixed_seeds = {"ofi":42,"iceberg":43,"funding_arb":44,"pairs":45,"avellaneda":46,
"momentum":47,"mean_rev":48,"hawkes":49,"deep_lob":50,
"cartea":51,"queue_imb":52,"gueant":53}
random.seed(_fixed_seeds.get(key, 42))
cfg = CONFIGS[key]
hr = cfg["daily_ret"]/24; hv = cfg["daily_vol"]/(24**0.5)
eq=100.0; curve=[]; rets=[]; trades=[]
+2
View File
@@ -174,9 +174,11 @@ async def list_backtests():
"start": data.get("start_time"),
"end": data.get("end_time"),
"sharpe": data.get("sharpe", 0),
"sortino": data.get("sortino", 0),
"pnl_pct": data.get("pnl_pct", 0),
"max_dd": data.get("max_dd", 0),
"win_rate": data.get("win_rate", 0),
"total_trades": data.get("total_trades", 0),
})
except (json.JSONDecodeError, IOError):
pass
+32 -6
View File
@@ -210,15 +210,34 @@ function openDetail(name,tab){
ss=lastData.strategies||{};
} else if(tab==='backtest'&&lastBT&&lastBT[name]){
var b=lastBT[name];
document.getElementById('det-desc').textContent=(b.strategy||'')+' — 30-day backtest, Sharpe '+(b.sharpe||0).toFixed(2)+', max DD '+(b.max_dd*100).toFixed(1)+'%.';
document.getElementById('det-desc').textContent=(b.strategy||'')+' — 30-day backtest, Sharpe '+(b.sharpe||0).toFixed(2)+', max DD '+(b.max_dd*100).toFixed(1)+'%. '+((b.total_trades||0)+' trades').replace('0 trades','');
document.getElementById('det-stats').innerHTML='<div class="stat"><div class="lbl">PnL</div><div class="val '+(b.pnl_pct>=0?'up':'dn')+'">'+(b.pnl_pct>=0?'+':'')+(b.pnl_pct||0).toFixed(2)+'%</div></div>'+
'<div class="stat"><div class="lbl">Sharpe</div><div class="val">'+(b.sharpe||0).toFixed(2)+'</div></div>'+
'<div class="stat"><div class="lbl">Sortino</div><div class="val">'+(b.sortino||0).toFixed(2)+'</div></div>'+
'<div class="stat"><div class="lbl">Max DD</div><div class="val dn">'+(b.max_dd*100).toFixed(2)+'%</div></div>'+
'<div class="stat"><div class="lbl">Win Rate</div><div class="val">'+Math.round((b.win_rate||0)*100)+'%</div></div>'+
'<div class="stat"><div class="lbl">From</div><div class="val" style="font-size:9px">'+(b.start||'').substr(0,10)+'</div></div>'+
'<div class="stat"><div class="lbl">To</div><div class="val" style="font-size:9px">'+(b.end||'').substr(0,10)+'</div></div>';
'<div class="stat"><div class="lbl">Trades</div><div class="val">'+(b.total_trades||0)+'</div></div>';
if(detSer)detSer.setData([]);
document.getElementById('det-trades').innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--tx);padding:20px">Full trade data not in summary — run with --strategy to regenerate</td></tr>';
document.getElementById('det-trades').innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--tx);padding:20px">Loading full trade data&hellip;</td></tr>';
// Fetch full backtest data from API
fetch('/cv/api/backtest/'+encodeURIComponent(b.name)).then(function(r){return r.json()}).then(function(full){
document.getElementById('det-desc').textContent=(full.strategy||b.strategy)+' — '+full.num_periods+' periods, '+full.total_trades+' trades, ▲ $'+(full.pnl||0).toFixed(2);
// Equity chart
if(!detChart)initDetChart();
var pts=[],curve=full.equity_curve||[];
for(var i=0;i<curve.length;i++){if(curve[i]&&curve[i].t)pts.push({time:curve[i].t,value:curve[i].v})}
if(pts.length>0){detSer.setData(pts);detChart.timeScale().fitContent();setTimeout(function(){detChart.timeScale().fitContent()},200)}
// Trades table
var trows='',tlist=full.trades||[];
for(var j=Math.max(0,tlist.length-100);j<tlist.length;j++){
var t=tlist[j],tp=t.pnl||0,tside=(t.side||'').toUpperCase();
trows+='<tr><td>'+(t.time||'').substr(0,16)+'</td><td class="'+(tside.indexOf('BUY')>=0?'green':'red')+'">'+tside+'</td><td>'+t.size+'</td><td>$'+(t.price||0).toFixed(1)+'</td><td class="'+(tp>=0?'green':'red')+'">'+(tp>=0?'+':'')+'$'+Math.abs(tp).toFixed(4)+'</td><td class="red"></td><td class="reason"></td></tr>';
}
document.getElementById('det-trades').innerHTML=trows||'<tr><td colspan="7" style="text-align:center;color:var(--tx);padding:20px">No trades recorded</td></tr>';
setTimeout(function(){if(detChart)detChart.applyOptions({width:document.getElementById('det-chart').offsetWidth,height:280})},300);
}).catch(function(e){
document.getElementById('det-trades').innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--rd);padding:20px">Failed to load trade data: '+e.message+'</td></tr>';
});
return;
}
var s=ss?ss[name]:null;
@@ -276,10 +295,17 @@ function renLive(d){if(!d)return;var p=d.total_pnl||0;document.getElementById('s
function renPaper(d){if(!d)return;var p=d.total_pnl||0;document.getElementById('stpnl').textContent=(p>=0?'+':'')+'$'+Math.abs(p).toFixed(2);document.getElementById('stpnl').className='pnl '+(p>=0?'up':'dn');document.getElementById('stpct').textContent='Paper · '+d.total_equity+' · Regime: '+(d.regime||'—');renCards('paper-sgrid',d.strategies||{},d.base_equity||100000,'paper','paper-stats')}
// ═══════════ Backtests ═══════════
var lastBT={}, lastBTList=[];
function loadBT(){
fetch('/cv/api/backtests').then(function(r){return r.json()}).then(function(data){
var h='';lastBT={};for(var i=0;i<data.length;i++){var b=data[i];var pnl=b.pnl_pct||0;lastBT[b.strategy]=b;h+='<div class="scard" onclick="openDetail(\''+b.strategy+'\',\'backtest\')\"><div class="sh"><div><div class="sname">'+b.strategy+'</div><div class="salloc">30-day &middot; $100</div></div><span class="stag run">BACKTEST</span></div><div class="spnl '+(pnl>=0?'up':'dn')+'">'+(pnl>=0?'+':'')+pnl.toFixed(2)+'%</div><div class="smeta"><span>Sharpe: <b>'+b.sharpe.toFixed(2)+'</b></span><span>DD: <b class="red">'+(b.max_dd*100).toFixed(2)+'%</b></span><span>Win: <b>'+Math.round(b.win_rate*100)+'%</b></span></div></div>';}
document.getElementById('bt-sgrid').innerHTML=h||'<div style="padding:20px;color:var(--tx)">No backtests.</div>';
lastBTList=data; lastBT={};
// Keep latest backtest per strategy (sorted by time desc — first wins)
for(var i=0;i<data.length;i++){var b=data[i];if(!lastBT[b.strategy])lastBT[b.strategy]=b;}
var h='';
for(var s in lastBT){var b=lastBT[s];var pnl=b.pnl_pct||0;
h+='<div class=\"scard\" onclick=\"openDetail(\''+s+'\',\'backtest\')\"><div class=\"sh\"><div><div class=\"sname\">'+s+'</div><div class=\"salloc\">30-day &middot; $100</div></div><span class=\"stag run\">BACKTEST</span></div><div class=\"spnl '+(pnl>=0?'up':'dn')+'\">'+(pnl>=0?'+':'')+pnl.toFixed(2)+'%</div><div class=\"smeta\"><span>Sharpe: <b>'+b.sharpe.toFixed(2)+'</b></span><span>DD: <b class=\"red\">'+(b.max_dd*100).toFixed(2)+'%</b></span><span>Win: <b>'+Math.round(b.win_rate*100)+'%</b></span></div></div>';
}
document.getElementById('bt-sgrid').innerHTML=h||'<div style=\"padding:20px;color:var(--tx)\">No backtests.</div>';
})
}
+12 -5
View File
@@ -64,11 +64,18 @@ def get_fills(addr):
return r.json() if r.status_code==200 else []
def get_mark_prices():
r = requests.post(TESTNET_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
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."""
+4 -4
View File
@@ -226,12 +226,12 @@ def compute_signals():
if len(btc_prices) < 20: return
btc = btc_prices[-1]; eth = eth_prices[-1] if eth_prices else btc/34
# OFI
# Order Book Imbalance — 5-tick price momentum (1.5 bps threshold for flat markets)
if len(btc_prices) >= 5:
ret = (btc - btc_prices[-5]) / btc_prices[-5]
if ret > 0.0005:
if ret > 0.00015:
STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"SELL","strength":ret})
elif ret < -0.0005:
elif ret < -0.00015:
STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(ret)})
# Iceberg
@@ -406,7 +406,7 @@ def simulate_avellaneda(btc_bid, btc_ask):
side = "BUY" if cfg["position"] <= 0 else "SELL"
sz = cfg["size"]
notional = sz * bid_fill_price
fee = notional * TAKER_FEE
fee = notional * MAKER_FEE # A-S is a MAKER strategy — pay maker fee, not taker
spread_profit = sz * (btc_ask - btc_bid)/2 if side == "BUY" else 0
if side == "BUY":