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 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-13 21:48:56 +00:00
parent e311d00fe4
commit 183209dbd4
+58 -16
View File
@@ -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"""<!doctype html><html lang="it"><head><meta charset="utf-8">
<div class="card sec" id="eqcard"><div class="eqhead"><h2 style="margin:0">Equity</h2>
<div><span class="big" id="eqval">—</span> <span class="pc" id="eqpc"></span></div></div>
<div class="chartbox eq"><canvas id="eq"></canvas></div></div>
<div class="card sec"><h2>PnL per strategia REALE (realizzato, netto fee)</h2><div id="strat"></div></div>
<div class="card sec"><h2>Equity per famiglia <span class="mut" style="font-size:12px">(PnL cumulato realizzato)</span></h2>
<div class="chartbox" style="height:240px"><canvas id="famchart"></canvas></div></div>
<div class="card sec"><h2>Strategie REALI per famiglia <span class="mut" style="font-size:12px">(realizzato, netto fee)</span></h2><div id="strat"></div></div>
<div class="card sec"><h2>Trade attivi — stato in tempo reale</h2><div id="active"></div></div>
<div class="card sec"><h2>Trade chiusi (ultimi 50)</h2><div id="closed"></div></div>
@@ -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=`<span class="bar" style="width:${w}px;background:${s.realized_pnl>=0?'#3fb950':'#f85149'}"></span>`;
const badges=(s.retired?' <span class="tag badge-ret">RITIRATA→15m</span>':'')
+(s.in_position?' <span class=tag>•aperta</span>':'');
tr.innerHTML=`<td><span class=id title="apri scheda">${s.id}</span>${badges}
<div class=desc>${s.desc||''}</div><div class=ver>🏷 ${s.version||''}</div></td>
<td class="${cls(s.realized_pnl)}">${eur(s.realized_pnl)}</td>
<td>${s.n_trades}</td><td class=mut>${s.win_rate.toFixed(0)}</td><td class=mut>€${s.capital.toFixed(0)}</td><td>${bar}</td>`;
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='<tr><th>Strategia</th><th>Fam</th><th>PnL realizz.</th><th>Trade</th><th>Win%</th><th>Capitale</th><th></th></tr>';
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=`<span class="bar" style="width:${w}px;background:${s.realized_pnl>=0?'#3fb950':'#f85149'}"></span>`;
const badges=(s.retired?' <span class="tag badge-ret">RITIRATA→15m</span>':'')
+(s.paper?' <span class="tag badge-paper">paper</span>':'')
+(s.in_position?' <span class=tag>•aperta</span>':'');
tr.className+=' clk';tr.onclick=()=>openModal(s.wid);
tr.innerHTML=`<td><span class=id title="apri scheda">${s.id}</span>${badges}
<div class=desc>${s.desc||''}</div><div class=ver>🏷 ${s.version||''}</div></td>
<td class=mut>${s.family}</td><td class="${cls(s.realized_pnl)}">${eur(s.realized_pnl)}</td>
<td>${s.n_trades}</td><td class=mut>${s.win_rate.toFixed(0)}</td><td class=mut>€${s.capital.toFixed(0)}</td><td>${bar}</td>`;
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=`<span style="font-weight:600;color:${FAMCOL[fam]||'#c9d1d9'}">▌${fam}</span>`
+`<span class="${cls(fp)}">${eur(fp)}</span>`;
wrap.append(head);
const t=E('table');t.innerHTML='<tr><th>Strategia</th><th>PnL realizz.</th><th>Trade</th><th>Win%</th><th>Capitale</th><th></th></tr>';
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);