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:
+63
-6
@@ -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,'"')}">${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>"""
|
||||||
|
|||||||
Reference in New Issue
Block a user