842420ce7b
Il follow-up "book sul path live" era fermo da tre sessioni su questa premessa:
"il simulatore compone per-trade a nozionale unitario, LO SLEEVE E' VOL-TARGETED".
LA PREMESSA ERA FALSA. Verificato in tre modi: _skyhook_returns chiama backtest_signals
con leverage=1.0/position_size=1.0; nessun target_vol/vol_target/realized_vol nel
sorgente; sim_equity(canonical) riproduce backtest_signals a max|diff| = 0.0. Il 20.7%
di vol realizzata dello sleeve e' un PRODOTTO della strategia (uscite % asimmetriche +
poco tempo a mercato), non un parametro: SKH01 e' l'unica delle 5 a NON essere
vol-targeted, il contrario di quanto si credeva. Test permanente cablato.
MISURA (ingressi live vs backtest, de-luckata su 8 offset a priori; sanity 120/120 bin
con ingresso ricostruiti):
- il numero del 26/07 era ~4x troppo grande: like-with-like (mediana PER-ASSET) +0.38 su
3 offset -> +0.097 su 8, e "6/6 non negativi" -> 13/16. Sleeve 50/50: +0.112, 8/8.
- a livello di BOOK lo Sharpe e' una monetina (FULL +0.048, HOLD +0.051) ma il DRIFT e'
+0.73pp positivo nel 100% delle estrazioni: l'ingresso intra-bin prende un prezzo
migliore, i falsi ingressi aggiungono churn, vol e ritorno salgono insieme.
IL FATTORE x0.6 DECOMPOSTO E MISURATO:
(a) fortuna d'ancora sul DRIFT: x0.874 (5-sleeve) / x0.890 (book live). La vol e'
invariata fra le ancore (7.80->7.76%): la fortuna sta tutta nel drift.
(b) path live: NON-NEGATIVO ovunque (uscite +0.081 Sh, ingressi +0.73pp, TP01 ~0).
Il x0.6 implicava un residuo x0.687 attribuito al live oltre l'ancora, che nessuna misura
sostiene -> FATTORE ONESTO x0.87-0.91, troppo severo del 31-34%. Il sospetto registrato
il 25/07 ("conta due volte la degradazione SKH01") e' confermato quantitativamente.
MURI DI CAPITALE ricalcolati con la stessa macchineria del 25/07: perpetua 6.00% ->
10.91%, muro per 50 EUR/g $494.758 -> $272.061 (-45%) a leva 1.0. La conclusione
STRUTTURALE non cambia: $272k restano ~453x il conto di oggi.
IPOTESI MIA REFUTATA nella stessa sessione: che la grid timing-luck di SKH01 fosse un
artefatto della lente a chiusura-di-bin. Dispersione LIVE/CANONICO = 1.59x -> il live e'
PIU' disperso. L'audit del 02/07 resta valido com'e'. (Nata su 2 offset, chiusa a 8.)
Trovato per strada: simulate() in r0726_skh_partial_entry.py non era mai chiamata da
main() — i numeri headline di quel diario venivano da una corsa mai committata.
Book, pesi, cron, config INVARIATI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
132 lines
6.1 KiB
Python
132 lines
6.1 KiB
Python
#!/usr/bin/env python
|
||
"""r0726_deluck_factor.py — DECOMPOSIZIONE del fattore de-luck ×0.6 del 25/07.
|
||
|
||
IL PROBLEMA. `r0725_capcurve.py` applica `b = b - 0.4*mean(b)` ("de-luck ×0.6": taglia il 40% del
|
||
DRIFT, lascia la vol) sopra un book gia' calcolato con SKH01 sulla lente `hourly`. Il ×0.6 fu
|
||
scelto **a occhio**, e CLAUDE.md registrava gia' il sospetto: *"il ×0.6 sopra il path live rischia
|
||
di contare DUE VOLTE la degradazione SKH01"*.
|
||
|
||
Il fattore mescola due cose che ora si possono separare e misurare:
|
||
(a) **fortuna d'ancora** — i numeri canonici sono un punto alto dello spazio d'ancora
|
||
(misurato il 26/07 sul Sharpe: FULL 97.0 pctl, HOLD 99.6 pctl). Qui serve sul **DRIFT**,
|
||
perche' e' il drift che il ×0.6 taglia.
|
||
(b) **degradazione del path live** — che per SKH01 e' misurata **NEGATIVA** (il live e' MEGLIO
|
||
del backtest su entrambi i lati: uscite +0.081 di Sharpe FULL di book su 23/23 offset,
|
||
ingressi +0.38 di Sharpe di sleeve) e per TP01 e' ~zero (±0.03 su 24 ancore).
|
||
|
||
Questo script misura (a) e la riporta accanto a (b), per due book:
|
||
* il book ATTIVO 5-sleeve (33/15/12/20/20)
|
||
* il book LIVE Deribit (TP01/SKH01 75/25) — quello che gira davvero
|
||
|
||
METODO: identico a `r0726_loo_deluck.py` (stesse serie d'ancora, stesse estrazioni congiunte
|
||
uniformi indipendenti), ma la metrica e' il **drift annualizzato** (media giornaliera x 365.25) e
|
||
il CAGR, non il Sharpe. Il fattore onesto e' `mediana / canonico` sul drift.
|
||
|
||
NON decide da solo il valore da usare: la componente (b) sugli INGRESSI di SKH01 e' in corso di
|
||
misura de-luckata (`r0726_skh_sigcache.py` + `r0726_skh_live_book.py`). Qui si stabilisce quanto
|
||
del ×0.6 e' spiegato dall'ancora — cioe' quanto ne resta da attribuire al live.
|
||
|
||
uv run python scripts/research/r0726_deluck_factor.py [--draws N]
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import numpy as np
|
||
|
||
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 r0726_loo_deluck as L # noqa: E402
|
||
from src.portfolio.portfolio import HOLDOUT, combine_outer, metrics # noqa: E402
|
||
|
||
DPY = 365.25
|
||
|
||
BOOKS = {
|
||
"book ATTIVO 5-sleeve (33/15/12/20/20)": L.WEIGHTS,
|
||
"book LIVE Deribit (TP01/SKH01 75/25)": {"TP01_trend_1d": 0.75, "SKH01_skyhook": 0.25},
|
||
}
|
||
|
||
|
||
def drift_and_cagr(cols: dict, weights: dict) -> tuple[float, float, float]:
|
||
"""(drift annualizzato, CAGR, vol annualizzata) del book a quei pesi."""
|
||
w = {k: weights[k] for k in cols}
|
||
s = combine_outer(cols, w)
|
||
r = np.asarray(s.dropna().values, float)
|
||
m = metrics(s)
|
||
return float(r.mean() * DPY), float(m["cagr"]), float(r.std() * np.sqrt(DPY))
|
||
|
||
|
||
def main() -> None:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--draws", type=int, default=2000)
|
||
args = ap.parse_args()
|
||
|
||
print("=" * 100)
|
||
print(" r0726 — DECOMPOSIZIONE del fattore de-luck ×0.6 (25/07)")
|
||
print("=" * 100)
|
||
print(" Il ×0.6 taglia il DRIFT del book (b - 0.4*mean(b)). Quanto di quel taglio e'")
|
||
print(" spiegato dalla FORTUNA D'ANCORA, e quanto resterebbe da attribuire al path live?")
|
||
|
||
S = L.build_all()
|
||
rng = np.random.default_rng(L.SEED)
|
||
cfgs = [{n: int(rng.choice(L.SPEC[n][1])) for n in L.SPEC} for _ in range(args.draws)]
|
||
canon_cfg = {n: L.SPEC[n][2] for n in L.SPEC}
|
||
|
||
for label, W in BOOKS.items():
|
||
names = list(W)
|
||
print("\n" + "=" * 100)
|
||
print(f" {label}")
|
||
print("=" * 100)
|
||
|
||
def cols_at(cfg):
|
||
return {n: S[n][cfg[n]] for n in names}
|
||
|
||
d_can, c_can, v_can = drift_and_cagr(cols_at(canon_cfg), W)
|
||
rows = np.array([drift_and_cagr(cols_at(c), W) for c in cfgs], float)
|
||
d_med, c_med, v_med = np.median(rows, axis=0)
|
||
d_lo, d_hi = np.percentile(rows[:, 0], [10, 90])
|
||
c_lo, c_hi = np.percentile(rows[:, 1], [10, 90])
|
||
|
||
n_anch = int(np.prod([len(L.SPEC[n][1]) for n in names]))
|
||
print(f" spazio d'ancora di questo book: {n_anch:,} configurazioni "
|
||
f"({' x '.join(str(len(L.SPEC[n][1])) for n in names)})")
|
||
print(f"\n {'':<24}{'canonico':>11}{'pctl':>8}{'MEDIANA':>11}{'p10':>10}{'p90':>10}")
|
||
print(f" {'drift annualizzato':<24}{d_can*100:10.2f}%"
|
||
f"{float((rows[:, 0] < d_can).mean()*100):7.1f}%{d_med*100:10.2f}%"
|
||
f"{d_lo*100:9.2f}%{d_hi*100:9.2f}%")
|
||
print(f" {'CAGR':<24}{c_can*100:10.2f}%"
|
||
f"{float((rows[:, 1] < c_can).mean()*100):7.1f}%{c_med*100:10.2f}%"
|
||
f"{c_lo*100:9.2f}%{c_hi*100:9.2f}%")
|
||
print(f" {'vol annualizzata':<24}{v_can*100:10.2f}%{'':>8}{v_med*100:10.2f}%")
|
||
|
||
f_drift = d_med / d_can if d_can else float("nan")
|
||
f_cagr = c_med / c_can if c_can else float("nan")
|
||
print(f"\n >>> FATTORE D'ANCORA sul drift : ×{f_drift:.3f} (il ×0.6 ne assume ×0.600)")
|
||
print(f" >>> FATTORE D'ANCORA sul CAGR : ×{f_cagr:.3f}")
|
||
residuo = 0.6 / f_drift if f_drift else float("nan")
|
||
print(f" >>> residuo implicito da attribuire al PATH LIVE: ×{residuo:.3f}")
|
||
if residuo < 1.0:
|
||
print(f" cioe' il ×0.6 assume che il live tolga un altro "
|
||
f"{(1-residuo)*100:.0f}% di drift OLTRE l'ancora.")
|
||
else:
|
||
print(f" cioe' il ×0.6 e' CONSERVATIVO anche prima di contare il live.")
|
||
|
||
print("\n" + "=" * 100)
|
||
print(" COMPONENTE (b) — degradazione del path live, gia' misurata")
|
||
print("=" * 100)
|
||
print(" SKH01 uscite : +0.081 Sharpe FULL di book, 23/23 offset (26/07, banda appaiata)")
|
||
print(" SKH01 ingressi: +0.38 Sharpe di sleeve, mediana su 3 offset x 2 asset (26/07)")
|
||
print(" -> in corso la versione de-luckata a 8 offset, a livello di BOOK")
|
||
print(" TP01 barra parziale: -0.031 FULL / +0.118 HOLD su 24 ancore = trascurabile")
|
||
print(" => il segno di (b) e' NON-NEGATIVO su tutto cio' che e' stato misurato.")
|
||
print(" Un fattore <1 per il live non e' sostenuto da nessuna misura del progetto.")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|