feat(dashboard): area PAPER distinta con equity separata

I 4 multi-asset (TR01/ROT02/TSM01/XS01, solo statistica fuori dal pool reale) ora
in una zona dedicata con la PROPRIA equity (capitale nozionale + PnL cumulato del
book paper) e tabella separata. La tabella/fam_pnl/closed dell'area principale
diventano SOLO reali. Curva paper Chart.js (ocra), cliccabile -> stesso modal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-13 21:25:17 +00:00
parent b6b465878a
commit f6287fcd36
+62 -6
View File
@@ -177,6 +177,7 @@ def build_state() -> dict:
equity = led.get("equity", curve[-1]["equity"] if curve else 0.0)
init_cap = 2000.0
strategies, active, closed = [], [], []
paper_strategies, paper_closed = [], []
open_assets: set[str] = set()
dirs = sorted(list(PAPER.glob("*")) + list(STATS.glob("*")))
@@ -188,14 +189,14 @@ 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)
closed.extend(closes)
(paper_closed if paper_only else closed).extend(closes)
cap = st.get("capital", 0.0)
rc = st.get("real_capital")
# ritirata = status.json non aggiornato da >30min (non piu' tickato dal runner =
# fuori dal config attivo, es. le fade 1h sostituite dal 15m). I paper/stats
# tickano sempre -> mai ritirati.
retired = (not paper_only) and _age_min(sp) > RETIRED_MIN
strategies.append({
(paper_strategies if paper_only else strategies).append({
"id": _short(wid), "wid": wid, "family": _family_of(wid), "code": _code_of(wid),
"capital": round(cap, 2), "real_capital": round(rc, 2) if rc is not None else None,
"realized_pnl": round(pnl, 2), "n_trades": n, "wins": wins,
@@ -240,11 +241,21 @@ def build_state() -> dict:
a["to_tp_pct"] = round(abs(a["tp"] - mark) / mark * 100, 2)
closed.sort(key=lambda c: c["ts"], reverse=True)
# aggregati per famiglia
# aggregati per famiglia (SOLO reali: il paper ha la sua area)
fam_pnl: dict[str, float] = {}
for s in strategies:
fam_pnl[s["family"]] = fam_pnl.get(s["family"], 0.0) + s["realized_pnl"]
# --- area PAPER distinta: equity propria = capitale-base + PnL cumulato del book ---
paper_init = round(sum(s["capital"] - s["realized_pnl"] for s in paper_strategies), 2)
paper_closed.sort(key=lambda c: c["ts"])
pcurve, acc = [], paper_init
for c in paper_closed:
acc += c["pnl"] or 0.0
pcurve.append({"t": c["ts"], "equity": round(acc, 2)})
paper_pnl = round(sum(s["realized_pnl"] for s in paper_strategies), 2)
paper_cap = round(sum(s["capital"] for s in paper_strategies), 2)
return {
"ts": now.isoformat(),
"version": _app_version(),
@@ -263,6 +274,12 @@ def build_state() -> dict:
"active": sorted(active, key=lambda a: a.get("unreal_eur", 0)),
"closed": closed[:50],
"n_active": len(active),
# area PAPER (multi-asset TR01/ROT02/TSM01/XS01: solo statistica, fuori dal pool reale)
"paper": {
"strategies": sorted(paper_strategies, key=lambda s: s["id"]),
"curve": pcurve, "init": paper_init, "equity": paper_cap, "pnl": paper_pnl,
"n": len(paper_strategies),
},
}
@@ -369,7 +386,8 @@ HTML = r"""<!doctype html><html lang="it"><head><meta charset="utf-8">
.ov.on{display:flex}.modal{background:var(--card);border:1px solid var(--bd);border-radius:12px;max-width:820px;width:94%;margin:40px 0;padding:20px}
.modal h3{margin:0 0 4px}.x{float:right;cursor:pointer;color:var(--mut);font-size:20px;line-height:1}
.btn{display:inline-block;background:#1f6feb;color:#fff;padding:7px 12px;border-radius:7px;text-decoration:none;font-size:13px;margin-top:10px}
canvas{width:100%!important;height:220px!important}#mchart{height:240px!important}#eq{height:300px!important}
canvas{width:100%!important;height:220px!important}#mchart{height:240px!important}#eq{height:300px!important}#peq{height:200px!important}
.paperzone{margin-top:22px;border:1px dashed #6e552233;border-radius:12px;padding:14px;background:linear-gradient(180deg,rgba(210,153,34,.05),transparent)}
#eqcard{background:linear-gradient(180deg,#11161d,#0d1117)}
.eqhead{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px}
.eqhead .big{font-size:22px;font-weight:600}.eqhead .pc{font-size:13px}
@@ -380,9 +398,18 @@ 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>
<canvas id="eq"></canvas></div>
<div class="card sec"><h2>PnL per strategia (realizzato, netto fee)</h2><div id="strat"></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>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="paperzone">
<div class="eqhead"><h2 style="margin:0;color:#d29922">📄 Area PAPER — multi-asset (solo statistica, fuori dal pool reale)</h2>
<div><span class="big" id="peqval">—</span> <span class="pc" id="peqpc"></span></div></div>
<div class="sub">TR01 / ROT02 / TSM01 / XS01 girano con capitale nozionale fisso per valutarne l'edge in vista di un'esecuzione reale futura (bloccata dal capitale a €2k). Equity e PnL separati dal portafoglio reale.</div>
<div class="card" style="margin:10px 0"><canvas id="peq"></canvas></div>
<div class="card"><div id="pstrat"></div></div>
</div>
<div class="card sec"><h2>Descrizioni strategie</h2><div id="descs"></div></div>
</div>
<div class="ov" id="ov"><div class="modal">
@@ -478,6 +505,34 @@ async function openModal(wid){const ov=document.getElementById('ov');ov.classLis
}catch(e){document.getElementById('mtitle').textContent='errore: '+e;}}
document.getElementById('mx').onclick=()=>document.getElementById('ov').classList.remove('on');
document.getElementById('ov').onclick=e=>{if(e.target.id=='ov')document.getElementById('ov').classList.remove('on');};
let peqchart=null;
function renderPaper(p){
const v=document.getElementById('peqval'),pc=document.getElementById('peqpc');
const up=p.pnl>=0;v.textContent=''+p.equity.toFixed(2);
pc.textContent=(up?'▲ +':'')+p.pnl.toFixed(2)+'€ realizz.';pc.className='pc '+(up?'grn':'red');
// tabella paper (riuso lo stile della tabella strategie)
const wrap=document.getElementById('pstrat');wrap.innerHTML='';
const max=Math.max(50,...p.strategies.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>';
p.strategies.forEach(s=>{const tr=E('tr');tr.className='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>`;
tr.innerHTML=`<td><span class=id>${s.id}</span> <span class="tag badge-paper">paper</span>${s.in_position?' <span class=tag>•aperta</span>':''}
<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);
// curva equity paper
const c=p.curve||[];const ctx=document.getElementById('peq');
if(!window.Chart||!ctx)return;
const lab=c.map(x=>x.t.slice(5,16).replace('T',' ')),data=c.map(x=>x.equity);
if(peqchart){peqchart.data.labels=lab;peqchart.data.datasets[0].data=data;peqchart.update('none');return;}
peqchart=new Chart(ctx,{type:'line',data:{labels:lab.length?lab:['inizio'],datasets:[{
data:data.length?data:[p.init],borderColor:'#d29922',backgroundColor:'rgba(210,153,34,.12)',
fill:true,pointRadius:c.length<30?3:0,borderWidth:2,tension:.2}]},
options:{plugins:{legend:{display:false},tooltip:{callbacks:{label:x=>''+x.parsed.y.toFixed(2)}}},
scales:{x:{ticks:{color:'#6e7681',maxTicksLimit:7,font:{size:10}},grid:{display:false}},
y:{ticks:{color:'#6e7681',callback:v=>''+v},grid:{color:'rgba(48,54,61,.5)'}}}}});}
function renderDescs(d){const wrap=document.getElementById('descs');if(!wrap)return;wrap.innerHTML='';
const box=E('div');box.className='dl';
Object.keys(d).forEach(k=>{const r=E('div');r.innerHTML=`<b>${k}</b> — ${d[k]}`;box.append(r);});
@@ -508,7 +563,8 @@ 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);renderDescs(s.descriptions);
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);
</script></body></html>"""