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"
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.
+ +| data/ora UTC | strum. | dir | amount | prezzo | fee 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.
+ +| sleeve | peso | FULL Sh | DD | HOLD Sh |
|---|
| data | asset | azione | posizione | prezzo |
|---|
⚠️ 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.
-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.
-⚠️ 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.
-| data/ora UTC | strum. | dir | amount | prezzo | fee 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