feat(dashboard): badge strategie RITIRATE (staleness) + descrizioni per strategia

Le fade 1h sostituite dallo swap 15m sono marcate RITIRATA→15m (rilevate per
status.json stantio >30min, ordinate in fondo, riga barrata). Descrizioni concise
per ogni strategia (da strategie_attive.html) come tooltip + sezione legenda.
Fix rilevazione paper: per directory (portfolio_paper_stats), non per chiave
'weights' (TR01/TSM01/XS01 non ce l'hanno) -> ora i 4 multi-asset sono paper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-13 21:12:42 +00:00
parent 31f08ddf32
commit 22af19c7f9
+63 -6
View File
@@ -30,6 +30,42 @@ LEDGER = PROJECT_ROOT / "data" / "portfolios" / "PORT06"
FAMILY = [("MR01", "FADE"), ("MR02", "FADE"), ("MR07", "FADE"), ("DIP01", "HONEST"), FAMILY = [("MR01", "FADE"), ("MR02", "FADE"), ("MR07", "FADE"), ("DIP01", "HONEST"),
("PR01", "PAIRS"), ("SH01", "SHAPE"), ("TR01", "HONEST"), ("PR01", "PAIRS"), ("SH01", "SHAPE"), ("TR01", "HONEST"),
("ROT02", "HONEST"), ("TSM01", "TSM"), ("XS01", "XSEC")] ("ROT02", "HONEST"), ("TSM01", "TSM"), ("XS01", "XSEC")]
RETIRED_MIN = 30 # status.json non aggiornato da >30min = worker non piu' nel
# config attivo (es. le fade 1h sostituite dal 15m allo swap)
# Descrizioni concise (fonte: docs/report/strategie_attive.html / make_strategy_doc.py)
DESCRIPTIONS = {
"MR01": "Bollinger Fade — quando il close esce dalla banda ±2.5σ (SMA50) entra CONTRO il "
"movimento (short sopra/long sotto); TP alla media, SL 2·ATR, max 24 barre. Mean-reversion.",
"MR02": "Donchian Fade — fada la rottura degli estremi del canale a 20 barre (max/min recenti); "
"TP al centro del canale. Stessa tesi di MR01 con trigger diverso.",
"MR07": "Return Reversal — fada il rendimento di barra estremo (z-score ±3.5); exit in multipli "
"di ATR. La fade più selettiva (esposizione ~8% del tempo).",
"DIP01": "Dip Buy — compra il dip quando lo z del prezzo incrocia sotto 2.5; TP alla SMA50, "
"EXIT-16 sul SL, max 24 barre. Unico sleeve BTC con round-trip reali su testnet.",
"PR01": "Pairs Reversion — market-neutral: quando |z| del log-ratio fra due asset ≥2 compra la "
"gamba debole/shorta la forte, chiude a |z|≤0.75 o 72 barre. Scorrelato dal mercato (~0.05). "
"Anche ETH/BTC a 15m (flat-skip). Fee su 2 gambe, senza stop → size ridotta.",
"SH01": "Shape-ML — una LogisticRegression legge 17 feature di forma in walk-forward e predice il "
"segno del rendimento a 12 barre. Win-rate ~50%: l'edge è nell'asimmetria. Diversificatore, no stop.",
"TR01": "Basket Trend (4h) — long quando EMA20>EMA100 su paniere equal-weight di 5 asset, flat "
"altrimenti. Trend-following difensivo che cattura i trend lunghi che le fade non prendono.",
"ROT02": "Dual Momentum (1d) — ogni giorno tiene le top-3 per momentum 60g (se positive), gross 0.45, "
"tutto cash se BTC<SMA100. Rotazione multi-crypto.",
"TSM01": "TSMOM (1d) — long sugli asset con consenso pieno di momentum su 3/6/12 mesi, gross 0.30, "
"risk-off se BTC<SMA100. Diversificatore, non motore di ritorno.",
"XS01": "Cross-Sectional Reversion — ogni 12h classifica 8 crypto per momentum e va long i perdenti "
"relativi / short i vincenti (market-neutral), con dispersion-gate. 3 sub-book sfasati.",
}
def _code_of(wid: str) -> str:
for code, _ in FAMILY:
if wid.startswith(code):
return code
return "?"
_MARK_CACHE: dict = {"ts": 0.0, "marks": {}} _MARK_CACHE: dict = {"ts": 0.0, "marks": {}}
@@ -127,17 +163,23 @@ def build_state() -> dict:
continue continue
wid = d.name wid = d.name
st = json.loads(sp.read_text()) st = json.loads(sp.read_text())
paper_only = ("weights" in st) # multi-asset stats (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)
closed.extend(closes) 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")
# 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({ strategies.append({
"id": _short(wid), "family": _family_of(wid), "id": _short(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, "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, "realized_pnl": round(pnl, 2), "n_trades": n, "wins": wins,
"win_rate": round(wins / n * 100, 0) if n else 0.0, "win_rate": round(wins / n * 100, 0) if n else 0.0,
"in_position": bool(st.get("in_position")), "paper": paper_only, "in_position": bool(st.get("in_position")), "paper": paper_only,
"retired": retired, "tf": wid.split("__")[-1],
"desc": DESCRIPTIONS.get(_code_of(wid), ""),
}) })
if st.get("in_position") and not paper_only: if st.get("in_position") and not paper_only:
is_pair = "entry_a" in st is_pair = "entry_a" in st
@@ -190,7 +232,9 @@ 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())},
"strategies": sorted(strategies, key=lambda s: (s["family"], s["id"])), "descriptions": DESCRIPTIONS,
# ritirate in fondo, poi per famiglia/id
"strategies": sorted(strategies, key=lambda s: (s["retired"], s["family"], s["id"])),
"active": sorted(active, key=lambda a: a.get("unreal_eur", 0)), "active": sorted(active, key=lambda a: a.get("unreal_eur", 0)),
"closed": closed[:50], "closed": closed[:50],
"n_active": len(active), "n_active": len(active),
@@ -241,6 +285,10 @@ HTML = r"""<!doctype html><html lang="it"><head><meta charset="utf-8">
.dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:5px} .dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:5px}
.pill{font-size:11px;padding:1px 7px;border-radius:10px}.long{background:#0f3d2e;color:var(--grn)}.short{background:#3d1418;color:var(--red)} .pill{font-size:11px;padding:1px 7px;border-radius:10px}.long{background:#0f3d2e;color:var(--grn)}.short{background:#3d1418;color:var(--red)}
.stale{color:#d29922}.muted-row td{color:var(--mut)} .stale{color:#d29922}.muted-row td{color:var(--mut)}
.retired-row td{color:#6e7681;opacity:.7}.retired-row .id{text-decoration:line-through}
.badge-ret{background:#3d1418;color:#f85149}.badge-paper{background:#21262d;color:var(--mut)}
.desc{font-size:11px;color:var(--mut);margin-top:2px;max-width:520px}
.dl b{color:var(--acc)}.dl div{padding:5px 0;border-bottom:1px solid var(--bd)}
canvas{width:100%!important;height:220px!important} canvas{width:100%!important;height:220px!important}
</style></head><body><div class="wrap"> </style></head><body><div class="wrap">
<h1>PORT06 — Dashboard live <span id="ver" class="tag"></span></h1> <h1>PORT06 — Dashboard live <span id="ver" class="tag"></span></h1>
@@ -250,6 +298,7 @@ HTML = r"""<!doctype html><html lang="it"><head><meta charset="utf-8">
<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 (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 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>
<div class="card sec"><h2>Descrizioni strategie</h2><div id="descs"></div></div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<script> <script>
@@ -274,13 +323,21 @@ function renderChart(curve){const ctx=document.getElementById('eq');
function renderStrat(strats,fam){const wrap=document.getElementById('strat');wrap.innerHTML=''; function renderStrat(strats,fam){const wrap=document.getElementById('strat');wrap.innerHTML='';
const max=Math.max(100,...strats.map(s=>Math.abs(s.realized_pnl))); 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>'; 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');if(s.paper)tr.className='muted-row'; 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 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>`;
tr.innerHTML=`<td>${s.id}${s.paper?'<span class=tag>paper</span>':''}${s.in_position?' <span class=tag>•aperta</span>':''}</td> 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.innerHTML=`<td><span class=id title="${(s.desc||'').replace(/"/g,'&quot;')}">${s.id}</span>${badges}
<div class=desc>${s.desc||''}</div></td>
<td class=mut>${s.family}</td><td class="${cls(s.realized_pnl)}">${eur(s.realized_pnl)}</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>`; <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);} t.append(tr);});wrap.append(t);}
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);});
wrap.append(box);}
function renderActive(a){const wrap=document.getElementById('active');wrap.innerHTML=''; function renderActive(a){const wrap=document.getElementById('active');wrap.innerHTML='';
if(!a.length){wrap.innerHTML='<div class=mut>nessuna posizione aperta</div>';return;} if(!a.length){wrap.innerHTML='<div class=mut>nessuna posizione aperta</div>';return;}
const t=E('table');t.innerHTML='<tr><th>Strategia</th><th>Lato</th><th>Entry</th><th>Mark</th><th>PnL non realizz.</th><th>Barre</th><th>→TP%</th><th>Età</th></tr>'; const t=E('table');t.innerHTML='<tr><th>Strategia</th><th>Lato</th><th>Entry</th><th>Mark</th><th>PnL non realizz.</th><th>Barre</th><th>→TP%</th><th>Età</th></tr>';
@@ -306,7 +363,7 @@ async function tick(){try{const r=await fetch('/api/state');const s=await r.json
document.getElementById('ts').textContent=s.ts.slice(11,19); document.getElementById('ts').textContent=s.ts.slice(11,19);
document.getElementById('nact').textContent=s.n_active; document.getElementById('nact').textContent=s.n_active;
renderKpis(s.portfolio);renderChart(s.portfolio.curve); renderKpis(s.portfolio);renderChart(s.portfolio.curve);
renderStrat(s.strategies,s.fam_pnl);renderActive(s.active);renderClosed(s.closed); renderStrat(s.strategies,s.fam_pnl);renderActive(s.active);renderClosed(s.closed);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);
</script></body></html>""" </script></body></html>"""