research(wave-0822): GROWTH-POLICY — il libro gira al 7% di Kelly, e ogni gate del progetto e' cieco alla scala

This commit is contained in:
Adriano Dal Pastro
2026-08-22 17:10:03 +00:00
parent 3b206c8e6d
commit 14c1a75a7e
8 changed files with 275 additions and 89 deletions
+37 -8
View File
@@ -254,7 +254,8 @@ def sezione2(snap: pd.DataFrame) -> pd.DataFrame:
g["p_x24"] = [bboot_p(d[d.odec == k].x24.values, block=48) for k in g.index]
print(g.round(3).to_string())
print(" av4/av24 = |ret| futuro medio: e' la gamba 'squeeze/cascata' dell'ipotesi")
print(" (piu' vol dopo un estremo di OI). E' PIATTA: nessuna cascata prevedibile.")
print(" (piu' vol dopo un estremo di OI). E' QUASI PIATTA (0.59-0.75%% a 4h, solo il decile 0")
print(" leggermente sopra): la 'cascata' prevedibile non c'e'.")
print("\n--- CONTROLLO: e' solo 'il prezzo si e' appena mosso'? ---")
z = lambda s: (s - s.mean()) / s.std() # noqa: E731
@@ -293,9 +294,25 @@ def sezione2(snap: pd.DataFrame) -> pd.DataFrame:
for a in ASSETS:
sub = d[(d.asset == a) & (d.odec == 0)]
cond = (d[d.asset == a].odec == 0).values
ep = int(np.sum(cond[1:] & ~cond[:-1])) + int(cond[0])
print(f" {a}: ore in decile0={len(sub)} episodi distinti={ep} "
# AMPIEZZA EFFETTIVA: il numero di ORE non e' la taglia campionaria se la
# condizione e' fatta di pochi episodi lunghi (lezione del filone gemello
# dealer-gamma della stessa ondata: 73 "episodi" di cui 6-8 all'80% del tempo).
lens, cur = [], 0
for v in cond:
if v:
cur += 1
elif cur:
lens.append(cur); cur = 0
if cur:
lens.append(cur)
L = np.array(sorted(lens)[::-1])
print(f" {a}: ore in decile0={len(sub)} episodi distinti={len(L)} "
f"durata mediana={np.median(L):.1f}h max={L.max()}h "
f"quota ore negli 8 episodi piu' lunghi={L[:8].sum()/L.sum():.2f} "
f"y4={100*sub.y4.mean():+.3f}% y24={100*sub.y24.mean():+.3f}%")
print(" -> QUI l'ampiezza NON e' la patologia: 86-87 episodi corti (mediana 2h) e i")
print(" primi 8 coprono solo il 20-25% delle ore. Il vincolo binding e' la")
print(" LUNGHEZZA DEL CALENDARIO (90 giorni), non la concentrazione degli episodi.")
print("\n scomposizione PER MESE (demean mensile) — il test che il progetto impone")
print(" dopo il caso SOL ('un contributo positivo si scompone prima di crederci'):")
tab = d[d.odec == 0].groupby("mese")[["y4", "y24"]].agg(["size", "mean"])
@@ -475,6 +492,9 @@ def sezione4(snap: pd.DataFrame) -> None:
print(" -> il candidato NON e' la statica travestita: il timing porta qualcosa.")
print("\n--- NULL 2: timing CASUALE a pari esposizione (shift circolare, 300 estrazioni) ---")
print(" e' il null LOCATION-MATCHED nella dimensione del tempo: stessa esposizione, stessa")
print(" struttura di episodi, stessa finestra — cambia SOLO QUANDO. Un null piu' ingenuo")
print(" (posizione casuale iid) direbbe di si' a qualunque cosa in una finestra con drift.")
base_trig = {}
for a in ASSETS:
df = A.get(a, "1h")
@@ -504,12 +524,19 @@ def sezione4(snap: pd.DataFrame) -> None:
sr = float(cd.mean() / cd.std() * np.sqrt(365.25))
rep = A.marginal_vs_tp01(cd)
print(f" candidate_daily : n={len(cd)} giorni, Sharpe={sr:.2f}")
n_hold = int((cd.index >= A.HOLDOUT).sum())
print(f" marginal_vs_tp01 : verdetto={rep.get('marginal_verdict')} "
f"corr_beta_tp01={rep.get('beta_to_tp01')} has_insample_edge={rep.get('has_insample_edge')} "
f"beta_to_tp01={rep.get('beta_to_tp01')} has_insample_edge={rep.get('has_insample_edge')} "
f"is_hedge={rep.get('is_hedge')} robust_oos={rep.get('robust_oos')} "
f"beats_noise_null={rep.get('beats_noise_null')} null_pctl_full={rep.get('null_pctl_full')}")
print(" (robust_oos=False non e' un difetto del candidato: con 90 giorni NON ESISTE il")
print(" multi-cut che il gate richiede. Il gate e' girato e il suo esito e' NEUTRAL.)")
print(f" ATTENZIONE: giorni totali={len(cd)}, giorni dopo HOLDOUT({A.HOLDOUT.date()})"
f"={n_hold} -> COINCIDONO")
print(f" (cand_full_sharpe={rep.get('cand_full_sharpe')} == "
f"cand_hold_sharpe={rep.get('cand_hold_sharpe')}). Il candidato vive INTERAMENTE")
print(" dentro l'hold-out del progetto: FULL e HOLD-OUT sono la STESSA finestra e la")
print(" meta' out-of-sample del gate e' strutturalmente INERTE (robust_oos=False non")
print(" giudica il candidato, dice che il multi-cut non esiste). Da leggere come")
print(" 'girato ma NON informativo sull'OOS', non come un NEUTRAL guadagnato sul campo.")
all_sr = list(R.sharpe.values)
d18 = A.deflated_sharpe(sr, all_sr, cd)
d_all = A.deflated_sharpe(sr, all_sr * 7, cd)
@@ -628,8 +655,10 @@ def main() -> None:
giugno -0.64%, luglio -0.74%): 2 mesi su 3 non sono un plateau, sono due mesi.
3. IL SEGNALE E' UN MASSIMO DI 18 CELLE SU 114 GIORNI: Sharpe 3.6 con SE(Sharpe)=2.0,
deflated-Sharpe 0.10 (18 celle) / 0.004 (118 trial) contro un massimo atteso PER CASO di
Sharpe 5.4; implausible_sharpe=True; marginal_vs_tp01 = NEUTRAL; e il P&L e' per il 71%
di giugno e per il 60% concentrato in 3 giorni su 114.
Sharpe 5.4; implausible_sharpe=True; e il P&L e' per il 71% di giugno e per il 60%
concentrato in 3 giorni su 114. marginal_vs_tp01 gira e dice NEUTRAL, ma va dichiarato
NON INFORMATIVO SULL'OOS: il candidato sta interamente dopo HOLDOUT(2025-01-01), quindi
full e hold-out sono la stessa finestra (Sharpe identico) e il multi-cut non esiste.
4. ED E' COMUNQUE MORTO ALLA NASCITA: la colonna su cui poggia non e' ricostruibile con i dati
che raccogliamo (l'OI della catena e' quello delle OPZIONI, rango ~0.05 con la colonna) e
la sorgente e' stata dismessa il 2026-07-30.
+53 -28
View File
@@ -60,7 +60,8 @@ FEE_SIDE_CONS = A.FEE_SIDE # 5 bps/lato = convenzione conservativa dei bac
MAX_H = 50 # ore prima della scadenza da tenere in memoria
HTE_GRID = (1, 2, 3, 6, 12, 24, 36, 48) # ore-alla-scadenza all'INGRESSO
RULES = ("oi", "gex", "mp") # max OI · max gamma*OI · max pain
PLACEBOS = ("rand", "adj", "jit", "mid", "mr7")
PLACEBOS_GLOB = ("rand", "mid", "mr7") # non dipendono dalla regola
PLACEBOS_RULE = ("adj", "jit") # costruiti sulla distanza DELLA regola
SEED = 20260822
RNG = np.random.default_rng(SEED)
@@ -144,29 +145,39 @@ def levels_table(chain: pd.DataFrame, spot: dict[str, pd.Series]) -> pd.DataFram
"gex": float(K[int(np.argmax(gx))]) if gx.max() > 0 else np.nan,
"mp": _max_pain(K, oi_c, oi_p),
}
# ---- placebo location-matched -------------------------------------------------
# rand: permutare l'OI fra gli strike e riprendere l'argmax equivale ESATTAMENTE a
# estrarre uno strike uniforme dalla griglia (l'argmax di un vettore permutato
# e' uniforme sulle posizioni) -> lo si implementa cosi', ed e' esatto.
p_rand = float(K[RNG.integers(len(K))])
# adj: lo strike ADIACENTE al max-OI (stesso quartiere, OI diverso)
j = i_oi + (1 if (i_oi == 0 or (i_oi < len(K) - 1 and RNG.random() < 0.5)) else -1)
p_adj = float(K[int(np.clip(j, 0, len(K) - 1))])
# jit: strike arbitrario alla STESSA distanza dallo spot ("0.618 vs 0.58?")
dist = abs(K - S)
d0 = abs(lv["oi"] - S)
cand = np.where((dist >= 0.5 * d0) & (dist <= 1.5 * d0) & (K != lv["oi"]))[0]
p_jit = float(K[cand[RNG.integers(len(cand))]]) if len(cand) else np.nan
# mid: punto medio della griglia quotata = puro artefatto di copertura, zero info OI
p_mid = float(0.5 * (K[0] + K[-1]))
# ---- placebo LOCATION-MATCHED, uno per OGNI regola --------------------------------
# ⚠ difetto trovato e corretto in sessione: la prima stesura costruiva `adj`/`jit`
# attorno al SOLO max-OI e poi li usava come null anche per max-pain e gamma. Ma
# |K_oi - S| mediano e' ~2,4% e |K_mp - S| ~1,1%: confrontare mp con un placebo
# tarato sulla distanza di oi NON e' location-matched, e' proprio l'errore che il
# null doveva prevenire. Ogni regola ha ora i placebo alla PROPRIA distanza.
dist = np.abs(K - S)
for nm, kv in list(lv.items()):
if not np.isfinite(kv):
out_adj = out_jit = np.nan
else:
i_r = int(np.argmin(np.abs(K - kv)))
j = i_r + (1 if (i_r == 0 or (i_r < len(K) - 1 and RNG.random() < 0.5)) else -1)
out_adj = float(K[int(np.clip(j, 0, len(K) - 1))])
d0 = abs(kv - S)
cand = np.where((dist >= 0.5 * d0) & (dist <= 1.5 * d0) & (K != kv))[0]
out_jit = float(K[cand[RNG.integers(len(cand))]]) if len(cand) else np.nan
lv[f"adj_{nm}"] = out_adj
lv[f"jit_{nm}"] = out_jit
# placebo GLOBALI (non dipendono dalla regola)
# rand: permutare l'OI fra gli strike e riprenderne l'argmax equivale ESATTAMENTE a
# estrarre uno strike uniforme dalla griglia (l'argmax di un vettore permutato e'
# uniforme sulle posizioni) -> lo si implementa cosi', ed e' esatto, non simulato.
lv["rand"] = float(K[RNG.integers(len(K))])
# mid: centro della griglia quotata = puro artefatto di copertura, zero informazione OI
lv["mid"] = float(0.5 * (K[0] + K[-1]))
rows.append(dict(
asset=asset, exp=exp, hh=hh, dte_h=float((exp - hh).total_seconds() / 3600.0),
S=float(S), n_k=len(K), tot_oi=float(tot),
k_oi=lv["oi"], k_gex=lv["gex"], k_mp=lv["mp"],
p_rand=p_rand, p_adj=p_adj, p_jit=p_jit, p_mid=p_mid,
top_share=float(oi[i_oi] / tot),
hhi=float(((oi / tot) ** 2).sum()),
k_step=float(np.median(np.diff(K))) if len(K) > 1 else np.nan,
**{f"L_{k}": v for k, v in lv.items()},
))
return pd.DataFrame(rows)
@@ -198,9 +209,8 @@ def build_events(lev: pd.DataFrame, spot: dict[str, pd.Series]) -> pd.DataFrame:
top_share=float(r["top_share"]), hhi=float(r["hhi"]),
k_step=float(r["k_step"]),
dow=int(pd.Timestamp(exp).dayofweek))
lvls = {"oi": r["k_oi"], "gex": r["k_gex"], "mp": r["k_mp"],
"rand": r["p_rand"], "adj": r["p_adj"], "jit": r["p_jit"],
"mid": r["p_mid"], "mr7": float(mr7[a].asof(hh))}
lvls = {c[2:]: r[c] for c in r.index if str(c).startswith("L_")}
lvls["mr7"] = float(mr7[a].asof(hh))
for nm, K in lvls.items():
K = float(K) if K is not None else np.nan
if not np.isfinite(K) or K <= 0:
@@ -421,12 +431,12 @@ def main() -> None:
P("\n LA STESSA STATISTICA SUI LIVELLI PLACEBO — se sale anche li', NON e' pinning:")
P(f" {'hte':>4} " + " ".join(f"{('tw_' + n):>10}" for n in
("oi", "mp", "rand", "jit", "mid", "mr7", "maj", "short")))
("oi", "mp", "rand", "jit_mp", "mid", "mr7", "maj", "short")))
for hte in HTE_GRID:
e = ev[ev["hte"] == hte]
P(f" {hte:>4} " + " ".join(
f"{np.nanmean(e[f'tw_{n}']):>10.5f}" for n in
("oi", "mp", "rand", "jit", "mid", "mr7", "maj", "short")))
("oi", "mp", "rand", "jit_mp", "mid", "mr7", "maj", "short")))
P("\n QUANTO SONO LO STESSO SEGNALE? accordo del SEGNO di (K-S) fra max-pain e i placebo:")
for n in ("mid", "mr7", "oi", "maj"):
agr = [float(np.nanmean(np.sign(ev.loc[ev.hte == h, "d_mp"])
@@ -446,22 +456,25 @@ def main() -> None:
P(" placebo: rand=strike uniforme (== permutazione dell'OI, equivalenza esatta) · adj=strike")
P(" adiacente al max-OI · jit=strike arbitrario a distanza 0.5-1.5x (analogo Fib±jitter) ·")
P(" mid=centro della griglia quotata (zero info OI) · mr7=media 7g dello spot (mean-reversion).")
P(f" {'hte':>4} {'rule':>5} " + " ".join(f"{('Δ vs ' + p):>12}" for p in PLACEBOS))
P(f" {'hte':>4} {'rule':>5} {'Δ vs adj_r':>12} {'Δ vs jit_r':>12}"
f" {'Δ vs rand':>12} {'Δ vs mid':>12} {'Δ vs mr7':>12}")
npos = ntot = 0
for hte in HTE_GRID:
e = ev[ev["hte"] == hte]
for rl in RULES:
cells = []
for p in PLACEBOS:
for p in (f"adj_{rl}", f"jit_{rl}") + PLACEBOS_GLOB:
dd = e[f"tw_{rl}"].to_numpy(float) - e[f"tw_{p}"].to_numpy(float)
m, lo, hi, _ = cluster_boot(dd, e["exp"].to_numpy())
mark = "*" if (np.isfinite(lo) and lo > 0) else (" ")
cells.append(f"{m:>11.5f}{mark}")
cells.append(f"{m:>11.5f}{'*' if (np.isfinite(lo) and lo > 0) else ' '}")
ntot += 1
npos += int(np.isfinite(lo) and lo > 0)
P(f" {hte:>4} {rl:>5} " + " ".join(cells))
P(f" celle (rule x hte x placebo) in cui lo SPECIALE batte il placebo con IC95>0:"
f" {npos}/{ntot} (atteso per caso ~{0.025 * ntot:.0f})")
P(" LETTURA: `adj_r`/`jit_r` chiedono 'e' proprio QUELLO strike?'; `mid`/`mr7` chiedono")
P(" 'serve l'open interest, o basta il centro recente del prezzo?'. La seconda e' la")
P(" domanda che uccide o salva il filone.")
# ---------------------------------------------------------------- 5. CONDIZIONAMENTO
P("\n[5] CONDIZIONAMENTO — la concentrazione di OI e le scadenze grandi cambiano qualcosa?")
@@ -581,7 +594,7 @@ def main() -> None:
# DSR: conta TUTTI i trial al rialzo
all_sr = [float(v) for v in C["sharpe"].to_numpy(float) if np.isfinite(v)]
all_sr_wide = all_sr + [float(v) for v in N["sharpe"].to_numpy(float) if np.isfinite(v)]
n_fact = len(HTE_GRID) * len(RULES) * (1 + len(PLACEBOS)) + 3 * 4 # celle del fatto + condiz.
n_fact = len(HTE_GRID) * len(RULES) * (1 + len(PLACEBOS_GLOB) + len(PLACEBOS_RULE)) + 3 * 4 # celle del fatto + condiz.
trials_tot = len(all_sr_wide) + n_fact
dsr, sr0 = A.deflated_sharpe(mb["sharpe"], all_sr, dbest, dpy=365.25)
dsr_w, _ = A.deflated_sharpe(mb["sharpe"], all_sr_wide, dbest, dpy=365.25)
@@ -632,6 +645,18 @@ def main() -> None:
f" · MEDIANA ONESTA {ab.get('median'):.2f} · banda [{ab.get('lo'):.2f},{ab.get('hi'):.2f}]"
f" · positive {ab.get('frac_positive'):.0%} · gate_pass={ab.get('gate_pass')}")
# LA DOMANDA DECISIVA, sulla banda d'ancora: max-pain aggiunge qualcosa a un livello
# che NON usa opzioni? Mediana delle DIFFERENZE APPAIATE sui 8 hte (mai differenza
# delle mediane: lezione 26/07).
def _fn(rule):
return lambda h: strat_daily(ev, rule, int(h), (0.002, 0.03), "sign", "all",
FEE_SIDE_REAL)[0]
for other in ("mr7", "mid", "oi", "rand"):
dl = A.anchor_luck_delta(_fn("mp"), _fn(other), offsets=list(HTE_GRID))
P(f" Δ appaiato mp-{other:<4}: mediana {dl.get('median_paired'):+.2f}"
f" · positiva in {dl.get('n_positive')}/{dl.get('n_anchors')} hte"
f" · banda [{dl.get('lo'):+.2f},{dl.get('hi'):+.2f}] · gate_pass={dl.get('gate_pass')}")
# eseguibilita' a $600
P(" eseguibilita' $600 :", end=" ")
hc = []
+30 -8
View File
@@ -92,7 +92,7 @@ RAW = ROOT / "data" / "raw"
DELUCK = 0.89 # fattore d'ancora MISURATO sul drift (26/07), non stimato a occhio
START_WIN = "2024-01-01" # finestra comune ai 5 sleeve (XS01 nasce qui)
BLOCK = 20 # blocco del bootstrap, in giorni
N_PATHS = 4000
N_PATHS = 2500
SPLIT = pl.SPLIT # 80% al trader
TARGET_EUR_DAY = 50.0
FIRMS = {f["name"]: f for f in pl.FIRMS}
@@ -386,7 +386,15 @@ def hr(t: str = "") -> None:
def main() -> None:
try:
sys.stdout.reconfigure(line_buffering=True)
except Exception: # pragma: no cover
pass
t0 = time.time()
def tick(lab: str) -> None:
print(f" [{time.time()-t0:6.0f}s] {lab}")
print("=" * 104)
print(" PROP-ALLOC — allocare per la BARRIERA (P(pass) x P(vivo 12m)) invece che per lo Sharpe")
print("=" * 104)
@@ -398,6 +406,7 @@ def main() -> None:
print(f" de-luck sul drift x{DELUCK} (misurato 26/07) | bootstrap a blocchi di {BLOCK}g, "
f"indici CONDIVISI fra configurazioni | {N_PATHS} percorsi")
tick("pannello costruito")
off = {"TP01": _d(_tp01_returns()), "SKH01": _d(_skyhook_returns()), "XS01": _d(_xsec_returns()),
"VRP01": _d(_vrp_combo_returns()), "GTAA01": _d(_gtaa_daily_returns())}
rec = {"TP01": _crypto(1.0, 0.0)["R"], "SKH01": _crypto(0.0, 1.0)["R"],
@@ -430,6 +439,7 @@ def main() -> None:
print(" non per assenza di rischio (mark infra-settimana misurato il 30/07: mediana -6.9%,")
print(" minimo -81.7% del capitale su un trade finito in UTILE).")
tick("A. profilo sleeve")
hr("B. IL CONTO ANALITICO — perche' con leva LIBERA la barriera premia lo SHARPE, non la vol")
tgt, dd, _dl = FIRMS["HYRO"]["ev"]
print(f" P(toccare +{tgt:.0%} prima di -{dd:.0%}), gambler's ruin senza limite di tempo, "
@@ -460,6 +470,7 @@ def main() -> None:
f"= {len(grid)*len(LEVS)} celle, firm HYRO (eval +10%/-6%/-4%, funded -6%/-4%)")
print(f" orizzonte eval {EV_H}g, funded {FU_H}g. Lente ACCOPPIATA. "
f"J = P(pass) x P(vivo 12m).")
tick("B. conto analitico")
t = time.time()
G3 = grid_scan(grid, LEVS, "HYRO", ev_idx, fu_idx)
print(f" ({time.time()-t:.0f}s)")
@@ -513,14 +524,15 @@ def main() -> None:
print(" lette insieme: la leva alta non aumenta la probabilita' di farcela, aumenta la FRETTA.")
# ---------------------------------------------------------------- D. i null
tick("C. griglia + leva")
hr("D. NULL 1 — l'argmax e' speciale, o e' solo il MASSIMO DI 45 ESTRAZIONI?")
rw = random_weights(300, CRYPTO_PROP, seed=8223)
rw = random_weights(200, CRYPTO_PROP, seed=8223)
lev_star = float(best["lev"])
Jr = np.array([objective(w, lev_star, "HYRO", ev_idx, fu_idx)["J"] for w in rw])
pctl = float((Jr < best["J"]).mean() * 100.0)
bestofk = 100.0 * len(grid) / (len(grid) + 1)
print(f" J dell'argmax = {best['J']:.3f} alla leva {lev_star:g}")
print(f" null: 300 vettori di peso casuali uniformi sul simplesso (stesso campionatore di")
print(f" null: {len(rw)} vettori di peso casuali uniformi sul simplesso (stesso campionatore di")
print(f" `portfolio.weights_tilt_null`), valutati alla STESSA leva")
print(f" mediana {np.median(Jr):.3f} | p90 {np.percentile(Jr, 90):.3f} | "
f"max {Jr.max():.3f} | frazione sopra il LIVE 75/25 = {(Jr > best_live['J']).mean():.0%}")
@@ -531,6 +543,7 @@ def main() -> None:
print(f" (la meta' bassa del null e' pero' informativa: il LIVE 75/25 e' battuto dal "
f"{(Jr > best_live['J']).mean():.0%} dei pesi CASUALI)")
tick("D. null casuale")
hr("D-bis. NULL 2 — selezione IN-SAMPLE-ONLY (la scelta guardando J e' selezione come ogni altra)")
half = n_hist // 2
R_all = {}
@@ -569,6 +582,7 @@ def main() -> None:
print(" -> se rho e' alto la mappa e' stabile e la scelta si trasferisce; se e' ~0 l'argmax")
print(" e' rumore e la decisione va presa sul MECCANISMO (la leva), non sulla cella.")
tick("D-bis. selezione in-sample")
hr("D-ter. NULL DEL DE-LEVERING — la leva e' GIA' dentro l'ottimizzazione, ma va detto")
print(" Il null classico ('esiste k<1 che da' lo stesso DD con Sharpe migliore?') qui e'")
print(" strutturale: ogni configurazione e' valutata a OTTO leve, quindi nessuna puo' vincere")
@@ -586,8 +600,11 @@ def main() -> None:
print(" Ri-fare i 24x23x10 offset col recon MTM orario e' fuori budget. Si applica il fattore")
print(" di drift MISURATO il 26/07 (x0.89 sul libro live) e si controlla cio' che conta: se")
print(" l'ORDINE fra le configurazioni sopravvive alla banda 0.60 (stima 25/07, ri-misurata")
print(" troppo severa) - 1.00 (nessuna correzione).")
print(" troppo severa) - 1.00 (nessuna correzione). L'argmax e' ri-cercato sulle 12 celle-peso")
print(" migliori (non su tutte le 45: costo), il che e' conservativo per la STABILITA'.")
cands = [best["w"], best_sh["w"], best_live["w"]]
top_keys = list(G3.sort_values("J", ascending=False).drop_duplicates("w")["w"].head(12))
top_w = [dict(zip(CRYPTO_PROP, [int(x) / 100 for x in k.split("/")])) for k in top_keys]
print(f"\n {'fattore':>9} " + " ".join(f"{('J ' + c):>14}" for c in dict.fromkeys(cands))
+ f" {'argmax pesi':>14} {'argmax leva':>11}")
for fct in (1.00, 0.89, 0.60):
@@ -596,12 +613,13 @@ def main() -> None:
wc = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in c.split("/")]))
sub = [objective(wc, lv, "HYRO", ev_idx, fu_idx, factor=fct)["J"] for lv in LEVS]
cells.append(max(sub))
g = grid_scan(grid, LEVS, "HYRO", ev_idx, fu_idx, factor=fct)
g = grid_scan(top_w, LEVS, "HYRO", ev_idx, fu_idx, factor=fct)
b = g.loc[g["J"].idxmax()]
print(f" {fct:>9.2f} " + " ".join(f"{v:>14.3f}" for v in cells)
+ f" {b['w']:>14} {b['lev']:>11.3f}")
# ---------------------------------------------------------------- F. VRP01 / GTAA01
tick("E. banda ancora")
hr("F. E GLI SLEEVE 'INUTILI A $600'? — l'asimmetria del 25/07 §4 vale per XS01, non per tutti")
print(" Su un conto funded da $100k gli sleeve fuori portata a $600 diventano ESEGUIBILI per")
print(" TAGLIA. Ma la taglia non e' l'unico vincolo: una firm CRYPTO non lista opzioni Deribit")
@@ -625,11 +643,12 @@ def main() -> None:
print(" 6% verrebbe sfondata dal mark, non dal risultato.")
# ---------------------------------------------------------------- G. FTMO
tick("F. VRP/GTAA")
hr("G. LA BARRIERA CAMBIA LA RISPOSTA? — HYRO (-6% statico) vs FTMO (-10% statico)")
print(f" {'firm':>6} {'argmax pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo12m)':>11} "
f"{'E[pay/a]':>10} | {'LIVE 75/25: leva':>17} {'J':>7}")
for fm in ("HYRO", "FTMO"):
g = grid_scan(grid, LEVS, fm, ev_idx, fu_idx)
g = G3 if fm == "HYRO" else grid_scan(grid, LEVS, fm, ev_idx, fu_idx)
b = g.loc[g["J"].idxmax()]
lv2 = g[g["w"] == "75/25/0"]
b2 = lv2.loc[lv2["J"].idxmax()]
@@ -640,12 +659,13 @@ def main() -> None:
print(" vincolo binding e' la regola di DD, non il capitale'), qui misurata sull'ottimo.")
# ---------------------------------------------------------------- H. gate ufficiale sui pesi
tick("G. FTMO")
hr("H. GATE UFFICIALE SUI PESI (`portfolio.weights_tilt_null`) — sullo SHARPE, come previsto")
cols = {nm: off[nm] for nm in CRYPTO_PROP}
try:
res = weights_tilt_null(cols, {"TP01": 0.75, "SKH01": 0.25, "XS01": 0.0001},
{nm: max(w_best.get(nm, 0.0), 1e-4) for nm in CRYPTO_PROP},
floor=0.0, n=300, k_seen=len(grid) * len(LEVS))
floor=0.0, n=200, k_seen=len(grid) * len(LEVS))
print(f" proposta {best['w']} vs LIVE 75/25 : delta_insample {res['delta_insample']:+.4f} | "
f"delta_hold {res['delta_hold']:+.4f} | pctl_hold {res['pctl_hold']:.1f}° "
f"(best-of-k {res['bestofk_pctl']:.1f}°) | gate_pass = {res['gate_pass']}")
@@ -656,6 +676,7 @@ def main() -> None:
print(" e' il libro live e non passa da questo gate.")
# ---------------------------------------------------------------- I. tavola operativa
tick("H. gate pesi")
hr("I. TAVOLA OPERATIVA — le 3 configurazioni migliori, e quanti conti servono per 50 EUR/g")
top = G3.sort_values("J", ascending=False).drop_duplicates("w").head(3)
rows_ops = []
@@ -681,6 +702,7 @@ def main() -> None:
print(f" -> al massimo {cap_tot/100_000:.0f} conti da $100k. Se ne servono di piu', servono "
f"ALTRE FIRM (e ognuna aggiunge rischio di controparte).")
tick("I. tavola operativa")
hr("I-bis. LA SCALA DI CONTI — macchineria del 25/07 (`r0725_prop_coupled.simulate`), "
"de-luck portato a x0.89")
print(f" 36 mesi, EUR {pl.START_EUR:.0f} di cassa iniziale, max {pl.MAX_CONCURRENT} conti, "
@@ -698,7 +720,7 @@ def main() -> None:
f"{'P(>=50/g)':>10} {'P(zero)':>8}")
for pname, (cfgs, slots) in pols.items():
for lev in (float(best["lev"]), 0.75):
r = pc.simulate(cfgs, slots, n_paths=2000, lev=lev, seed=20260822,
r = pc.simulate(cfgs, slots, n_paths=1500, lev=lev, seed=20260822,
lens="coupled", start=START_WIN)
print(f" {pname:>44} {lev:>6.3f} {r['med']:>10.2f} {r['p90']:>8.2f} "
f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}")
+20 -5
View File
@@ -115,7 +115,10 @@ def load_quoted_chain() -> pd.DataFrame:
df = pd.concat(parts, ignore_index=True)
del parts
df = df.drop_duplicates(subset=["ts", "asset", "strike", "option_type", "exp"], keep="last")
df["hr"] = df["ts"].dt.floor("h")
# ⚠️ le quote di bite sono datetime64[us], il feed certificato e' [ns]: un join fra
# DatetimeIndex di unita' diverse degrada a Index generico e rompe ogni resample a valle.
df["hr"] = df["ts"].dt.floor("h").dt.as_unit("ns")
df["exp"] = df["exp"].dt.as_unit("ns")
df["dte"] = (df["exp"] - df["ts"]).dt.total_seconds() / 86400.0
df["quoted"] = (df["bid"] > 0) & (df["ask"] > 0) & df["iv"].notna() & df["delta"].notna()
return df
@@ -290,7 +293,9 @@ def sezione1(curve: pd.DataFrame) -> dict:
# SEZIONE 2 — Q1: LEAD-LAG. La pendenza precede o segue?
# ===========================================================================================
def slope_hourly(curve: pd.DataFrame, a: str) -> pd.Series:
s = curve[curve.asset == a].set_index("hr")
s = curve[curve.asset == a].copy()
s["hr"] = pd.to_datetime(s["hr"], utc=True).dt.as_unit("ns")
s = s.set_index("hr")
return (s[f"iv_{BACK}"] - s[f"iv_{FRONT}"]).dropna().sort_index()
@@ -356,7 +361,9 @@ def sezione2(curve: pd.DataFrame) -> dict:
print("\n (c) IL CONFOUND DI QUESTO FILONE — la pendenza e' informazione NUOVA o RV riciclata?")
rvp = (r1.rolling(24).std() * np.sqrt(24 * 365) * 100).reindex(idx)
rvf = (r1.shift(-24).rolling(24).std().shift(-23) * np.sqrt(24 * 365) * 100).reindex(idx)
ivf = curve[curve.asset == a].set_index("hr")[f"iv_{FRONT}"].reindex(idx)
cf = curve[curve.asset == a].copy()
cf["hr"] = pd.to_datetime(cf["hr"], utc=True).dt.as_unit("ns")
ivf = cf.set_index("hr")[f"iv_{FRONT}"].reindex(idx)
c_pp = float(j.slope.corr(rvp))
c_pf = float(j.slope.corr(rvf))
print(f" corr(pendenza, RV 24h PASSATA) {c_pp:+.3f} <-- quanto e' termometro")
@@ -400,8 +407,14 @@ def sezione2(curve: pd.DataFrame) -> dict:
x, y = sld[m].to_numpy(), fwd[m].to_numpy()
c = float(np.corrcoef(x, y)[0, 1])
p = _block_pctl(x, y, c, L=max(3 * h, 7))
print(f" h={h}g n={m.sum():3d} corr={c:+.3f} pctl vs null a blocchi={p:.3f}")
TRIALS.append((f"Q1 {a}: 3 orizzonti giornalieri", 3))
c7 = float(np.corrcoef(x[:-7], y[:-7])[0, 1]) if len(x) > 20 else np.nan
c7b = float(np.corrcoef(x[7:], y[7:])[0, 1]) if len(x) > 20 else np.nan
print(f" h={h}g n={m.sum():3d} corr={c:+.3f} pctl vs null a blocchi={p:.3f}"
f" | senza ultima settimana {c7:+.3f} | senza prima settimana {c7b:+.3f}")
print(" ^ le due colonne di destra sono il test di RISOLUZIONE: se togliere 7")
print(" giorni su 74 sposta la correlazione di piu' di quanto la correlazione")
print(" valga, il campione non sta misurando, sta oscillando.")
TRIALS.append((f"Q1 {a}: 3 orizzonti giornalieri x 3 sottocampioni", 9))
return res
@@ -600,6 +613,7 @@ def sezione4(curve: pd.DataFrame) -> None:
sub("(b) ridondanza, per quel che il campione consente")
for a in ASSETS:
sl = slope_hourly(curve, a).resample("1D").last().dropna()
sl.index = pd.DatetimeIndex(sl.index).as_unit("ns")
j = pd.concat({"slope": sl, "tp": pos[a]}, axis=1, join="inner").dropna()
for qq in (0.10, 0.25, 0.40):
th = j.slope.quantile(qq)
@@ -704,6 +718,7 @@ def sezione5(curve: pd.DataFrame) -> None:
sub("(a) quanto e' buono il proxy (misurato sulla finestra vera, non assunto)")
for a in ASSETS:
sl = slope_hourly(curve, a).resample("1D").last().dropna()
sl.index = pd.DatetimeIndex(sl.index).as_unit("ns")
df = A.get(a, "1d")
p = pd.Series(_proxy_slope(df, a),
index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))).dropna()
+38 -13
View File
@@ -282,6 +282,17 @@ def leverage_series(base: pd.Series, tv: float | None, w: int, cap: float,
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."""
@@ -389,11 +400,12 @@ def main() -> None:
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(BVT) + list(NAIVE)
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(BVT)} vol-target di libro + {len(NAIVE)} controlli non-causali = {len(ALL)} celle "
f"(+ baseline).")
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()
@@ -420,6 +432,16 @@ def main() -> None:
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:
@@ -467,7 +489,7 @@ def main() -> None:
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(NAIVE):
for nm in list(SZ) + list(VTL) + list(ACT) + list(NAIVE):
prow(nm, SLE[nm][can], SIZEMED.get(nm))
# ------------------------------------------------------------------ §3 libro, banda appaiata
@@ -592,7 +614,7 @@ def main() -> None:
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)], key=lambda x: is_med[x])
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", {})
@@ -606,7 +628,7 @@ def main() -> None:
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 [best, best_sleeve] + list(BVT)[:2]:
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,
@@ -630,7 +652,7 @@ def main() -> None:
no = ns = 0
for a in ASSETS:
df1h = TO.get1h(a)
tgt = netted_target(a, EX[can][a], nm, SZ, VTL, BVT, SLE, TP, can, df1h)
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"]
@@ -647,8 +669,8 @@ def main() -> None:
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})
top = [best, best_iso] + [max(BVT, key=lambda x: RES[x]["iF"])]
top = list(dict.fromkeys(top))
top = list(dict.fromkeys([best, best_iso, 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 = []
@@ -668,11 +690,12 @@ def main() -> None:
if nm in SZ:
szs = np.concatenate([SZ[nm](EX[can][a]) for a in ASSETS])
dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS]))
elif nm in VTL:
elif nm in VTL or nm in ACT:
mk = ((lambda b: leverage_series(b, **VTL[nm])) if nm in VTL
else (lambda b: activity_L(b, **ACT[nm])))
szs, dys = [], []
for a in ASSETS:
szs.append(sizes_from_L(EX[can][a],
leverage_series(EX[can][a]["base_daily"], **VTL[nm])))
szs.append(sizes_from_L(EX[can][a], mk(EX[can][a]["base_daily"])))
dys.append(EX[can][a]["day_ent"].values)
szs = np.concatenate(szs); dys = pd.DatetimeIndex(np.concatenate(dys))
else:
@@ -697,7 +720,7 @@ def main() -> None:
print("=" * 112)
def netted_target(asset: str, ex: dict, nm, SZ, VTL, BVT, SLE, TP, can, df1h) -> np.ndarray:
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."""
@@ -711,6 +734,8 @@ def netted_target(asset: str, ex: dict, nm, SZ, VTL, BVT, SLE, TP, can, df1h) ->
sizes = SZ[nm](ex)
elif nm in VTL:
sizes = sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm]))
elif nm in ACT:
sizes = sizes_from_L(ex, activity_L(ex["base_daily"], **ACT[nm]))
else: # BOOKVT
L = leverage_series(book(TP, SLE["BASE"][can]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"])
sizes = sizes_from_L(ex, L)
+36 -6
View File
@@ -326,11 +326,11 @@ def main() -> None:
V = CB.dvol_series(a)
win = V[(V.index >= puts["ts"].min()) & (V.index <= puts["ts"].max())]
hist = V[V.index < win.index[0]].to_numpy(float)
print(f" {a}: DVOL nel campione min {win.min().1f} / mediana {win.median().1f} / "
f"max {win.max().1f}")
print(f" vs storia 2021+ ({len(hist)} giorni) la MEDIANA del campione sta al "
f"{float((hist < win.median()).mean()).0%} pctl, il suo MASSIMO al "
f"{float((hist < win.max()).mean()).0%} pctl")
print(f" {a}: DVOL nel campione min {win.min():.1f} / mediana {win.median():.1f} / "
f"max {win.max():.1f}")
print(f" vs storia 2021+ ({len(hist)} giorni): la MEDIANA del campione sta al "
f"{float((hist < win.median()).mean()):.0%} pctl, il suo MASSIMO al "
f"{float((hist < win.max()).mean()):.0%} pctl")
print("\n \u26a0\ufe0f Due statistiche diverse, da tenere distinte: CLAUDE.md cita «DVOL max al 24\u00b0/28\u00b0\n"
" pctl» dal campione del 30/07. Un MASSIMO cresce con la finestra — e' una statistica\n"
" di ESTREMO, non di livello. Per dire in che regime siamo la lente e' la MEDIANA, ed\n"
@@ -368,6 +368,29 @@ def main() -> None:
f"max {tr['ivrank'].max():.3f})")
print(f" TOTALE: {passed}/{tot}. Il sleeve sarebbe stato FLAT per l'intero campione.")
print("\n QUANDO TORNERA' MISURABILE — il gate non e' chiuso per sempre, e la soglia e' un numero.")
print(" Regola del 19/06: «rivalutare quando la catena cattura un crash». Un criterio senza\n"
" numero non e' un criterio, quindi eccolo (IV-rank espandente causale, come nel sleeve):")
for a in ASSETS:
V = CB.dvol_series(a)
vals = V.to_numpy(float)
ivr = np.full(len(vals), np.nan)
for i in range(1, len(vals)):
ivr[i] = (vals[:i] < vals[i]).mean()
ser = pd.Series(ivr, index=V.index).dropna()
per_year = ser.groupby(ser.index.year).apply(lambda x: float((x > 0.30).mean()))
thr = float(np.quantile(vals[:-1], 0.30))
last_open = ser[ser > 0.30]
since = (V.index[-1] - last_open.index[-1]).days if len(last_open) else None
print(f" {a}: quota di giorni col gate APERTO per anno — "
+ " ".join(f"{y}:{v:.0%}" for y, v in per_year.items()))
print(f" DVOL oggi {vals[-1]:.1f} · serve DVOL > {thr:.1f} "
f"(+{thr / vals[-1] - 1:.0%}) per riaprire il gate · ultimo giorno aperto "
f"{last_open.index[-1]:%Y-%m-%d} ({since} giorni fa)")
print(" → il campione non e' 'sfortunato': cade dentro un regime di vol bassa lungo e\n"
" persistente. La misura giusta si potra' rifare quando quel numero sara' superato,\n"
" e da allora servira' comunque un anno di settimane per avere un campione.")
ret = {a: (canon[a]["ST"].iloc[-1] / canon[a]["spot"].iloc[0] - 1.0) for a in ASSETS}
print(f"\n E il sottostante nel campione: BTC {ret['BTC']:+.1%}, ETH {ret['ETH']:+.1%}"
f"vol implicita bassa E rialzo forte\n = il regime IDEALE per vendere put. "
@@ -582,6 +605,13 @@ def main() -> None:
if not W.empty:
print(f"\n Peggior mark infra-settimana: mediana {W['frac'].median():.1%} del rischio, "
f"minimo {W['frac'].min():.1%}")
wmin = W.loc[W['frac'].idxmin()]
print(f" Denominatore, per non fabbricare una discrepanza: qui il rischio e' "
f"`width - credito`.\n"
f" CLAUDE.md cita «-81,7% del capitale» per la STESSA settimana "
f"({wmin['asset']} {wmin['exp']:%Y-%m-%d}):\n"
f" e' lo stesso mark (${wmin['worst']:.2f}) diviso per la width nuda. Due "
f"denominatori, un solo fatto.")
wl = W[W["pnl"] > 0]
print(f" Fra le sole settimane chiuse in UTILE ({len(wl)}/{len(W)}): mediana "
f"{wl['frac'].median():.1%}, minimo {wl['frac'].min():.1%}")
@@ -694,7 +724,7 @@ def main() -> None:
1. IL CAMPIONE NON CONTIENE LA STRATEGIA. 0 settimane su 19 passano il gate IV-rank>0.30
che E' l'alpha di VRP01. Il campione e' vol implicita al 24-28 percentile E un rialzo
del +25%/+57%: il regime migliore possibile per vendere put, non uno neutro.
forte del sottostante: il regime migliore possibile per vendere put, non uno neutro.
Le 10 settimane per asset (non 16: prima del 2026-06-09 la finestra 4-10 DTE non
esiste) danno 10/10 vincenti su BTC. La lente giusta non e' lo Sharpe, e' la regola
del tre: 0 perdite su 10 lascia un tasso vero fino al 30%, su un payoff che perde
+37 -21
View File
@@ -375,11 +375,9 @@ def main() -> None:
print(" su GTAA/ADJUSTED_LAST). NON l'ho inseguito: tutte le mie conclusioni sono confronti")
print(" RELATIVI dentro UNA lente. Ma tocca un numero pubblicato e il gate del 23/10.")
print(f" XSR01 dal 25/07 a oggi (forward, {len(xsr_fwd)} barre, stessa lente): "
f"ritorno {(np.prod(1+xsr_fwd.values)-1)*100:+.2f}% "
f"Sharpe {sh(xsr_fwd) if len(xsr_fwd) > 30 else float('nan'):.2f} "
f"{'(campione troppo corto per uno Sharpe: si legge il ritorno)' if len(xsr_fwd) <= 30 else ''}")
print(" ^ la differenza fra 1.82 e il numero FULL di oggi NON e' una lente diversa (le repliche")
print(" sono bit-exact): e' un mese in piu' di dati. Il gate pre-registrato resta il 23/10.")
f"ritorno {(np.prod(1+xsr_fwd.values)-1)*100:+.2f}%"
+ (" (campione troppo corto per uno Sharpe: si legge il ritorno)"
if len(xsr_fwd) <= 30 else f" Sharpe {sh(xsr_fwd):.2f}"))
print(f" finestra MAJ19: {C19.index[0].date()} -> {C19.index[-1].date()} ({len(C19)} barre); "
f"in-sample = pre {HOLDOUT.date()} = {int((C19.index < HOLDOUT).sum())} barre")
cov51 = np.isfinite(C51.values).sum(axis=1)
@@ -535,24 +533,42 @@ def main() -> None:
f"{dg['med_ticket']:>11.0f}{dg['orders_py']:>10.0f}"
f"{trk:>12.2f}{'' if trk < 0.8 else ' '}")
print()
print(f" {'cella':<13}{'|dw| mediano al ribil.':>24}{'capitale sleeve minimo':>25}{' = book al 15%':>17}")
print(f" {'cella':<13}{'|dw| RIBILANCIO':>18}{'|dw| deriva':>14}{'cap sleeve min':>17}"
f"{'= book al 15%':>16}")
capmin_k5 = None
for nm in ("MAJ19 k=1", "MAJ19 k=2", "MAJ19 k=3", "MAJ19 k=5*"):
cell = xs_cells[nm]
Weff = xs_effective_weights(cell["W"], cell["scale"])
chg = np.abs(np.diff(cell["W"], axis=0)).sum(axis=1) > 1e-12 # giorni di ribilanciamento
rows = np.where(chg)[0] + 1
d = np.abs(np.diff(Weff, axis=0))[rows - 1]
med = float(np.median(d[d > 1e-9])) if np.any(d > 1e-9) else float("nan")
capmin = MIN_ORDER_HL / med if med > 0 else float("nan")
print(f" {'XS ' + nm.split()[1]:<13}{med:>24.4f}{capmin:>24.0f}$"
f"{capmin/0.15:>16.0f}$")
print(" ^ ARITMETICA: il ticket di un ribilanciamento e' |dw|*capitale_sleeve. Il min-order")
print(" morde sotto il 'capitale sleeve minimo'. Per il CANONICO k=5 quel numero e' ~150$")
print(" di sleeve = ~1.000$ di book al 15%, non 20.000$. La soglia dichiarata dal progetto")
print(" veniva da una diagnostica di TURNOVER (quota di turnover sopra il min-order), che a")
print(" capitale piccolo e' bassa perche' salta la deriva GIORNALIERA del vol-target — la")
print(" quale non porta segnale. Stessa lezione gia' imparata su TP01 ('a $600 il min-order")
print(" $5 e' GIA' la banda ottimale'), non applicata a XS01.")
W = cell["W"]
Weff = xs_effective_weights(W, cell["scale"])
dW = np.abs(np.diff(W, axis=0)) # W cambia alla riga j+1
dE = np.abs(np.diff(Weff, axis=0)) # Weff[j+1]-Weff[j], Weff[i]=W[i-1]*scale[i]
# il cambio di W alla riga j+1 si vede su Weff alla riga j+2 -> confine da rispettare
rb, dr = [], []
for j in range(len(dW)):
if j + 1 >= len(dE):
break
m = dW[j] > 1e-12
if m.any():
rb.extend(dE[j + 1][m].tolist()) # gambe DAVVERO ribilanciate
dr.extend(dE[j + 1][~m][dE[j + 1][~m] > 1e-12].tolist())
else:
dr.extend(dE[j + 1][dE[j + 1] > 1e-12].tolist())
med_rb = float(np.median(rb)) if rb else float("nan")
med_dr = float(np.median(dr)) if dr else float("nan")
capmin = MIN_ORDER_HL / med_rb if med_rb > 0 else float("nan")
if nm.startswith("MAJ19 k=5"):
capmin_k5 = capmin
print(f" {'XS ' + nm.split()[1]:<13}{med_rb:>18.4f}{med_dr:>14.4f}{capmin:>16.0f}$"
f"{capmin/0.15:>15.0f}$")
print(" ^ ARITMETICA: il ticket vale |dw|*capitale_sleeve, e le due colonne |dw| sono ordini")
print(" di grandezza diversi: il RIBILANCIO (ogni 10 giorni, porta il segnale) e la DERIVA")
print(" giornaliera del vol-target (non porta segnale). Il min-order taglia la seconda molto")
print(" prima della prima, ed e' per questo che la quota di turnover eseguito puo' essere del")
print(f" 4% con haircut ~0. Per il CANONICO k=5 il ribilancio smette di passare sotto "
f"~{capmin_k5:.0f}$")
print(f" di sleeve = ~{capmin_k5/0.15:.0f}$ di book al 15%. La soglia '$20k' del progetto viene da")
print(" una diagnostica di TURNOVER, non di Sharpe — stessa lezione gia' imparata su TP01")
print(" ('a $600 il min-order $5 e' GIA' la banda ottimale'), mai applicata a XS01.")
print()
print(" 'gambe eseg' = quota delle VARIAZIONI di posizione sopra il min-order (la diagnostica")
print(" su cui si basa la soglia '$20k'). 'haircut' = quanto ne costa in Sharpe. Sono cose")