From 183209dbd4ac0ece0006e0606a37d6f8b3a67009 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 13 Jun 2026 21:48:56 +0000 Subject: [PATCH] feat(dashboard): grafico equity per famiglia + tabella strategie raggruppata per famiglia - nuovo grafico multi-linea 'Equity per famiglia' (PnL cumulato realizzato di ogni famiglia su asse-tempo comune, colori per famiglia, legenda col totale) - tabella strategie reali raggruppata per famiglia con intestazioni colorate + PnL di famiglia; attive prima delle ritirate dentro ogni gruppo - backend: fam_curves (step-wise cumulato per famiglia dai CLOSE reali), tag fam sui trade chiusi Co-Authored-By: Claude Fable 5 --- src/live/dashboard.py | 74 +++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/src/live/dashboard.py b/src/live/dashboard.py index 1b01367..f24a1a9 100644 --- a/src/live/dashboard.py +++ b/src/live/dashboard.py @@ -189,6 +189,8 @@ def build_state() -> dict: st = json.loads(sp.read_text()) paper_only = (d.parent.name == "portfolio_paper_stats") # multi-asset (no real exec) pnl, n, wins, closes = _worker_trades(wid) + for c in closes: # tag famiglia per le curve aggregate + c["fam"] = _family_of(wid) (paper_closed if paper_only else closed).extend(closes) cap = st.get("capital", 0.0) rc = st.get("real_capital") @@ -240,6 +242,20 @@ def build_state() -> dict: if a["tp"]: a["to_tp_pct"] = round(abs(a["tp"] - mark) / mark * 100, 2) + # curve EQUITY per FAMIGLIA: PnL cumulato di ogni famiglia su asse-tempo comune + # (ad ogni CLOSE di una qualsiasi famiglia, il cumulato di TUTTE avanza step-wise). + real_asc = sorted(closed, key=lambda c: c["ts"]) + fams = sorted({c["fam"] for c in real_asc}) + fcum = {f: 0.0 for f in fams} + flabels: list[str] = [] + fseries: dict[str, list[float]] = {f: [] for f in fams} + for c in real_asc: + fcum[c["fam"]] += c["pnl"] or 0.0 + flabels.append(c["ts"]) + for f in fams: + fseries[f].append(round(fcum[f], 3)) + fam_curves = {"labels": flabels, "series": fseries} + closed.sort(key=lambda c: c["ts"], reverse=True) # aggregati per famiglia (SOLO reali: il paper ha la sua area) fam_pnl: dict[str, float] = {} @@ -268,6 +284,7 @@ def build_state() -> dict: "curve": curve, }, "fam_pnl": {k: round(v, 2) for k, v in sorted(fam_pnl.items())}, + "fam_curves": fam_curves, "descriptions": DESCRIPTIONS, # ritirate in fondo, poi per famiglia/id "strategies": sorted(strategies, key=lambda s: (s["retired"], s["family"], s["id"])), @@ -401,7 +418,9 @@ HTML = r"""

Equity

-

PnL per strategia REALE (realizzato, netto fee)

+

Equity per famiglia (PnL cumulato realizzato)

+
+

Strategie REALI per famiglia (realizzato, netto fee)

Trade attivi — stato in tempo reale

Trade chiusi (ultimi 50)

@@ -461,21 +480,44 @@ function renderChart(curve,init){const ctx=document.getElementById('eq'); scales:{x:{ticks:{color:'#6e7681',maxTicksLimit:8,font:{size:10}},grid:{display:false}}, y:{ticks:{color:'#6e7681',callback:v=>'€'+v},grid:{color:'rgba(48,54,61,.5)'}, suggestedMin:Math.min(init,...data)-2,suggestedMax:Math.max(init,...data)+2}}}});} -function renderStrat(strats,fam){const wrap=document.getElementById('strat');wrap.innerHTML=''; +const FAMCOL={FADE:'#3fb950',PAIRS:'#58a6ff',SHAPE:'#bc8cff',HONEST:'#d29922',TSM:'#39c5cf',XSEC:'#f778ba'}; +let famchart=null; +function renderFamChart(fc,fpnl){const ctx=document.getElementById('famchart');if(!ctx||!window.Chart)return; + const labels=(fc.labels||[]).map(t=>t.slice(5,16).replace('T',' ')); + const ds=Object.keys(fc.series||{}).map(f=>({label:f+' ('+eur(fpnl[f]||0)+')',data:fc.series[f], + borderColor:FAMCOL[f]||'#8b949e',backgroundColor:'transparent',pointRadius:0,pointHoverRadius:4,borderWidth:2,tension:.15})); + if(famchart){famchart.data.labels=labels;famchart.data.datasets=ds;famchart.update('none');return;} + famchart=new Chart(ctx,{type:'line',data:{labels,datasets:ds}, + options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false}, + plugins:{legend:{labels:{color:'#c9d1d9',boxWidth:12,font:{size:11}}}, + tooltip:{callbacks:{label:c=>c.dataset.label.split(' ')[0]+': '+eur(c.parsed.y)}}}, + scales:{x:{ticks:{color:'#6e7681',maxTicksLimit:8,font:{size:10}},grid:{display:false}}, + y:{ticks:{color:'#6e7681',callback:v=>eur(v)},grid:{color:'rgba(48,54,61,.5)'}}}}});} +function stratRow(s,max){const tr=E('tr');tr.className=(s.retired?'retired-row':(s.paper?'muted-row':''))+' clk'; + tr.onclick=()=>openModal(s.wid); + const w=Math.abs(s.realized_pnl)/max*120; + const bar=``; + const badges=(s.retired?' RITIRATA→15m':'') + +(s.in_position?' •aperta':''); + tr.innerHTML=`${s.id}${badges} +
${s.desc||''}
🏷 ${s.version||''}
+ ${eur(s.realized_pnl)} + ${s.n_trades}${s.win_rate.toFixed(0)}€${s.capital.toFixed(0)}${bar}`; + return tr;} +function renderStrat(strats,fpnl){const wrap=document.getElementById('strat');wrap.innerHTML=''; const max=Math.max(100,...strats.map(s=>Math.abs(s.realized_pnl))); - const t=E('table');t.innerHTML='StrategiaFamPnL realizz.TradeWin%Capitale'; - strats.forEach(s=>{const tr=E('tr');tr.className=s.retired?'retired-row':(s.paper?'muted-row':''); - const w=Math.abs(s.realized_pnl)/max*120; - const bar=``; - const badges=(s.retired?' RITIRATA→15m':'') - +(s.paper?' paper':'') - +(s.in_position?' •aperta':''); - tr.className+=' clk';tr.onclick=()=>openModal(s.wid); - tr.innerHTML=`${s.id}${badges} -
${s.desc||''}
🏷 ${s.version||''}
- ${s.family}${eur(s.realized_pnl)} - ${s.n_trades}${s.win_rate.toFixed(0)}€${s.capital.toFixed(0)}${bar}`; - t.append(tr);});wrap.append(t);} + // raggruppa per famiglia, attive prima delle ritirate + const byfam={};strats.forEach(s=>{(byfam[s.family]=byfam[s.family]||[]).push(s);}); + Object.keys(byfam).sort().forEach(fam=>{ + const head=E('div');head.style.cssText='display:flex;justify-content:space-between;align-items:center;margin:14px 0 4px'; + const fp=fpnl[fam]||0; + head.innerHTML=`▌${fam}` + +`${eur(fp)}`; + wrap.append(head); + const t=E('table');t.innerHTML='StrategiaPnL realizz.TradeWin%Capitale'; + byfam[fam].sort((a,b)=>(a.retired-b.retired)||a.id.localeCompare(b.id)) + .forEach(s=>t.append(stratRow(s,max))); + wrap.append(t);});} let mchart=null; async function openModal(wid){const ov=document.getElementById('ov');ov.classList.add('on'); document.getElementById('mtitle').textContent='caricamento…'; @@ -566,7 +608,7 @@ async function tick(){try{const r=await fetch('/api/state');const s=await r.json document.getElementById('ver').textContent='v'+(s.version||'?'); document.getElementById('nact').textContent=s.n_active; renderKpis(s.portfolio);renderChart(s.portfolio.curve,s.portfolio.init); - renderStrat(s.strategies,s.fam_pnl);renderActive(s.active);renderClosed(s.closed); + renderFamChart(s.fam_curves,s.fam_pnl);renderStrat(s.strategies,s.fam_pnl);renderActive(s.active);renderClosed(s.closed); if(s.paper)renderPaper(s.paper);renderDescs(s.descriptions); }catch(e){document.getElementById('ts').textContent='offline';}} tick();setInterval(tick,5000);