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:
+58
-16
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user