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:
+52
-10
@@ -189,6 +189,8 @@ def build_state() -> dict:
|
|||||||
st = json.loads(sp.read_text())
|
st = json.loads(sp.read_text())
|
||||||
paper_only = (d.parent.name == "portfolio_paper_stats") # multi-asset (no real exec)
|
paper_only = (d.parent.name == "portfolio_paper_stats") # multi-asset (no real exec)
|
||||||
pnl, n, wins, closes = _worker_trades(wid)
|
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)
|
(paper_closed if paper_only else closed).extend(closes)
|
||||||
cap = st.get("capital", 0.0)
|
cap = st.get("capital", 0.0)
|
||||||
rc = st.get("real_capital")
|
rc = st.get("real_capital")
|
||||||
@@ -240,6 +242,20 @@ def build_state() -> dict:
|
|||||||
if a["tp"]:
|
if a["tp"]:
|
||||||
a["to_tp_pct"] = round(abs(a["tp"] - mark) / mark * 100, 2)
|
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)
|
closed.sort(key=lambda c: c["ts"], reverse=True)
|
||||||
# aggregati per famiglia (SOLO reali: il paper ha la sua area)
|
# aggregati per famiglia (SOLO reali: il paper ha la sua area)
|
||||||
fam_pnl: dict[str, float] = {}
|
fam_pnl: dict[str, float] = {}
|
||||||
@@ -268,6 +284,7 @@ def build_state() -> dict:
|
|||||||
"curve": curve,
|
"curve": curve,
|
||||||
},
|
},
|
||||||
"fam_pnl": {k: round(v, 2) for k, v in sorted(fam_pnl.items())},
|
"fam_pnl": {k: round(v, 2) for k, v in sorted(fam_pnl.items())},
|
||||||
|
"fam_curves": fam_curves,
|
||||||
"descriptions": DESCRIPTIONS,
|
"descriptions": DESCRIPTIONS,
|
||||||
# ritirate in fondo, poi per famiglia/id
|
# ritirate in fondo, poi per famiglia/id
|
||||||
"strategies": sorted(strategies, key=lambda s: (s["retired"], s["family"], s["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 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><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="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 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>
|
<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}},
|
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)'},
|
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}}}});}
|
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'};
|
||||||
const max=Math.max(100,...strats.map(s=>Math.abs(s.realized_pnl)));
|
let famchart=null;
|
||||||
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>';
|
function renderFamChart(fc,fpnl){const ctx=document.getElementById('famchart');if(!ctx||!window.Chart)return;
|
||||||
strats.forEach(s=>{const tr=E('tr');tr.className=s.retired?'retired-row':(s.paper?'muted-row':'');
|
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 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 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>':'')
|
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>':'');
|
+(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}
|
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>
|
<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 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>`;
|
<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);}
|
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)));
|
||||||
|
// 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;
|
let mchart=null;
|
||||||
async function openModal(wid){const ov=document.getElementById('ov');ov.classList.add('on');
|
async function openModal(wid){const ov=document.getElementById('ov');ov.classList.add('on');
|
||||||
document.getElementById('mtitle').textContent='caricamento…';
|
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('ver').textContent='v'+(s.version||'?');
|
||||||
document.getElementById('nact').textContent=s.n_active;
|
document.getElementById('nact').textContent=s.n_active;
|
||||||
renderKpis(s.portfolio);renderChart(s.portfolio.curve,s.portfolio.init);
|
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);
|
if(s.paper)renderPaper(s.paper);renderDescs(s.descriptions);
|
||||||
}catch(e){document.getElementById('ts').textContent='offline';}}
|
}catch(e){document.getElementById('ts').textContent='offline';}}
|
||||||
tick();setInterval(tick,5000);
|
tick();setInterval(tick,5000);
|
||||||
|
|||||||
Reference in New Issue
Block a user