feat(dashboard): riordino LIVE→trades→storico + trade/anno contati

- dashboard riorganizzata in 3 sezioni: ① LIVE (mainnet sola lettura) in
  alto, ② TRADES ESEGUITI (reali) + frequenza operativa al centro,
  ③ STORICO (backtest/forward simulato) in fondo (COMBO/BOOK/FORWARD come ③·a/b/c).
- book_trade_frequency() in sleeves.py: trade/anno CONTATI sui dati certificati
  (cache di modulo, una volta per processo). SKH01 round-trip BTC ~37 / ETH ~43
  -> ~75/anno combinato; TP01 turnover ~7x/anno. Card "trade/anno" + blocco freq.
- fix: collisione var `pos` nel ramo shadow-online (-> shpos) che avrebbe rotto
  la tabella posizioni se il conto fosse leggibile dal container.
- fix: turnover TP01 nan (disallineamento groupby) -> groupby posizionale, 7.2x/anno.
- 56 test pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-23 21:18:52 +00:00
parent eeac97dde4
commit 25a22fc7c1
2 changed files with 86 additions and 15 deletions
+45 -15
View File
@@ -14,7 +14,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd import numpy as np, pandas as pd
from src.portfolio.portfolio import StrategyPortfolio, metrics, HOLDOUT from src.portfolio.portfolio import StrategyPortfolio, metrics, HOLDOUT
from src.portfolio.sleeves import active_sleeves from src.portfolio.sleeves import active_sleeves, book_trade_frequency
from src.portfolio.gtaa import gtaa_weights from src.portfolio.gtaa import gtaa_weights
from src.live.shadow import shadow_report, tp01_trades from src.live.shadow import shadow_report, tp01_trades
from src.version import APP_VERSION from src.version import APP_VERSION
@@ -65,6 +65,10 @@ def build():
trades = tp01_trades(limit=15) # entry/exit TP01 dal segnale causale trades = tp01_trades(limit=15) # entry/exit TP01 dal segnale causale
except Exception: except Exception:
trades = [] trades = []
try:
freq = book_trade_frequency() # trade/anno contati (SKH01 round-trip + TP01 turnover)
except Exception as e:
freq = {"error": f"{type(e).__name__}: {e}"}
data = dict( data = dict(
version=APP_VERSION, version=APP_VERSION,
last_data=str(idx[-1].date()), last_data=str(idx[-1].date()),
@@ -72,7 +76,7 @@ def build():
per_sleeve=bt["per_sleeve"], yearly=bt["yearly"], per_sleeve=bt["per_sleeve"], yearly=bt["yearly"],
positions=pf.current_positions(), spark=spark, paper=paper, prevday=prevday, positions=pf.current_positions(), spark=spark, paper=paper, prevday=prevday,
combo=combo, gtaa_weights=gtaa_w, deribit=deribit, combo=combo, gtaa_weights=gtaa_w, deribit=deribit,
shadow=shadow, trades=trades, bh=None, shadow=shadow, trades=trades, freq=freq, bh=None,
) )
_CACHE.update(t=time.time(), data=data) _CACHE.update(t=time.time(), data=data)
return data return data
@@ -180,13 +184,13 @@ def html():
f"{a['target']:+.2f}x" for a in sh["assets"]) f"{a['target']:+.2f}x" for a in sh["assets"])
if sh.get("online"): if sh.get("online"):
eq = f"${sh['real_equity']:,.2f}" if sh.get("real_equity") else sh.get("eq_basis", "?") eq = f"${sh['real_equity']:,.2f}" if sh.get("real_equity") else sh.get("eq_basis", "?")
pos = ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in sh["assets"]) shpos = ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in sh["assets"])
ordtxt = ("; ".join(f"{o['side'].upper()} {o['amount']:.0f} {o['instrument']}" for o in sh["orders"]) ordtxt = ("; ".join(f"{o['side'].upper()} {o['amount']:.0f} {o['instrument']}" for o in sh["orders"])
if sh.get("orders") else "nessuno (target flat / gia' a target)") if sh.get("orders") else "nessuno (target flat / gia' a target)")
dsl = sh.get("disaster_sls") or [] dsl = sh.get("disaster_sls") or []
dsl_txt = (" · ".join(f"{x['asset']} stop <b>${x['stop']:,.0f}</b> ({x['amount']:.4f})" for x in dsl) dsl_txt = (" · ".join(f"{x['asset']} stop <b>${x['stop']:,.0f}</b> ({x['amount']:.4f})" for x in dsl)
if dsl else "nessuno (flat)") if dsl else "nessuno (flat)")
shadow_html = (f"mainnet · sola lettura · conto reale <b>{eq}</b> · pos {pos} · dato {sh['last_data']}<br>" shadow_html = (f"mainnet · sola lettura · conto reale <b>{eq}</b> · pos {shpos} · dato {sh['last_data']}<br>"
f"TP01 target: {bits}<br>ordini-che-invierebbe (<b>NON inviati</b>): {ordtxt}<br>" f"TP01 target: {bits}<br>ordini-che-invierebbe (<b>NON inviati</b>): {ordtxt}<br>"
f"🛡️ disaster-SL attivi (30%): {dsl_txt}") f"🛡️ disaster-SL attivi (30%): {dsl_txt}")
else: else:
@@ -212,6 +216,24 @@ def html():
f"<td>{x['amount']:.4f}</td><td>${x['price']:,.1f}</td><td>{x['fee']:.5f}</td></tr>") f"<td>{x['amount']:.4f}</td><td>${x['price']:,.1f}</td><td>{x['fee']:.5f}</td></tr>")
if not live_trows: if not live_trows:
live_trows = "<tr><td colspan=6 style='color:#8a93a0'>nessun trade reale eseguito (o conto non leggibile dal container)</td></tr>" live_trows = "<tr><td colspan=6 style='color:#8a93a0'>nessun trade reale eseguito (o conto non leggibile dal container)</td></tr>"
fq = d.get("freq")
if fq and "error" not in fq:
skh = fq["skh"]
by = lambda a: " ".join(f"{y}:{c}" for y, c in skh[a]["by_year"].items())
tpy = fq["skh_combo_per_year"]
freq_html = (
f"<b>SKH01</b> (round-trip = apri+chiudi, no-overlap): BTC <b>~{fq['skh_per_year']['BTC']:.0f}/anno</b> · "
f"ETH <b>~{fq['skh_per_year']['ETH']:.0f}/anno</b> → combinato <b>~{tpy:.0f}/anno</b> "
f"(~{2*tpy:.0f} esecuzioni)<br>"
f"<b>TP01</b> (posizione continua vol-target, NON trade discreti): turnover "
f"<b>~{fq['tp01_turnover_per_year']:.0f}×/anno</b> per asset · zero quando flat/risk-off<br>"
f"<span style='color:#8a93a0;font-size:13px'>→ book eseguibile ~{tpy:.0f}-{tpy+5:.0f} operazioni/anno: "
f"poche per design (regime AND breakout). Fee ~$45-60/anno, gia' nette nei numeri storici · min-order $5 OK a $600.<br>"
f"SKH01 round-trip/anno — BTC: {by('BTC')} · ETH: {by('ETH')}</span>")
tpy_card = f"~{tpy:.0f}"
else:
freq_html = "n/d" + (f"{fq['error']}" if fq and fq.get("error") else "")
tpy_card = "~75"
return f"""<!doctype html><html><head><meta charset=utf-8> return f"""<!doctype html><html><head><meta charset=utf-8>
<meta http-equiv=refresh content=300><title>PythagorasGoal — Portafoglio</title> <meta http-equiv=refresh content=300><title>PythagorasGoal — Portafoglio</title>
<style>body{{font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:#0e1116;color:#e6e6e6;margin:0;padding:24px;max-width:980px;margin:auto}} <style>body{{font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:#0e1116;color:#e6e6e6;margin:0;padding:24px;max-width:980px;margin:auto}}
@@ -226,14 +248,26 @@ th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;b
.section{{font-size:15px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;margin:34px 0 14px;padding:10px 14px;border-radius:9px;background:#12181f;border-left:5px solid #2ecc71;color:#d7dee6}} .section{{font-size:15px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;margin:34px 0 14px;padding:10px 14px;border-radius:9px;background:#12181f;border-left:5px solid #2ecc71;color:#d7dee6}}
.section.live{{border-left-color:#e74c3c;background:#1c1316;color:#f0c4c4}}</style></head><body> .section.live{{border-left-color:#e74c3c;background:#1c1316;color:#f0c4c4}}</style></head><body>
<h1>PythagorasGoal — Portafoglio attivo (TP01 + XS01 + VRP01 + SKH01)</h1> <h1>PythagorasGoal — Portafoglio attivo (TP01 + XS01 + VRP01 + SKH01)</h1>
<div class=sub>monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test)</div> <div class=sub>monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test) · ordine: LIVE → trades fatti → storico</div>
<div class="section">PAPER — simulato (backtest + forward virtuale)</div>
<div class="section live">① LIVE — Deribit mainnet (conto reale, sola lettura)</div>
<div class=box><b>Shadow TP01</b> (cosa farebbe ORA sul conto reale, nessun ordine inviato):<br>{shadow_html}</div>
<p class=warn>Solo TP01 e' armato live (cap $300/asset · disaster-SL on-book 30% · capitale reale ≈ $600). SKH01/XS01/VRP01 = paper/stat-mode. Lo "Shadow" mostra il target reale ma NON invia ordini.</p>
<div class="section">② TRADES ESEGUITI (reali) + frequenza operativa</div>
<div class=box><b>Frequenza operativa</b> (contata sui dati certificati, non stimata):<br>{freq_html}</div>
<h3 style="font-size:14px;color:#8a93a0">Trades REALI eseguiti su Deribit</h3>
<table><tr><th>data/ora UTC</th><th>strum.</th><th>dir</th><th>amount</th><th>prezzo</th><th>fee USDC</th></tr>{live_trows}</table>
<p class=warn>Oggi TP01 e' in target TSMOM risk-off → flat: nessun ordine reale. La tabella si popola quando il segnale arma una posizione e il cron giornaliero la esegue.</p>
<div class="section">③ STORICO — simulato (backtest + forward virtuale)</div>
<div class=cards> <div class=cards>
<div class=card><div class=k>FULL Sharpe</div><div class="v g">{f['sharpe']:.2f}</div></div> <div class=card><div class=k>FULL Sharpe</div><div class="v g">{f['sharpe']:.2f}</div></div>
<div class=card><div class=k>HOLD-OUT Sharpe (2025-26)</div><div class="v g">{ho['sharpe']:.2f}</div></div> <div class=card><div class=k>HOLD-OUT Sharpe (2025-26)</div><div class="v g">{ho['sharpe']:.2f}</div></div>
<div class=card><div class=k>maxDD</div><div class=v>{f['maxdd']*100:.1f}%</div></div> <div class=card><div class=k>maxDD</div><div class=v>{f['maxdd']*100:.1f}%</div></div>
<div class=card><div class=k>CAGR</div><div class=v>{f['cagr']*100:.0f}%</div></div> <div class=card><div class=k>CAGR</div><div class=v>{f['cagr']*100:.0f}%</div></div>
<div class=card><div class=k>ret totale</div><div class=v>{f['ret']*100:+.0f}%</div></div> <div class=card><div class=k>ret totale</div><div class=v>{f['ret']*100:+.0f}%</div></div>
<div class=card><div class=k>trade/anno (book eseguibile)</div><div class=v>{tpy_card}</div></div>
</div> </div>
<div class=box><div class=k style="color:#8a93a0;font-size:12px">EQUITY backtest (2019→oggi, €2k)</div>{svg_spark(d['spark'])}</div> <div class=box><div class=k style="color:#8a93a0;font-size:12px">EQUITY backtest (2019→oggi, €2k)</div>{svg_spark(d['spark'])}</div>
<div class=box><b>Paper forward-only:</b> {paper_html}</div> <div class=box><b>Paper forward-only:</b> {paper_html}</div>
@@ -241,25 +275,21 @@ th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;b
<table><tr><th>sleeve</th><th>peso</th><th>FULL Sh</th><th>DD</th><th>HOLD Sh</th></tr>{rows}</table> <table><tr><th>sleeve</th><th>peso</th><th>FULL Sh</th><th>DD</th><th>HOLD Sh</th></tr>{rows}</table>
<h3 style="font-size:14px;color:#8a93a0">Posizioni correnti (ultima barra chiusa)</h3> <h3 style="font-size:14px;color:#8a93a0">Posizioni correnti (ultima barra chiusa)</h3>
<table>{pos}</table> <table>{pos}</table>
<h3 style="font-size:14px;color:#8a93a0">Trades TP01 — entry/exit (segnale causale, ultimi 15)</h3> <h3 style="font-size:14px;color:#8a93a0">Trades TP01 — entry/exit (segnale causale SIMULATO, ultimi 15)</h3>
<table><tr><th>data</th><th>asset</th><th>azione</th><th>posizione</th><th>prezzo</th></tr>{trows}</table> <table><tr><th>data</th><th>asset</th><th>azione</th><th>posizione</th><th>prezzo</th></tr>{trows}</table>
<div style="margin-top:10px">{yrs}</div> <div style="margin-top:10px">{yrs}</div>
<div class="section">COMBO DEPLOYABLE — cross-venue (paper, forward-only)</div> <div class="section">③·a COMBO DEPLOYABLE — cross-venue (paper, forward-only)</div>
<div class=box><b>TP01 (Deribit) + GTAA (IB)</b> — le DUE gambe ESEGUIBILI a basso capitale, scorrelate (corr ~0.21) → blend Sharpe ~1.5, drawdown dimezzato. <b>Nessuna esecuzione reale</b>:<br>{combo_html}<br> <div class=box><b>TP01 (Deribit) + GTAA (IB)</b> — le DUE gambe ESEGUIBILI a basso capitale, scorrelate (corr ~0.21) → blend Sharpe ~1.5, drawdown dimezzato. <b>Nessuna esecuzione reale</b>:<br>{combo_html}<br>
<span style="color:#8a93a0;font-size:13px">posizioni azionabili IB (GTAA, peso ETF):</span> {gw_html}</div> <span style="color:#8a93a0;font-size:13px">posizioni azionabili IB (GTAA, peso ETF):</span> {gw_html}</div>
<p class=warn>⚠️ PAPER cross-venue: valida l'operativita' su due conti (Deribit + IB) a rischio zero. Lo Sharpe ~1.5 e' ottimistico (finestra crypto corta/favorevole); il dato robusto e' la diversificazione (corr 0.21, DD dimezzato), non il livello. XS01/VRP01 esclusi (STAT-MODE): qui solo TP01+GTAA.</p> <p class=warn>⚠️ PAPER cross-venue: valida l'operativita' su due conti (Deribit + IB) a rischio zero. Lo Sharpe ~1.5 e' ottimistico (finestra crypto corta/favorevole); il dato robusto e' la diversificazione (corr 0.21, DD dimezzato), non il livello. XS01/VRP01 esclusi (STAT-MODE): qui solo TP01+GTAA.</p>
<div class="section">BOOK DERIBIT-ONLY — eseguibile (TP01 + SKH01)</div> <div class="section">③·b BOOK DERIBIT-ONLY — eseguibile (TP01 + SKH01)</div>
<div class=box><b>TP01 75% + SKH01 25%</b> — le due gambe direzionali BTC/ETH sullo STESSO venue (Deribit), ribilancio mensile. Sottoinsieme realmente armabile (XS01/VRP01 esclusi):<br>{deribit_html}<br> <div class=box><b>TP01 75% + SKH01 25%</b> — le due gambe direzionali BTC/ETH sullo STESSO venue (Deribit), ribilancio mensile. Sottoinsieme realmente armabile (XS01/VRP01 esclusi):<br>{deribit_html}<br>
<span style="color:#8a93a0;font-size:13px">forecast: <code>scripts/portfolio/forecast_deribit_book.py</code> · report: <code>scripts/portfolio/run_deribit_book.py</code></span></div> <span style="color:#8a93a0;font-size:13px">forecast: <code>scripts/portfolio/forecast_deribit_book.py</code> · report: <code>scripts/portfolio/run_deribit_book.py</code></span></div>
<p class=warn>⚠️ Accumulo = proiezione condizionata (storico bull crypto → pianifica sul conservativo); nessuna leva; SKH01 research/forward-monitor (solo TP01 armato live). A €50/g servono ~€177k @conservativo: la via è capitale+tempo, non leva.</p> <p class=warn>⚠️ Accumulo = proiezione condizionata (storico bull crypto → pianifica sul conservativo); nessuna leva; SKH01 research/forward-monitor (solo TP01 armato live). A €50/g servono ~€177k @conservativo: la via è capitale+tempo, non leva.</p>
<div class="section">FORWARD-MONITOR — lead paper (non deploy)</div> <div class="section">③·c FORWARD-MONITOR — lead paper (non deploy)</div>
<div class=box><b>PREVDAY range-breakout</b> — lead ORTOGONALE a TP01 (corr ~0.15 full / ~0 hold; marginal ADDS, non-hedge, robusto allo shift del confine-giorno). Forward-only, <b>nessuna esecuzione reale</b>:<br>{prevday_html}</div> <div class=box><b>PREVDAY range-breakout</b> — lead ORTOGONALE a TP01 (corr ~0.15 full / ~0 hold; marginal ADDS, non-hedge, robusto allo shift del confine-giorno). Forward-only, <b>nessuna esecuzione reale</b>:<br>{prevday_html}</div>
<p class=warn>⚠️ LEAD in osservazione, NON deployato. Sopravvissuto alla verifica avversariale dell'onda intraday; lo teniamo in paper per validarlo fuori-campione-vero. I due libri (modeled vs real-$600) mostrano l'haircut di fill che lo scettico aveva segnalato.</p> <p class=warn>⚠️ LEAD in osservazione, NON deployato. Sopravvissuto alla verifica avversariale dell'onda intraday; lo teniamo in paper per validarlo fuori-campione-vero. I due libri (modeled vs real-$600) mostrano l'haircut di fill che lo scettico aveva segnalato.</p>
<div class="section live">LIVE — Deribit mainnet (conto reale, sola lettura)</div> <p class=warn>⚠️ Paper/monitor. XS01 e' STAT-MODE (book a 19 gambe market-neutral, non eseguibile a €2k, storia ~2.5 anni). VRP01 = lead short-vol MODELLATO (non deploy pieno). SKH01 (Skyhook dual-TF regime+breakout, BTC/ETH) = diversificatore quasi-ortogonale (corr ~0.09) aggiunto @25%: alza il FULL Sharpe del portafoglio 1.68→2.13 e dimezza il DD (14→8%) — RESEARCH/forward-monitor (book a 230m, causalita' verificata su harness ma costi reali e codice d'esecuzione da validare prima del deploy). TP01 e' l'unico deployable pieno.</p>
<div class=box><b>Shadow TP01</b> (cosa farebbe ORA sul conto reale, nessun ordine inviato):<br>{shadow_html}</div>
<h3 style="font-size:14px;color:#8a93a0">Trades REALI eseguiti su Deribit</h3>
<table><tr><th>data/ora UTC</th><th>strum.</th><th>dir</th><th>amount</th><th>prezzo</th><th>fee USDC</th></tr>{live_trows}</table>
<p class=warn>⚠️ Paper/monitor. XS01 e' STAT-MODE (book a 19 gambe market-neutral, non eseguibile a €2k, storia ~2.5 anni). VRP01 = lead short-vol MODELLATO (non deploy pieno). SKH01 (Skyhook dual-TF regime+breakout, BTC/ETH) = diversificatore quasi-ortogonale (corr ~0.09) aggiunto @25%: alza il FULL Sharpe del portafoglio 1.68→2.13 e dimezza il DD (14→8%) — RESEARCH/forward-monitor (book a 230m, causalita' verificata su harness ma costi reali e codice d'esecuzione da validare prima del deploy). TP01 e' l'unico deployable pieno: lo "Shadow live" mostra cosa farebbe sul mainnet, ma NON invia ordini.</p>
</body></html>""" </body></html>"""
+41
View File
@@ -273,6 +273,47 @@ def skyhook_sleeve(weight: float = 0.25) -> Sleeve:
return Sleeve("SKH01_skyhook", weight, _skyhook_returns, pos_fn=_skyhook_positions) return Sleeve("SKH01_skyhook", weight, _skyhook_returns, pos_fn=_skyhook_positions)
_TRADE_FREQ_CACHE = None
def book_trade_frequency() -> dict:
"""Frequenza operativa del book eseguibile (TP01+SKH01), conteggiata sui dati certificati.
Strategia + storico fissi -> cache a livello di MODULO: calcolata una sola volta per processo
(le nuove barre aggiungono pochissimo). Niente magic-number: e' contata, non stimata.
- SKH01: round-trip DISCRETI (apri+chiudi) no-overlap dell'harness, per asset e per anno.
- TP01: posizione CONTINUA vol-target -> 'turnover' annuo (somma |Δpeso|), non trade discreti."""
global _TRADE_FREQ_CACHE
if _TRADE_FREQ_CACHE is not None:
return _TRADE_FREQ_CACHE
skh = {}
for a in ASSETS:
ltf, htf = build_frames(load_data(a, "5m"))
ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
yr = pd.to_datetime(pd.Series(ltf["timestamp"]).astype("int64"), unit="ms", utc=True).dt.year.values
yc: dict[int, int] = {}
for i, e in enumerate(ent):
if e is not None:
y = int(yr[i]); yc[y] = yc.get(y, 0) + 1
skh[a] = dict(by_year={int(k): int(v) for k, v in sorted(yc.items())},
total=int(sum(yc.values())), first=int(min(yc)), last=int(max(yc)))
sf = min(v["first"] for v in skh.values()); sl = max(v["last"] for v in skh.values())
skh_combo = sum(v["total"] for v in skh.values()) / (sl - sf + 1)
tp = TrendPortfolio(**CANONICAL); turns = []
for a in ASSETS:
df = resample_1d(load_data(a, "1h"))
tgt = np.asarray(tp.target_series(df), dtype=float)
years = pd.DatetimeIndex(pd.to_datetime(df["datetime"])).year # ndarray-aligned key
d = np.abs(np.diff(np.nan_to_num(tgt, nan=0.0), prepend=0.0)) # turnover = somma |Δpeso|
ty = pd.Series(d).groupby(years).sum() # groupby posizionale (RangeIndex)
turns.append(float(ty.mean()))
_TRADE_FREQ_CACHE = dict(
skh=skh,
skh_per_year={a: skh[a]["total"] / (skh[a]["last"] - skh[a]["first"] + 1) for a in ASSETS},
skh_combo_per_year=skh_combo,
tp01_turnover_per_year=sum(turns) / len(turns))
return _TRADE_FREQ_CACHE
# ----------------------------- REGISTRY ----------------------------- # ----------------------------- REGISTRY -----------------------------
def active_sleeves() -> list[Sleeve]: def active_sleeves() -> list[Sleeve]:
"""Sleeve ATTIVI nel portafoglio (pesi rinormalizzati; sleeve a date diverse si attivano """Sleeve ATTIVI nel portafoglio (pesi rinormalizzati; sleeve a date diverse si attivano