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"""
- 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
@@ -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='| Strategia | Fam | PnL realizz. | Trade | Win% | 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='| Strategia | PnL realizz. | Trade | Win% | 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);