#!/usr/bin/env python """r0822_vol_size.py — CONTROLLO DEL RISCHIO SU SKH01 e VOL-TARGET A LIVELLO DI LIBRO. DOMANDA (ondata 2026-08-22, filone VOL-SIZE). Il libro live Deribit e' TP01 75% + SKH01 25%, nettati su un solo strumento. TP01 e' vol-targeted al 20% sulla vol REALIZZATA dell'asset; **SKH01 non ha alcun controllo del rischio** — `backtest_signals(..., leverage=1.0, position_size=1.0)`, verificato il 26/07: il suo 20,7% di vol realizzata e' un PRODOTTO della strategia (uscite % asimmetriche + poco tempo a mercato), non un parametro. E' l'unica delle cinque gambe senza dimensionamento, la piu' fee-sensibile (~4x TP01) e la piu' fragile all'ancora (LOO 26/07). Q1 dare a SKH01 un dimensionamento del rischio (inverse-vol per-trade / rischio costante per trade / vol-target sulla sua vol di strategia) migliora il LIBRO, o e' de-levering? Q2 il vol-target va applicato al LIBRO gia' nettato invece che al singolo sleeve? Oggi ogni sleeve si dimensiona da solo e poi si nettano: la versione book-level non e' mai stata misurata. COSA NON SI RIFA' (gia' misurato): TP01 x DVOL (26/06, de-levering); il peso 75/25 (confermato 3 volte). I pesi NOMINALI non si toccano; una modulazione di size e' pero' un cambio di pesi nel tempo, quindi `weights_tilt_null` si riporta lo stesso (§7). IPOTESI A PRIORI, REGISTRATA PRIMA DI MISURARE: **de-levering su quasi tutto**. Ragione dichiarata prima: lo Sharpe e' INVARIANTE a una costante moltiplicativa, quindi una variante che si limiti ad abbassare la leva media non puo' cambiare lo Sharpe e puo' solo abbassare il DD — la firma esatta del null del de-levering (5 occorrenze nel progetto). Per VINCERE una variante deve cambiare la FORMA (quale trade pesa quanto), e la forma si vede solo sullo Sharpe. Sotto-ipotesi: l'inverse-vol per-trade e' l'unica con una ragione strutturale (lo stop di SKH01 e' una PERCENTUALE FISSA 4%/2%, quindi il rischio in unita' di sigma varia col regime); il vol-target sulla serie giornaliera dello sleeve e' quello che ha piu' probabilita' di rompersi (SKH01 e' ~88% di zeri: la vol trailing di quella serie misura l'ATTIVITA', non il rischio). ⚠️ DIFETTO TROVATO NELLA MIA PRIMA STESURA, E MISURATO INVECE CHE CANCELLATO (§4). Il modo ovvio di scrivere un vol-target su uno sleeve — moltiplicare la sua serie GIORNALIERA per L_t = tv/rv_{t-1} — e' **non causale su SKH01**, perche' la sua equity e' a GRADINO: tutto il P&L di un trade e' contabilizzato il giorno di CHIUSURA. Scalare quel giorno per L_t significa aver tenuto size L_t dall'INGRESSO, che e' fino a ~4 giorni prima (24 barre da 230m). L_t usa solo dati <= t-1 e quindi passa qualunque controllo di causalita' scritto sulla serie giornaliera: il look-ahead sta nella CONTABILITA', non nella formula. La versione causale fissa la size all'INGRESSO. Entrambe sono misurate qui, e la differenza e' un risultato. LENTE E ANCORA. Path CANONICO (fill al livello, entry a chiusura di bin): e' la lente di tutti i numeri pubblicati di SKH01 ed e' identica nelle due braccia di ogni confronto — il Delta e' pulito anche se il livello assoluto e' pessimistico verso il path live (26/07: il live e' migliore su ingressi E uscite). Banda d'ancora = **23 offset**, la griglia a priori dell'audit 02/07 (ogni 30m su [0,690)); statistica = **mediana delle DIFFERENZE APPAIATE** (`A.anchor_luck_delta`), mai differenza delle mediane. L'ancora di TP01 e' tenuta canonica in entrambe le braccia — §6 verifica che il segno non dipenda da lei. ⚠️ IL CONTROLLO CHE DECIDE — ISO-PESO. Quasi ogni variante di size alza la VOL della gamba SKH, e quindi il suo peso EFFETTIVO nel libro (i nominali restano 75/25). Ma "alzare il peso di SKH01" e' gia' stato misurato e RESPINTO il 26/07 (argmax w=0.35, `weights_tilt_null` fallito: alzarlo non compra Sharpe, compra dipendenza dall'ancora). Percio' il confronto pubblicato non e' vs il baseline nudo ma vs il baseline **RI-SCALATO allo stesso peso effettivo** (colonne ISO): li' resta solo la FORMA — quale trade pesa quanto. E' la regola dell'iso-rischio gia' codificata (25/07 §3). Corollario: siccome lo Sharpe e' invariante alla scala, `sh(k*baseline) == sh(baseline)`, quindi il null del de-levering in questa famiglia COINCIDE col confronto degli Sharpe. `k_iso` resta come diagnostica di quanto si e' mosso il DD. DECOMPOSIZIONE DEL MECCANISMO (famiglia ACT). La vol trailing di uno sleeve all'88% di zeri contiene DUE canali: la MAGNITUDINE degli esiti e la FREQUENZA dei trade. `VTL ... attive` isola il primo (vol sulle sole barre attive), `ACT` isola il secondo (conteggio di barre attive nella finestra). Se il guadagno vive in ACT la variante non e' "controllo del rischio" ma un filtro di frequenza, e va detto. NULL DI PERMUTAZIONE (§10). Lo stesso MULTINSIEME di size riassegnato a caso ai trade: separa "conta QUALE trade riceve quale size" (informazione) da "conta la DISTRIBUZIONE delle size". PRESENTAZIONE. Lo Sharpe e' invariante alla scala, maxDD e CAGR no: ogni variante e' riportata anche a **ISO-VOL** (costante che pareggia la deviazione standard piena a quella del baseline). E' una scelta di presentazione, post-hoc e dichiarata; non cambia lo Sharpe, che e' cio' che decide. Il null del de-levering (`k_iso_dd`, importato da r0822_sol_leg, non riscritto) e' calcolato per ogni variante che riduce il DD. USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_vol_size.py [--every K] 1 offset ogni K dei 23 (K=1 = tutti; pilota: --every 8) """ from __future__ import annotations import argparse import sys import time from pathlib import Path import numpy as np import pandas as pd ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research")) sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) import altlib as A # noqa: E402 import r0702_anchor_skh01 as R # noqa: E402 (run_asset/resample_off riusati) import r0702_tp01_offset as TO # noqa: E402 (TP01 per ancora + target 1h) from r0822_sol_leg import k_iso_dd # noqa: E402 (null de-levering, non riscritto) from src.portfolio.portfolio import ( # noqa: E402 HOLDOUT, combine_outer, weights_tilt_null, ) from src.backtest.harness import backtest_signals # noqa: E402 from src.strategies.skyhook import ( # noqa: E402 SKH01_V1, SKH01_V2_DD, skyhook_entries, ) ASSETS = ("BTC", "ETH") OFFSETS_FULL = tuple(range(0, 690, 30)) # 23 a priori: griglia dell'audit 02/07 LTF_MIN = 230 BARS_YR = 365.25 * 24 * 60 / LTF_MIN DPY = 365.25 FEE_RT = 0.001 W_TP, W_SKH = 0.75, 0.25 # libro live Deribit CAPITAL = 635.0 # conto reale dichiarato nel brief MIN_ORDER = 5.0 CAP_ASSET = 0.5 # cap $318/asset su equity $635 SIG_WARM = 2000 # barre 230m (~1 anno) prima di dimensionare VT_WARM = 250 # giorni prima di dimensionare su vol di strategia # --------------------------------------------------------------------------- metriche def sh(s) -> float: r = np.asarray(pd.Series(s).dropna().values, float) return float(r.mean() / r.std() * np.sqrt(DPY)) if len(r) > 2 and r.std() > 0 else 0.0 def maxdd(s) -> float: r = np.asarray(pd.Series(s).dropna().values, float) if len(r) < 2: return 0.0 eq = np.cumprod(1.0 + r) pk = np.maximum.accumulate(eq) return float(np.max((pk - eq) / pk)) def cagr(s) -> float: r = np.asarray(pd.Series(s).dropna().values, float) if len(r) < 2: return 0.0 eq = float(np.prod(1.0 + r)) yrs = len(r) / DPY return float(eq ** (1 / yrs) - 1) if eq > 0 and yrs > 0 else -1.0 def vol(s) -> float: r = np.asarray(pd.Series(s).dropna().values, float) return float(r.std() * np.sqrt(DPY)) if len(r) > 2 else 0.0 def hold(s: pd.Series) -> pd.Series: return s[s.index >= HOLDOUT] def ins(s: pd.Series) -> pd.Series: return s[s.index < HOLDOUT] # --------------------------------------------------------------------------- estrazione trade def extract(asset: str, off: int, params=None) -> dict: """Trade + contesto per (asset, offset). Replica il loop di `backtest_signals` con leverage=1: stessa detection, stesso non-overlap, stessi fill al livello. Cio' che aggiunge e' la SIZE per-trade, che l'harness non espone (ha un solo `position_size` scalare). La §0 verifica bit-exact che con size=1 si ritrovi la serie ufficiale.""" if params is None or params is SKH01_V2_DD: _, _, ltf, ent = R.run_asset(asset, off) # path canonico (sanity bit-exact §0) else: # stessa pipeline, altra config Skyhook df5 = R.get5m(asset) ltf = R.resample_off(df5, R.LTF_MIN, off) htf = R.resample_off(df5, R.HTF_MIN, off) ent = skyhook_entries(ltf, htf, params) c = ltf["close"].values.astype(float) h = ltf["high"].values.astype(float) lo = ltf["low"].values.astype(float) n = len(c) i_ent, i_ex, dirs, nets, stops = [], [], [], [], [] busy = -1 for i in range(n): e = ent[i] if i < len(ent) else None if e is None or e.get("dir", 0) == 0 or i <= busy: continue d = int(e["dir"]) entry = c[i] tp, sl = e.get("tp"), e.get("sl") mb = int(e.get("max_bars") or 24) ex_i = min(i + mb, n - 1) ex_p = c[ex_i] for j in range(i + 1, min(i + mb + 1, n)): hit_sl = sl is not None and ((d == 1 and lo[j] <= sl) or (d == -1 and h[j] >= sl)) hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and lo[j] <= tp)) if hit_sl: ex_p, ex_i = sl, j break if hit_tp: ex_p, ex_i = tp, j break ex_p, ex_i = c[j], j i_ent.append(i); i_ex.append(ex_i); dirs.append(d) nets.append((ex_p - entry) / entry * d - FEE_RT) stops.append(abs(float(sl) - entry) / entry) # DERIVATO dal segnale, non ridichiarato busy = ex_i idxE = np.asarray(i_ent, int) lr = A.log_returns(c) sig, sigref = {}, {} for w in (30, 90): sv = A.realized_vol(lr, w, BARS_YR) # finestra che termina in i: causale sig[w] = sv[idxE] if len(idxE) else np.array([]) sigref[w] = _expanding_median_at(sv, idxE) idx = pd.DatetimeIndex(pd.to_datetime(ltf["timestamp"].values, unit="ms", utc=True)) out = dict( n=n, idx=idx, i_ent=idxE, i_ex=np.asarray(i_ex, int), dir=np.asarray(dirs, int), net=np.asarray(nets, float), stop=np.asarray(stops, float), sig=sig, sigref=sigref, ts_close=ltf["timestamp"].values.astype(np.int64) + LTF_MIN * 60_000, day_ent=idx[idxE].floor("D") if len(idxE) else pd.DatetimeIndex([], tz="UTC"), ) out["base_daily"] = equity_daily(out, size_flat(out)) # riusata da VTL/§9: calcolarla 1 volta R._CACHE.clear() # ent = 18k dict per chiave: non accumulare return out def _expanding_median_at(v: np.ndarray, idxE: np.ndarray) -> np.ndarray: """Mediana ESPANDENTE di v fino a i incluso, valutata solo agli indici d'ingresso. Causale per costruzione. NaN prima del warm-up.""" out = np.full(len(idxE), np.nan) for k, i in enumerate(idxE): if i < SIG_WARM: continue x = v[: i + 1] x = x[np.isfinite(x)] if len(x) >= SIG_WARM // 2: out[k] = float(np.median(x)) return out def equity_daily(ex: dict, sizes: np.ndarray) -> pd.Series: """Serie giornaliera dei rendimenti dalla tabella trade, con size per-trade. Convenzione IDENTICA a `backtest_signals` + `_skyhook_returns`: equity a gradino a fine trade, resample 1D last, ffill, pct_change.""" n = ex["n"] eq = np.full(n, 1000.0, float) cap = 1000.0 for k in range(len(ex["i_ent"])): cap += cap * float(sizes[k]) * float(ex["net"][k]) cap = max(cap, 1.0) eq[ex["i_ent"][k]: ex["i_ex"][k] + 1] = cap for k in range(1, n): # stessi due riempimenti dell'harness if eq[k] == 1000.0 and eq[k - 1] != 1000.0: eq[k] = eq[k - 1] last = 1000.0 for k in range(n): if eq[k] != last and eq[k] != 1000.0: last = eq[k] else: eq[k] = last return pd.Series(eq, index=ex["idx"]).resample("1D").last().ffill().pct_change().dropna() def leg_daily(exs: dict, sizes: dict) -> pd.Series: """Sleeve SKH01 50/50 BTC+ETH (convenzione di `_skyhook_returns`: inner join).""" ser = {a: equity_daily(exs[a], sizes[a]) for a in ASSETS} J = pd.concat(ser, axis=1, join="inner").fillna(0.0) return pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index) # --------------------------------------------------------------------------- famiglie di size def size_flat(ex: dict) -> np.ndarray: return np.ones(len(ex["i_ent"])) def size_iv(p: float, w: int, cap: float): """INVERSE-VOL per-trade sulla vol dell'ASSET: size = clip((sig_rif/sig_ingresso)^p, 1/cap, cap). sig_rif = mediana ESPANDENTE causale -> size media ~1: la variante e' neutra alla leva, quindi qualunque effetto sullo Sharpe e' FORMA, non de-levering.""" def f(ex: dict) -> np.ndarray: s, r = ex["sig"][w], ex["sigref"][w] with np.errstate(divide="ignore", invalid="ignore"): raw = np.where((s > 0) & np.isfinite(r), (r / np.maximum(s, 1e-12)) ** p, 1.0) return np.clip(np.nan_to_num(raw, nan=1.0, posinf=cap, neginf=1.0), 1.0 / cap, cap) return f def size_risk(p: float): """RISCHIO COSTANTE PER TRADE: size ~ 1/distanza-dallo-stop. Lo stop di SKH01 e' una percentuale FISSA (4% long / 2% short) -> questa famiglia e' di fatto 'gli short pesano 2x i long'. La costante e' irrilevante (lo Sharpe e' scale-invariante).""" def f(ex: dict) -> np.ndarray: st = np.maximum(ex["stop"], 1e-9) return np.where(ex["stop"] > 0, (0.04 / st) ** p, 1.0) return f def _rv_daily(s: pd.Series, w: int, active_only: bool) -> pd.Series: """Vol realizzata trailing di una serie GIORNALIERA, SFASATA di 1 giorno (causale al giorno t). active_only=True la calcola sulle sole barre ATTIVE e la riporta sulla griglia di calendario: SKH01 e' ~88% di zeri, e la versione 'tutte le barre' misura l'ATTIVITA', non il rischio.""" if not active_only: return (s.rolling(w, min_periods=max(5, w // 3)).std() * np.sqrt(DPY)).shift(1) act = s[s != 0.0] rv = act.rolling(w, min_periods=max(5, w // 3)).std() * np.sqrt(DPY) return rv.reindex(s.index).ffill().shift(1) def leverage_series(base: pd.Series, tv: float | None, w: int, cap: float, active_only: bool = False) -> pd.Series: """L_t da una serie giornaliera di riferimento. tv=None -> versione NEUTRA ALLA LEVA (rif = mediana espandente causale di rv), tv=float -> vol-target assoluto.""" rv = _rv_daily(base, w, active_only).replace(0.0, np.nan) if tv is None: ref = rv.expanding(min_periods=VT_WARM).median() L = (ref / rv).clip(lower=1.0 / cap, upper=cap) else: L = (tv / rv).clip(lower=0.0, upper=cap) return L.fillna(1.0) def activity_L(base: pd.Series, w: int, cap: float) -> pd.Series: """L_t da sola ATTIVITA': quante barre ATTIVE (trade chiusi) negli ultimi w giorni, riferita alla mediana espandente causale. Serve a separare i DUE canali dentro `rv` calcolata su tutte le barre di uno sleeve all'88% di zeri: la MAGNITUDINE degli esiti e la FREQUENZA dei trade. Se il guadagno vive qui, la variante non e' 'vol-target' ma un filtro di frequenza.""" act = (base != 0.0).astype(float) cnt = act.rolling(w, min_periods=max(5, w // 3)).sum().shift(1).replace(0.0, np.nan) ref = cnt.expanding(min_periods=VT_WARM).median() return (ref / cnt).clip(lower=1.0 / cap, upper=cap).fillna(1.0) def sizes_from_L(ex: dict, L: pd.Series) -> np.ndarray: """La size di ogni trade = L al giorno d'INGRESSO. E' la versione CAUSALE del vol-target su uno sleeve a equity a gradino: L al giorno di CHIUSURA non era nota all'ingresso.""" if not len(ex["i_ent"]): return np.array([]) v = L.reindex(ex["day_ent"]).values.astype(float) return np.nan_to_num(v, nan=1.0) # --------------------------------------------------------------------------- libro def sizes_of(nm, ex: dict, SZ: dict, VTL: dict, ACT: dict, Lbook: pd.Series | None = None): """Vettore di size per-trade di una cella qualsiasi. Un solo posto in cui e' definita: §8 (eseguibilita'), §9 (deriva) e §10 (null di permutazione) devono usare LA STESSA.""" if nm is None or nm == "BASE": return size_flat(ex) if nm in SZ: return SZ[nm](ex) if nm in VTL: return sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm])) if nm in ACT: return sizes_from_L(ex, activity_L(ex["base_daily"], **ACT[nm])) return sizes_from_L(ex, Lbook) def book(tp: pd.Series, skh: pd.Series) -> pd.Series: return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": W_TP, "SKH01": W_SKH}) def iso_vol(s: pd.Series, ref: pd.Series) -> pd.Series: v = vol(s) return s * (vol(ref) / v) if v > 0 else s HEAD = (f" {'variante':<28}{'ShFULL':>8}{'ShHOLD':>9}{'ShIS':>8}" f"{'vol':>9}{'maxDD':>9}{'DDisovol':>10}{'CAGRiso':>9}{'size med':>10}") def transfer_test(offs) -> None: """TEST DI TRASFERIMENTO — la regola di size CONGELATA (tv=0.20, w=90, cap 3, fissata all'INGRESSO) applicata a SKH01_**V1**, l'altra config Skyhook: stesso motore, stessi asset, ma geometria d'uscita DIVERSA (stop/target in ATR 2.5/6.0 invece che a percentuale fissa 4%/2%, ptn_n=55 invece di 45). E' il gate giusto per questo filone: un forward di 6 mesi non puo' misurare un dSharpe di 0.07 (SE ~1.4), un trasferimento si'. Se il meccanismo e' 'pareggiare il contributo di rischio dei trade' deve funzionare anche li'; se e' cucito sulla sequenza di trade di V2-DD, no. NB: V1 e' l'unica config alternativa gia' nel repo e non scelta da me — non e' una griglia nuova.""" print("=" * 112) print(" TEST DI TRASFERIMENTO — regola di size CONGELATA da V2-DD, applicata a SKH01_V1") print("=" * 112) TP = A.tp01_baseline_daily() sp = dict(tv=0.20, w=90, cap=3.0, active_only=False) base_b, var_b, ctrl_b = {}, {}, {} for o in offs: ex = {a: extract(a, o, SKH01_V1) for a in ASSETS} lb = leg_daily(ex, {a: size_flat(ex[a]) for a in ASSETS}) lv = leg_daily(ex, {a: sizes_from_L(ex[a], leverage_series(ex[a]["base_daily"], **sp)) for a in ASSETS}) m_s = vol(lv) / vol(lb) if vol(lb) > 0 else 1.0 base_b[o], var_b[o] = book(TP, lb), book(TP, lv) ctrl_b[o] = book(TP, lb * m_s) dF = A.anchor_luck_delta(lambda o: var_b[o], lambda o: ctrl_b[o], offs, metric=sh) dH = A.anchor_luck_delta(lambda o: hold(var_b[o]), lambda o: hold(ctrl_b[o]), offs, metric=sh) rF = A.anchor_luck_delta(lambda o: var_b[o], lambda o: base_b[o], offs, metric=sh) print(f" trade V1 medi/ancora: " + ", ".join( f"{a} {int(np.mean([len(extract(a, offs[0], SKH01_V1)['i_ent'])]))}" for a in ASSETS)) print(f" libro V1 BASE, mediana della banda: ShFULL " f"{np.median([sh(base_b[o]) for o in offs]):.3f} / ShHOLD " f"{np.median([sh(hold(base_b[o])) for o in offs]):.3f}") print(f" dShFULL grezzo {rF['median_paired']:+.3f} ({rF['n_positive']}/{len(offs)} ancore)") print(f" dShFULL ISO-PESO {dF['median_paired']:+.3f} ({dF['n_positive']}/{len(offs)} ancore)" f" banda [{dF['lo']:+.3f}, {dF['hi']:+.3f}]") print(f" dShHOLD ISO-PESO {dH['median_paired']:+.3f} ({dH['n_positive']}/{len(offs)} ancore)") yrs = sorted({int(y) for y in base_b[offs[0]].index.year}) cells = [] for y in yrs: v = [sh(var_b[o][var_b[o].index.year == y]) - sh(ctrl_b[o][ctrl_b[o].index.year == y]) for o in offs if (var_b[o].index.year == y).sum() > 20] cells.append(float(np.median(v)) if v else float("nan")) print(" per anno (ISO): " + "".join(f"{y:>8}" for y in yrs)) print(" " + "".join(f"{c:>+8.2f}" for c in cells) + f" positivi {int(np.nansum(np.asarray(cells) > 0))}/{len(yrs)}") print("\n LETTURA: se il segno regge qui, il meccanismo non e' cucito su V2-DD. Se non regge,") print(" la regola e' una proprieta' della sequenza di trade su cui e' stata trovata.") def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--every", type=int, default=1) ap.add_argument("--transfer", action="store_true", help="esegue SOLO il test di trasferimento su SKH01_V1") args = ap.parse_args() if args.transfer: transfer_test(OFFSETS_FULL[:: max(1, args.every)]) return offs = OFFSETS_FULL[:: max(1, args.every)] t0 = time.time() print("=" * 112) print(" r0822 VOL-SIZE — controllo del rischio su SKH01 (Q1) e vol-target di LIBRO (Q2)") print("=" * 112) print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} offset a priori {list(offs)}") print(" lente: path CANONICO. Ancora TP01: canonica in ENTRAMBE le braccia (§6 la varia).") print("\n estrazione tabelle trade ...", flush=True) EX = {o: {a: extract(a, o) for a in ASSETS} for o in offs} print(f" fatto in {time.time()-t0:.0f}s — trade medi/ancora: " + ", ".join(f"{a} {int(np.mean([len(EX[o][a]['i_ent']) for o in offs]))}" for a in ASSETS)) # ------------------------------------------------------------------ §0 SANITY print("\n" + "-" * 112) print(" 0. SANITY — con size=1 la ricomposizione DEVE riprodurre la serie ufficiale bit-exact") print(" (se non e' 0, ogni numero sotto e' di un'altra strategia)") print("-" * 112) worst = 0.0 for o in offs[: min(3, len(offs))]: for a in ASSETS: mine = equity_daily(EX[o][a], size_flat(EX[o][a])) ref = R.run_asset(a, o)[0] R._CACHE.clear() assert len(mine) == len(ref), f"len {len(mine)} vs {len(ref)} ({a},{o})" d = float(np.max(np.abs(mine.values - ref.values))) worst = max(worst, d) print(f" {a} off{o:>3}: max|dif| = {d:.2e} ({len(mine)} giorni)") assert worst < 1e-15, f"ricomposizione NON bit-exact: {worst:.2e}" if 0 in EX: from src.portfolio.sleeves import _skyhook_returns s0 = leg_daily(EX[0], {a: size_flat(EX[0][a]) for a in ASSETS}) d0 = float(np.max(np.abs(s0.values - _skyhook_returns().values))) print(f" sleeve 50/50 off0 vs sleeves._skyhook_returns(): max|dif| = {d0:.2e}") assert d0 < 1e-15 # ------------------------------------------------------------------ §1 CAUSALITA' print("\n" + "-" * 112) print(" 1. CAUSALITA' — ogni size ricalcolata TRONCANDO i dati alla barra d'ingresso") print("-" * 112) ex = EX[offs[0]]["BTC"] _, _, ltf, _ = R.run_asset("BTC", offs[0]) c_full = ltf["close"].values.astype(float) R._CACHE.clear() full_sz = size_iv(1.0, 30, 3.0)(ex) sel = [k for k in range(len(ex["i_ent"])) if ex["i_ent"][k] >= SIG_WARM][:40] bad = 0 for k in sel: i = ex["i_ent"][k] sv_t = A.realized_vol(A.log_returns(c_full[: i + 1]), 30, BARS_YR) s_t = sv_t[i] r_t = float(np.median(sv_t[np.isfinite(sv_t)])) sz_t = np.clip((r_t / s_t) if s_t > 0 else 1.0, 1 / 3.0, 3.0) bad += int(abs(sz_t - full_sz[k]) > 1e-9) print(f" IV: {len(sel)-bad}/{len(sel)} size identiche a dati troncati (divergenze {bad})") assert bad == 0, "la size IV usa dati futuri" lag = np.array([(ex["idx"][ex["i_ex"][k]] - ex["idx"][ex["i_ent"][k]]).total_seconds() / 86400 for k in range(len(ex["i_ent"]))]) print(f" durata dei trade (giorni): mediana {np.median(lag):.2f}, p90 {np.percentile(lag,90):.2f}, " f"max {lag.max():.2f} <- e' l'ampiezza del look-ahead della versione NAIVE (§4)") # ------------------------------------------------------------------ griglia SZ: dict = {} for p in (0.5, 1.0): for w in (30, 90): for cp in (2.0, 4.0): SZ[f"IV p{p} w{w} cap{cp:.0f}"] = size_iv(p, w, cp) for p in (0.5, 1.0): SZ[f"RISK p{p}"] = size_risk(p) VTL = {} # vol-target sulla vol di STRATEGIA, causale for w in (30, 90): VTL[f"VTL neutro w{w}"] = dict(tv=None, w=w, cap=3.0, active_only=False) VTL[f"VTL tv20 w{w}"] = dict(tv=0.20, w=w, cap=3.0, active_only=False) VTL["VTL neutro w90 attive"] = dict(tv=None, w=90, cap=3.0, active_only=True) VTL["VTL tv20 w90 attive"] = dict(tv=0.20, w=90, cap=3.0, active_only=True) BVT = {} # Q2: vol-target del LIBRO for w in (30, 90, 180): for cp in (2.0, 3.0): BVT[f"BOOKVT w{w} cap{cp:.0f}"] = dict(tv=0.10, w=w, cap=cp) ACT = {f"ACT w{w}": dict(w=w, cap=3.0) for w in (90, 180)} NAIVE = {f"NAIVE-VT w{w}": dict(tv=0.20, w=w, cap=3.0, active_only=False) for w in (30, 90)} ALL = list(SZ) + list(VTL) + list(ACT) + list(BVT) + list(NAIVE) print(f"\n GRIGLIA DICHIARATA: {len(SZ)} size per-trade + {len(VTL)} vol-target di gamba + " f"{len(ACT)} filtro-attivita' + {len(BVT)} vol-target di libro + {len(NAIVE)} controlli " f"non-causali = {len(ALL)} celle (+ baseline).") # ------------------------------------------------------------------ serie TP = A.tp01_baseline_daily() SLE = {"BASE": {}} BK = {"BASE": {}} MTP = {"BASE": {o: 1.0 for o in offs}} # rapporto di vol della gamba TP01 SIZEMED = {} for o in offs: SLE["BASE"][o] = leg_daily(EX[o], {a: size_flat(EX[o][a]) for a in ASSETS}) BK["BASE"][o] = book(TP, SLE["BASE"][o]) for nm, fn in SZ.items(): SLE[nm] = {o: leg_daily(EX[o], {a: fn(EX[o][a]) for a in ASSETS}) for o in offs} BK[nm] = {o: book(TP, SLE[nm][o]) for o in offs} MTP[nm] = {o: 1.0 for o in offs} SIZEMED[nm] = float(np.median(np.concatenate([fn(EX[offs[0]][a]) for a in ASSETS]))) for nm, sp in VTL.items(): SLE[nm], BK[nm], MTP[nm] = {}, {}, {} for o in offs: sz = {} for a in ASSETS: sz[a] = sizes_from_L(EX[o][a], leverage_series(EX[o][a]["base_daily"], **sp)) SLE[nm][o] = leg_daily(EX[o], sz) BK[nm][o] = book(TP, SLE[nm][o]) MTP[nm][o] = 1.0 if o == offs[0]: SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values())))) for nm, sp in ACT.items(): SLE[nm], BK[nm], MTP[nm] = {}, {}, {} for o in offs: sz = {a: sizes_from_L(EX[o][a], activity_L(EX[o][a]["base_daily"], **sp)) for a in ASSETS} SLE[nm][o] = leg_daily(EX[o], sz) BK[nm][o] = book(TP, SLE[nm][o]) MTP[nm][o] = 1.0 if o == offs[0]: SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values())))) for nm, sp in BVT.items(): SLE[nm], BK[nm], MTP[nm] = {}, {}, {} for o in offs: L = leverage_series(BK["BASE"][o], sp["tv"], sp["w"], sp["cap"]) sz = {a: sizes_from_L(EX[o][a], L) for a in ASSETS} SLE[nm][o] = leg_daily(EX[o], sz) tp_s = TP * L.reindex(TP.index).ffill().fillna(1.0) BK[nm][o] = book(tp_s, SLE[nm][o]) MTP[nm][o] = vol(tp_s) / vol(TP) if o == offs[0]: SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values())))) for nm, sp in NAIVE.items(): # controllo NON causale (vedi §4) SLE[nm] = {o: SLE["BASE"][o] * leverage_series(SLE["BASE"][o], **sp) for o in offs} BK[nm] = {o: book(TP, SLE[nm][o]) for o in offs} MTP[nm] = {o: 1.0 for o in offs} SIZEMED[nm] = float(np.median(leverage_series(SLE["BASE"][offs[0]], **sp).values)) # CONTROLLO ISO-PESO: ogni variante che alza la vol della gamba SKH alza il suo peso EFFETTIVO # nel libro — e "alzare il peso di SKH01" e' gia' stato misurato (26/07: argmax w=0.35, ma # `weights_tilt_null` FALLITO e proposta respinta). Quindi il confronto che conta NON e' vs il # baseline nudo ma vs il baseline RI-SCALATO agli stessi rapporti di vol: cosi' resta solo la # FORMA (quale trade pesa quanto). E' la regola dell'ISO-RISCHIO gia' codificata (25/07 §3). CTRL, WEFF = {}, {} for nm in ALL: CTRL[nm], WEFF[nm] = {}, {} for o in offs: m_s = vol(SLE[nm][o]) / vol(SLE["BASE"][o]) if vol(SLE["BASE"][o]) > 0 else 1.0 m_t = MTP[nm][o] CTRL[nm][o] = book(TP * m_t, SLE["BASE"][o] * m_s) WEFF[nm][o] = W_SKH * m_s / (W_TP * m_t + W_SKH * m_s) # ------------------------------------------------------------------ §2 sleeve can = offs[0] print("\n" + "-" * 112) print(f" 2. SLEEVE SKH01 all'ancora canonica (off {can}). 'DDisovol'/'CAGRiso' a vol pareggiata") print(" col baseline (post-hoc, dichiarato: lo Sharpe non ne dipende).") print("-" * 112) print(HEAD) base_s = SLE["BASE"][can] def prow(nm, s, med=None): iv = iso_vol(s, base_s) m = f"{med:>10.2f}" if med is not None else f"{'-':>10}" print(f" {nm:<28}{sh(s):>8.3f}{sh(hold(s)):>9.3f}{sh(ins(s)):>8.3f}{vol(s)*100:>8.1f}%" f"{maxdd(s)*100:>8.1f}%{maxdd(iv)*100:>9.1f}%{cagr(iv)*100:>8.1f}%{m}") prow("BASELINE (size fissa)", base_s, 1.00) for nm in list(SZ) + list(VTL) + list(ACT) + list(NAIVE): prow(nm, SLE[nm][can], SIZEMED.get(nm)) # ------------------------------------------------------------------ §3 libro, banda appaiata print("\n" + "-" * 112) print(f" 3. LIBRO 75/25 — mediana delle DIFFERENZE APPAIATE su {len(offs)} ancore, + null del") print(" de-levering (k<1 sul BASELINE che pareggia il maxDD della variante)") print("-" * 112) print(f" {'variante':<28}{'wSKH eff':>9}{'dShFULL':>9}{'pos/n':>8}{'dShHOLD':>9}{'pos/n':>8}" f"{'| ISO dF':>10}{'pos/n':>8}{'ISO dH':>9}{'pos/n':>8}{'dDDpp':>8}{'k_iso':>8}" f"{'null de-lev':>14}") base_b = BK["BASE"] RES = {} for nm in ALL: dF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o: base_b[o], offs, metric=sh) dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]), lambda o: hold(base_b[o]), offs, metric=sh) iF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o, n=nm: CTRL[n][o], offs, metric=sh) iH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]), lambda o, n=nm: hold(CTRL[n][o]), offs, metric=sh) dd = float(np.median([maxdd(BK[nm][o]) - maxdd(base_b[o]) for o in offs])) * 100 if maxdd(BK[nm][can]) < maxdd(base_b[can]) - 1e-6: k = k_iso_dd(base_b[can], BK[nm][can]) shk = sh(k * base_b[can]) ks = f"{k:.3f}" vd = "DE-LEVERING" if shk >= sh(BK[nm][can]) else "oltre-null" else: ks, vd = "n/a", "DD peggiore" we = float(np.median([WEFF[nm][o] for o in offs])) RES[nm] = dict(dF=dF["median_paired"], nF=dF["n_positive"], dH=dH["median_paired"], nH=dH["n_positive"], iF=iF["median_paired"], niF=iF["n_positive"], iH=iH["median_paired"], niH=iH["n_positive"], dd=dd, k=ks, verdict=vd, weff=we) print(f" {nm:<28}{we:>9.3f}{dF['median_paired']:>+9.3f}{dF['n_positive']:>5}/{len(offs):<3}" f"{dH['median_paired']:>+9.3f}{dH['n_positive']:>5}/{len(offs):<3}" f"{iF['median_paired']:>+10.3f}{iF['n_positive']:>5}/{len(offs):<3}" f"{iH['median_paired']:>+9.3f}{iH['n_positive']:>5}/{len(offs):<3}{dd:>+8.2f}{ks:>8}" f"{vd:>14}") print(" ⚠️ NOTA sul null del de-levering: `sh(k*base)` e' IDENTICO a `sh(base)` perche' lo") print(" Sharpe e' invariante a una costante -> in questa forma il null COINCIDE col confronto") print(" degli Sharpe, ed e' per questo che qui lo Sharpe e' la metrica che decide e il DD no.") print(" 'k_iso' resta come diagnostica: quanta leva dovrebbe cedere il baseline per pareggiare") print(" il maxDD della variante. 'DE-LEVERING' = la variante NON batte quel baseline ridotto.") print(" wSKH eff = peso EFFETTIVO di SKH nel libro (i nominali restano 75/25: la size lo") print(" muove). Colonne 'ISO' = confronto vs il baseline RI-SCALATO a quello stesso peso:") print(" e' li' che si legge la FORMA. Le colonne non-ISO contengono anche il cambio di peso.") med_F = float(np.median([sh(base_b[o]) for o in offs])) med_H = float(np.median([sh(hold(base_b[o])) for o in offs])) med_D = float(np.median([maxdd(base_b[o]) for o in offs])) print(f"\n libro BASE, ancora canonica: ShFULL {sh(base_b[can]):.3f} / ShHOLD " f"{sh(hold(base_b[can])):.3f} / maxDD {maxdd(base_b[can])*100:.1f}% / " f"CAGR {cagr(base_b[can])*100:.1f}%") print(f" libro BASE, stima ONESTA (mediana della banda): ShFULL {med_F:.3f} / " f"ShHOLD {med_H:.3f} / maxDD {med_D*100:.1f}%") # ------------------------------------------------------------------ §4 il difetto misurato print("\n" + "-" * 112) print(" 4. IL DIFETTO MISURATO — vol-target NAIVE (scala il giorno di CHIUSURA) vs CAUSALE") print(" (size fissata all'INGRESSO). Stessa formula di L, stessa finestra: cambia solo") print(" QUANDO la si applica. La differenza e' il valore del look-ahead di contabilita'.") print("-" * 112) print(f" {'coppia':<34}{'dShFULL naive':>15}{'dShFULL causale':>17}{'phantom':>10}") for w in (30, 90): n_nm, c_nm = f"NAIVE-VT w{w}", f"VTL tv20 w{w}" a1, a2 = RES[n_nm]["dF"], RES[c_nm]["dF"] print(f" {'vol-target tv20 w%d' % w:<34}{a1:>+15.3f}{a2:>+17.3f}{a1-a2:>+10.3f}") for w in (30, 90): n_nm, c_nm = f"NAIVE-VT w{w}", f"VTL tv20 w{w}" a1, a2 = RES[n_nm]["dH"], RES[c_nm]["dH"] print(f" {' (hold-out) tv20 w%d' % w:<34}{a1:>+15.3f}{a2:>+17.3f}{a1-a2:>+10.3f}") # ------------------------------------------------------------------ §5 selezione + DSR print("\n" + "-" * 112) print(" 5. SELEZIONE AL BUIO (solo pre-2025, mediana di banda) + deflated-Sharpe") print("-" * 112) CAUSAL = [n for n in ALL if n not in NAIVE] is_med = {n: float(np.median([sh(ins(BK[n][o])) for o in offs])) for n in CAUSAL} ho_med = {n: float(np.median([sh(hold(BK[n][o])) for o in offs])) for n in CAUSAL} fu_med = {n: float(np.median([sh(BK[n][o]) for o in offs])) for n in CAUSAL} base_is = float(np.median([sh(ins(base_b[o])) for o in offs])) print(f" baseline in-sample (mediana banda): {base_is:.3f}") for n in sorted(CAUSAL, key=lambda x: -is_med[x])[:6]: print(f" {n:<28} IS {is_med[n]:>7.3f} HOLD {ho_med[n]:>7.3f} FULL {fu_med[n]:>7.3f}") best = max(CAUSAL, key=lambda x: is_med[x]) print(f"\n cella scelta AL BUIO: {best} (IS {is_med[best]:.3f} vs baseline {base_is:.3f})") iso_is = {n: float(np.median([sh(ins(BK[n][o])) - sh(ins(CTRL[n][o])) for o in offs])) for n in CAUSAL} best_iso = max(CAUSAL, key=lambda x: iso_is[x]) print(f" cella scelta AL BUIO **a ISO-PESO**: {best_iso} (dIS iso {iso_is[best_iso]:+.3f})") print(f" la stessa cella a iso-peso: dShFULL {RES[best]['iF']:+.3f} " f"({RES[best]['niF']}/{len(offs)}), dShHOLD {RES[best]['iH']:+.3f} " f"({RES[best]['niH']}/{len(offs)})") trials = [fu_med[n] for n in CAUSAL] + [med_F] dsr, sr0 = A.deflated_sharpe(fu_med[best], trials, BK[best][can]) dsr_b, _ = A.deflated_sharpe(med_F, trials, base_b[can]) print(f" DSR candidato {dsr:.3f} / DSR BASELINE {dsr_b:.3f} (null max-Sharpe {sr0:.3f}, " f"N={len(trials)})") print(f" dispersione degli {len(trials)} trial: sd {np.std(trials, ddof=1):.4f} -> " f"⚠️ il DSR e' QUASI VACUO su una famiglia di perturbazioni della stessa strategia") print(" (penalizza in proporzione alla varianza fra i trial: qui il baseline stesso lo") print(" passa). Il gate che decide e' la banda appaiata del §3, non questo.") # ------------------------------------------------------------------ §6 ancora TP01 print("\n" + "-" * 112) print(" 6. CONTROLLO — l'ancora di TP01 e' tenuta fissa: il segno del Delta dipende da lei?") print("-" * 112) cand6 = [best] + [n for n in (list(SZ)[:0] + ["RISK p1.0"] + list(BVT)[:2]) if n != best] print(f" {'variante':<28}" + "".join(f"{'TP h=%d' % h:>12}" for h in (0, 8, 16))) for nm in cand6: cells = [] for h in (0, 8, 16): tph = TO.port_daily_native(h) if h else TP if nm in BVT: arm = {} for o in offs: L = leverage_series(book(tph, SLE["BASE"][o]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) sz = {a: sizes_from_L(EX[o][a], L) for a in ASSETS} arm[o] = book(tph * L.reindex(tph.index).ffill().fillna(1.0), leg_daily(EX[o], sz)) else: arm = {o: book(tph, SLE[nm][o]) for o in offs} bse = {o: book(tph, SLE["BASE"][o]) for o in offs} d = A.anchor_luck_delta(lambda o, x=arm: x[o], lambda o, y=bse: y[o], offs, metric=sh) cells.append(d["median_paired"]) print(f" {nm:<28}" + "".join(f"{v:>+12.3f}" for v in cells)) # ------------------------------------------------------------------ §7 gate print("\n" + "-" * 112) print(" 7. GATE — marginal_vs_tp01 / implausible_sharpe / weights_tilt_null") print("-" * 112) best_sleeve = max([n for n in list(SZ) + list(VTL) + list(ACT)], key=lambda x: is_med[x]) for nm in ("BASE", best_sleeve): m = A.marginal_vs_tp01(SLE[nm][can]) b25 = m.get("blends", {}).get("w25", {}) print(f" marginal {nm:<26} {m.get('marginal_verdict')} corr {m.get('corr_full')} " f"uplift w25 full {b25.get('uplift_full')} / hold {b25.get('uplift_hold')}") nlos = int((EX[can]["BTC"]["net"] < 0).sum() + (EX[can]["ETH"]["net"] < 0).sum()) ntot = int(len(EX[can]["BTC"]["net"]) + len(EX[can]["ETH"]["net"])) for nm in ("BASE", best_sleeve): imp = A.implausible_sharpe(SLE[nm][can], n_trades=ntot, n_losing_trades=nlos) print(f" implausible {nm:<25} implausible={imp['implausible']} " f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive " f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}") cols = {"TP01": TP, "SKH01": SLE["BASE"][can]} for nm in dict.fromkeys([best, best_sleeve] + list(BVT)[:2]): m_s = vol(SLE[nm][can]) / vol(SLE["BASE"][can]) wp = {"TP01": W_TP * MTP[nm][can], "SKH01": W_SKH * m_s} g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp, caps={"SKH01": 0.5}, floor=0.05, n=300, k_seen=len(ALL)) print(f" tilt-null {nm:<26} peso equiv SKH {wp['SKH01']/sum(wp.values()):.3f} " f"d_IS {g['delta_insample']:+.4f} d_HOLD {g['delta_hold']:+.4f} " f"pctl {g['pctl_hold']:.1f} gate_pass={g['gate_pass']}") print(" LIMITE DICHIARATO: il gate confronta vettori di pesi STATICI; della modulazione") print(" cattura la sola componente di SCALA. La FORMA la giudica la banda appaiata (§3).") # ------------------------------------------------------------------ §8 eseguibilita' print("\n" + "-" * 112) print(f" 8. ESEGUIBILITA' a ${CAPITAL:.0f} — min-order ${MIN_ORDER:.0f}, cap {CAP_ASSET:.0%}" f" equity/asset, target NETTATO 0.75*TP01 + 0.25*SKH sul grid 1h (cio' che il cron manda)") print("-" * 112) print(f" {'configurazione':<28}{'Sh model':>10}{'Sh reale':>10}{'haircut':>9}" f"{'ordini eseg':>13}{'sotto-min':>11}{'turnover/a':>12}") todo = [("BASE (size fissa)", None), (best_sleeve, best_sleeve), (list(BVT)[0], list(BVT)[0])] for label, nm in todo: tm = tr = tu = 0.0 no = ns = 0 for a in ASSETS: df1h = TO.get1h(a) tgt = netted_target(a, EX[can][a], nm, SZ, VTL, ACT, BVT, SLE, TP, can, df1h) ev = A.eval_weights_smallcap(df1h, tgt, capital=CAPITAL, min_order=MIN_ORDER) tm += 0.5 * ev["modeled"]["sharpe"] tr += 0.5 * ev["realistic"]["sharpe"] tu += ev["executed_turnover_per_year"] no += ev["n_executed_trades"] dw = np.abs(np.diff(np.nan_to_num(tgt), prepend=0.0)) ns += int(((dw > 1e-12) & (dw * CAPITAL < MIN_ORDER)).sum()) print(f" {label:<28}{tm:>10.3f}{tr:>10.3f}{tm-tr:>9.3f}{no:>13}{ns:>11}{tu:>12.1f}") print(" 'ordini eseg' e 'sotto-min' sono sui due asset sommati, su tutta la storia 1h.") # ------------------------------------------------------------------ §9 per anno + deriva print("\n" + "-" * 112) print(" 9. SCOMPOSIZIONE PER ANNO del dSharpe a ISO-PESO (regola codificata dopo SOL, 22/08:") print(" un contributo positivo si scompone per ANNO prima di crederci) + deriva della size") print("-" * 112) yrs = sorted({int(y) for y in BK["BASE"][can].index.year}) best_iv = max([n for n in SZ if n.startswith("IV")], key=lambda x: RES[x]["iF"]) best_rk = max([n for n in SZ if n.startswith("RISK")], key=lambda x: RES[x]["iF"]) top = list(dict.fromkeys([best, best_iso, best_iv, best_rk, max(ACT, key=lambda x: RES[x]["iF"]), max(BVT, key=lambda x: RES[x]["iF"])])) print(f" {'variante':<28}" + "".join(f"{y:>8}" for y in yrs)) for nm in top: cells = [] for y in yrs: v = [] for o in offs: a_ = BK[nm][o][BK[nm][o].index.year == y] c_ = CTRL[nm][o][CTRL[nm][o].index.year == y] if len(a_) > 20: v.append(sh(a_) - sh(c_)) cells.append(float(np.median(v)) if v else float("nan")) npos = int(np.nansum(np.asarray(cells) > 0)) print(f" {nm:<28}" + "".join(f"{c:>+8.2f}" for c in cells) + f" anni positivi {npos}/{len(yrs)}") print() for nm in top: Lb = (leverage_series(BK["BASE"][can], BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) if nm in BVT else None) szs = np.concatenate([sizes_of(nm, EX[can][a], SZ, VTL, ACT, Lb) for a in ASSETS]) dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS])) s = pd.Series(szs, index=dys) med = s.groupby(s.index.year).median() rho = float(np.corrcoef(np.arange(len(med)), med.values)[0, 1]) if len(med) > 2 else float("nan") print(f" size mediana per anno — {nm:<22}" + "".join(f"{med.get(y, float('nan')):>8.2f}" for y in yrs) + f" corr col tempo {rho:+.2f}") print(" ⚠️ una size che DERIVA col tempo non e' controllo del rischio: e' un tilt temporale") print(" (la vol crypto e' scesa lungo il campione -> qualunque 1/vol pesa di piu' il") print(" periodo recente, che e' anche l'hold-out). La riga 'corr col tempo' la misura.") # noqa print("\n" + "-" * 112) print(" 10. NULL DI PERMUTAZIONE DELLA SIZE — la stessa MULTINSIEME di size, riassegnata a") print(" caso ai trade. Se il vantaggio a iso-peso sopravvive al null, conta QUALE trade") print(" riceve quale size (informazione); se no, e' solo la DISTRIBUZIONE delle size.") print("-" * 112) rng = np.random.default_rng(20260822) a10 = list(offs[:: max(1, len(offs) // 3)])[:3] NDRAW = 150 for nm in dict.fromkeys([best_iso, best_sleeve, best_iv, best_rk]): real = float(np.median([sh(BK[nm][o]) - sh(CTRL[nm][o]) for o in a10])) sz0 = {o: {a: sizes_of(nm, EX[o][a], SZ, VTL, ACT) for a in ASSETS} for o in a10} nul = [] for _ in range(NDRAW): v = [] for o in a10: szp = {a: rng.permutation(sz0[o][a]) for a in ASSETS} lg = leg_daily(EX[o], szp) m_s = vol(lg) / vol(SLE["BASE"][o]) if vol(SLE["BASE"][o]) > 0 else 1.0 v.append(sh(book(TP, lg)) - sh(book(TP, SLE["BASE"][o] * m_s))) nul.append(float(np.median(v))) nul = np.asarray(nul) pc = float((nul < real).mean()) * 100 print(f" {nm:<26} reale {real:+.3f} null mediana {np.median(nul):+.3f} " f"p90 {np.percentile(nul, 90):+.3f} max {nul.max():+.3f} " f"-> percentile {pc:.1f} (p ~ {(1 - pc / 100):.3f})") print(f" {NDRAW} permutazioni x {len(a10)} ancore {a10}; seme fisso 20260822.") print("\n" + "=" * 112) print(f" fatto in {time.time()-t0:.0f}s") print("=" * 112) def netted_target(asset: str, ex: dict, nm, SZ, VTL, ACT, BVT, SLE, TP, can, df1h) -> np.ndarray: """Peso NETTO per asset sul grid 1h: 0.75*target TP01 + 0.25*posizione SKH (dir*size), poi l'eventuale leva di libro sulla gamba TP01. E' cio' che il cron manda a Deribit come UNA posizione netta sullo stesso strumento.""" tp = np.asarray(TO.hourly_target(asset, 0), float) ts1 = df1h["timestamp"].values.astype(np.int64) + 3_600_000 pos = np.zeros(len(ts1)) L1h = np.ones(len(ts1)) if nm is not None and nm in BVT: # BOOKVT: leva anche sulla gamba TP01 L = leverage_series(book(TP, SLE["BASE"][can]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) sizes = sizes_of(nm, ex, SZ, VTL, ACT, L) idx1h = pd.DatetimeIndex(pd.to_datetime(df1h["timestamp"].values, unit="ms", utc=True)) L1h = L.reindex(idx1h.floor("D")).ffill().fillna(1.0).values else: sizes = sizes_of(nm, ex, SZ, VTL, ACT) t_in, t_out = ex["ts_close"][ex["i_ent"]], ex["ts_close"][ex["i_ex"]] for k in range(len(t_in)): j0 = int(np.searchsorted(ts1, t_in[k], side="left")) j1 = int(np.searchsorted(ts1, t_out[k], side="left")) pos[j0:j1] = ex["dir"][k] * sizes[k] return np.clip(W_TP * tp * L1h + W_SKH * pos, -CAP_ASSET, CAP_ASSET) if __name__ == "__main__": main()