From 25a22fc7c1173f3f1c2d496f132d122e92564a2e Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Tue, 23 Jun 2026 21:18:52 +0000 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20riordino=20LIVE=E2=86=92trad?= =?UTF-8?q?es=E2=86=92storico=20+=20trade/anno=20contati?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- src/live/dashboard.py | 60 ++++++++++++++++++++++++++++++---------- src/portfolio/sleeves.py | 41 +++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/src/live/dashboard.py b/src/live/dashboard.py index 0cacb98..02a493b 100644 --- a/src/live/dashboard.py +++ b/src/live/dashboard.py @@ -14,7 +14,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np, pandas as pd 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.live.shadow import shadow_report, tp01_trades from src.version import APP_VERSION @@ -65,6 +65,10 @@ def build(): trades = tp01_trades(limit=15) # entry/exit TP01 dal segnale causale except Exception: 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( version=APP_VERSION, last_data=str(idx[-1].date()), @@ -72,7 +76,7 @@ def build(): per_sleeve=bt["per_sleeve"], yearly=bt["yearly"], positions=pf.current_positions(), spark=spark, paper=paper, prevday=prevday, 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) return data @@ -180,13 +184,13 @@ def html(): f"{a['target']:+.2f}x" for a in sh["assets"]) if sh.get("online"): 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"]) if sh.get("orders") else "nessuno (target flat / gia' a target)") dsl = sh.get("disaster_sls") or [] dsl_txt = (" · ".join(f"{x['asset']} stop ${x['stop']:,.0f} ({x['amount']:.4f})" for x in dsl) if dsl else "nessuno (flat)") - shadow_html = (f"mainnet · sola lettura · conto reale {eq} · pos {pos} · dato {sh['last_data']}
" + shadow_html = (f"mainnet · sola lettura · conto reale {eq} · pos {shpos} · dato {sh['last_data']}
" f"TP01 target: {bits}
ordini-che-invierebbe (NON inviati): {ordtxt}
" f"🛡️ disaster-SL attivi (−30%): {dsl_txt}") else: @@ -212,6 +216,24 @@ def html(): f"{x['amount']:.4f}${x['price']:,.1f}{x['fee']:.5f}") if not live_trows: live_trows = "nessun trade reale eseguito (o conto non leggibile dal container)" + 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"SKH01 (round-trip = apri+chiudi, no-overlap): BTC ~{fq['skh_per_year']['BTC']:.0f}/anno · " + f"ETH ~{fq['skh_per_year']['ETH']:.0f}/anno → combinato ~{tpy:.0f}/anno " + f"(~{2*tpy:.0f} esecuzioni)
" + f"TP01 (posizione continua vol-target, NON trade discreti): turnover " + f"~{fq['tp01_turnover_per_year']:.0f}×/anno per asset · zero quando flat/risk-off
" + f"→ 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.
" + f"SKH01 round-trip/anno — BTC: {by('BTC')} · ETH: {by('ETH')}
") + 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""" PythagorasGoal — Portafoglio

PythagorasGoal — Portafoglio attivo (TP01 + XS01 + VRP01 + SKH01)

-
monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test)
-
PAPER — simulato (backtest + forward virtuale)
+
monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test) · ordine: LIVE → trades fatti → storico
+ +
① LIVE — Deribit mainnet (conto reale, sola lettura)
+
Shadow TP01 (cosa farebbe ORA sul conto reale, nessun ordine inviato):
{shadow_html}
+

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.

+ +
② TRADES ESEGUITI (reali) + frequenza operativa
+
Frequenza operativa (contata sui dati certificati, non stimata):
{freq_html}
+

Trades REALI eseguiti su Deribit

+{live_trows}
data/ora UTCstrum.diramountprezzofee USDC
+

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.

+ +
③ STORICO — simulato (backtest + forward virtuale)
FULL Sharpe
{f['sharpe']:.2f}
HOLD-OUT Sharpe (2025-26)
{ho['sharpe']:.2f}
maxDD
{f['maxdd']*100:.1f}%
CAGR
{f['cagr']*100:.0f}%
ret totale
{f['ret']*100:+.0f}%
+
trade/anno (book eseguibile)
{tpy_card}
EQUITY backtest (2019→oggi, €2k)
{svg_spark(d['spark'])}
Paper forward-only: {paper_html}
@@ -241,25 +275,21 @@ th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;b {rows}
sleevepesoFULL ShDDHOLD Sh

Posizioni correnti (ultima barra chiusa)

{pos}
-

Trades TP01 — entry/exit (segnale causale, ultimi 15)

+

Trades TP01 — entry/exit (segnale causale SIMULATO, ultimi 15)

{trows}
dataassetazioneposizioneprezzo
{yrs}
-
COMBO DEPLOYABLE — cross-venue (paper, forward-only)
+
③·a COMBO DEPLOYABLE — cross-venue (paper, forward-only)
TP01 (Deribit) + GTAA (IB) — le DUE gambe ESEGUIBILI a basso capitale, scorrelate (corr ~0.21) → blend Sharpe ~1.5, drawdown dimezzato. Nessuna esecuzione reale:
{combo_html}
posizioni azionabili IB (GTAA, peso ETF): {gw_html}

⚠️ 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.

-
BOOK DERIBIT-ONLY — eseguibile (TP01 + SKH01)
+
③·b BOOK DERIBIT-ONLY — eseguibile (TP01 + SKH01)
TP01 75% + SKH01 25% — le due gambe direzionali BTC/ETH sullo STESSO venue (Deribit), ribilancio mensile. Sottoinsieme realmente armabile (XS01/VRP01 esclusi):
{deribit_html}
forecast: scripts/portfolio/forecast_deribit_book.py · report: scripts/portfolio/run_deribit_book.py

⚠️ 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.

-
FORWARD-MONITOR — lead paper (non deploy)
+
③·c FORWARD-MONITOR — lead paper (non deploy)
PREVDAY range-breakout — lead ORTOGONALE a TP01 (corr ~0.15 full / ~0 hold; marginal ADDS, non-hedge, robusto allo shift del confine-giorno). Forward-only, nessuna esecuzione reale:
{prevday_html}

⚠️ 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.

-
LIVE — Deribit mainnet (conto reale, sola lettura)
-
Shadow TP01 (cosa farebbe ORA sul conto reale, nessun ordine inviato):
{shadow_html}
-

Trades REALI eseguiti su Deribit

-{live_trows}
data/ora UTCstrum.diramountprezzofee USDC
-

⚠️ 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.

+

⚠️ 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.

""" diff --git a/src/portfolio/sleeves.py b/src/portfolio/sleeves.py index f59284d..9d99bcc 100644 --- a/src/portfolio/sleeves.py +++ b/src/portfolio/sleeves.py @@ -273,6 +273,47 @@ def skyhook_sleeve(weight: float = 0.25) -> Sleeve: 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 ----------------------------- def active_sleeves() -> list[Sleeve]: """Sleeve ATTIVI nel portafoglio (pesi rinormalizzati; sleeve a date diverse si attivano