Files
PythagorasGoal/scripts/research/r0822_prop_alloc.py
T

947 lines
52 KiB
Python

"""r0822_prop_alloc — PROGETTARE l'allocazione per la BARRIERA, non per lo Sharpe (2026-08-22).
IL BUCO CHE CHIUDE
------------------
Il canale prop/funded e' l'unico misurato che moltiplica il nozionale senza possedere capitale.
Il progetto lo ha gia' VALUTATO (24-25/07: `r0725_prop_ladder.py`, `r0725_prop_coupled.py`,
`r0725_hyro.py`) ma non lo ha mai **PROGETTATO**: ha sempre preso il libro ottimizzato per lo
Sharpe (config A = live 75/25, config B = 55/20/25 scelta a mano) e poi MISURATO P(pass) del
risultato. Due punti nello spazio dei pesi, tre leve, nessuna ottimizzazione, nessun null.
Qui la funzione obiettivo cambia: sotto una **barriera di maxDD STATICA per-conto** conta la coda
sinistra e la DURATA, non il rapporto rendimento/vol. Si ottimizza
J(w, k) = P(passare l'eval) x P(essere vivo dopo 12 mesi da funded)
su pesi w e leva k, con la lente WICK ACCOPPIATA (la `close-only` e' CIECA: 0% di breach da
daily-loss su ogni configurazione, la regola non scatta mai sulle chiusure).
LA PREVISIONE, REGISTRATA PRIMA DI MISURARE
-------------------------------------------
Nel limite gaussiano con leva LIBERA l'obiettivo si riduce allo Sharpe, e si vede in due righe.
Per un moto browniano con drift mu e vol sigma a leva k, la probabilita' di toccare +T prima di
-D vale P = (1-e^{-theta*D})/(1-e^{-theta*(T+D)}) con **theta = 2*mu/(k*sigma^2)**: la leva entra
SOLO dividendo theta. Alla leva minima che porta a +T dentro l'orizzonte (k ~ T/mu) si ottiene
theta ~ 2*mu^2/(T*sigma^2) = 2*S^2/T, con S = Sharpe: **la vol sparisce e resta lo Sharpe al
quadrato**. Quindi mi aspetto PRIMA di misurare:
(a) l'argmax di J vicino all'argmax di Sharpe -> la premessa "obiettivo diverso" e' in gran
parte FALSA sui PESI;
(b) il contenuto vero nella **LEVA**, che lo Sharpe non determina affatto, e nella deviazione
dal gaussiano (skew, wick, daily-loss: una soglia ASSOLUTA non e' scale-invariante);
(c) leva ottima per J **piu' bassa** di quella ottima per il payout (0.75x, 25/07): J e' una
probabilita' di sopravvivenza, il payout e' un flusso.
Se (a) risulta falsa, e' un risultato; se risulta vera, il filone "riallocare per la barriera"
si chiude e si smette di pagarlo.
CONVENZIONI DICHIARATE
----------------------
* Macchineria del wick ACCOPPIATO **riusata** da `r0725_prop_coupled.py` (import, non riscritta):
`_hourly_legs` (recon MTM orario TP01/SKH01), `crypto_daily_tuples` (minimo ESATTO sul path
orario condiviso), `xsec_daily_tuples` (XS01 dagli OHLC 1d, ordine condiviso avverso).
* VRP01 e GTAA01 sono AGGIUNTI qui (non c'erano): GTAA01 col wick dagli OHLC dei 6 ETF (long-only
co-moventi -> ogni gamba al proprio minimo = convenzione severa dichiarata); VRP01 con
**gap = 0**, cioe' CIECO, perche' il suo mark infra-settimana non e' nel dataset dello sleeve
(il rendimento settimanale e' lumpato sul giorno di scadenza). E' una lente GENEROSA per VRP01:
la misura del 30/07 sulle quote reali dice mark peggiore infra-settimana mediano -6.9% e minimo
**-81.7%** del capitale su un trade finito in UTILE. Se VRP01 perde sotto una lente generosa,
perde.
* de-luck sul DRIFT **x0.89** (fattore MISURATO il 26/07 sul libro live, `r0726_deluck_factor.py`),
non x0.6 come nel 25/07 (li' era stimato a occhio e ri-misurato troppo severo del 31-34%).
Sensibilita' a 1.00 e 0.60 girata: cio' che conta e' se l'ORDINE fra le configurazioni tiene.
* bootstrap a blocchi (20g) con **indici CONDIVISI fra tutte le configurazioni** -> le differenze
fra configurazioni sono APPAIATE e il loro errore MC e' molto minore di quello dei livelli.
* finestra comune **2024+** (XS01 nasce li'): ~2.6 anni, ed e' la finestra in cui XS01 e' stato
scoperto -> le configurazioni che lo contengono sono FAVORITE per costruzione. Dichiarato.
* fisco 33% sui payout, split 80%, regole vere delle due firm (max-loss STATICO dal saldo
iniziale, base ripristinata dopo il prelievo — bug del 25/07 gia' corretto a monte).
Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822_prop_alloc.py`
"""
from __future__ import annotations
import sys
import time
from functools import lru_cache
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"))
from src.data.eq_splits import repair_splits # noqa: E402
from src.portfolio.gtaa import ( # noqa: E402
ANN, EQ_UNIVERSE, GTAA_DEFAULT_CAPITAL, HORIZONS, IB_MIN_TRADE_USD, REBAL_BAND_USD,
REBAL_EVERY, TARGET_VOL, ib_commission,
)
from src.portfolio.portfolio import metrics, weights_tilt_null # noqa: E402
from src.portfolio.sleeves import ( # noqa: E402
GTAA_BOOK_ACTIVATION, _gtaa_daily_returns, _skyhook_returns, _tp01_returns,
_vrp_combo_returns, _xsec_returns,
)
import r0725_prop_coupled as pc # noqa: E402
import r0725_prop_ladder as pl # noqa: E402
from r0725_capcurve import EURUSD, TAX_RATE # noqa: E402
RAW = ROOT / "data" / "raw"
# ------------------------------------------------------------------ costanti dichiarate
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 = 2500
SPLIT = pl.SPLIT # 80% al trader
TARGET_EUR_DAY = 50.0
FIRMS = {f["name"]: f for f in pl.FIRMS}
SLEEVES = ("TP01", "SKH01", "XS01", "VRP01", "GTAA01")
CRYPTO_PROP = ("TP01", "SKH01", "XS01") # cio' che una firm CRYPTO puo' davvero far eseguire
# ================================================================== §0. PANNELLO (R, wick)
@lru_cache(maxsize=8)
def _eq_ohlc(sym: str) -> pd.DataFrame:
p = RAW / f"eq_{sym.lower()}_1d.parquet"
d = pd.read_parquet(p)
d.index = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
d = d.sort_index()
d, _ = repair_splits(d) # split IWM/EFA non aggiustati (25/07)
return d[["open", "high", "low", "close"]].astype(float)
def _gtaa_exposure(close: pd.Series) -> np.ndarray:
"""Copia FEDELE di gtaa._exposure (che prende una Series di soli close)."""
px = close.values
n = len(px)
tgt = np.zeros(n)
mh = max(HORIZONS)
for i in range(mh, n):
tgt[i] = np.mean([1.0 if px[i] > px[i - H] else 0.0 for H in HORIZONS])
s = pd.Series(tgt, index=close.index)
rv = close.pct_change().rolling(63, min_periods=20).std().shift(1) * ANN
scale = np.clip(np.nan_to_num(TARGET_VOL / rv.replace(0, np.nan).values, nan=0.0), 0, 10.0)
return (s * scale).clip(0, 1.0).values
@lru_cache(maxsize=2)
def gtaa_daily_tuples() -> pd.DataFrame:
"""(R, m) giornalieri di GTAA01: chiusura replicata da gtaa.gtaa_returns e minimo intraday
dagli OHLC delle 6 gambe. Long-only e co-moventi -> convenzione **ogni gamba al proprio
minimo insieme** (severa, la stessa famiglia del bound 'perleg' di XS01).
Weekend/festivi: R = 0 e m = 0 (capitale IB fermo — convenzione del book)."""
cap_leg = GTAA_DEFAULT_CAPITAL / len(EQ_UNIVERSE)
legs_r, legs_m = {}, {}
for sym in EQ_UNIVERSE:
d = _eq_ohlc(sym)
close = d["close"]
ex = np.nan_to_num(_gtaa_exposure(close))
px = close.values
prev = np.concatenate(([np.nan], px[:-1]))
ret = np.zeros(len(px))
ret[1:] = px[1:] / px[:-1] - 1.0
ret_low = d["low"].values / prev - 1.0 # long-only: il peggio e' il minimo
held = np.empty(len(ex))
comm = np.zeros(len(ex))
cur = 0.0
for i in range(len(ex)): # identico a gtaa._gated_returns
if i % REBAL_EVERY == 0:
notional = abs(ex[i] - cur) * cap_leg
if notional >= max(REBAL_BAND_USD, IB_MIN_TRADE_USD):
comm[i] = ib_commission(notional, px[i]) / cap_leg
cur = ex[i]
held[i] = cur
pos = np.zeros(len(held))
pos[1:] = held[:-1]
legs_r[sym] = pd.Series(pos * ret - comm, index=close.index)
legs_m[sym] = pd.Series(pos * np.nan_to_num(ret_low) - comm, index=close.index)
R = pd.concat(legs_r, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
M = pd.concat(legs_m, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
days = pd.date_range(R.index.min().normalize(), R.index.max().normalize(), freq="1D", tz="UTC")
R = R.reindex(days).fillna(0.0)
M = M.reindex(days).fillna(0.0)
out = pd.DataFrame({"R": R, "m": np.minimum(M, R)})
return out[out.index >= GTAA_BOOK_ACTIVATION]
@lru_cache(maxsize=2)
def vrp_daily_tuples() -> pd.DataFrame:
"""(R, m) di VRP01 con **m = R**: il mark infra-settimana NON e' nel dataset dello sleeve
(rendimento settimanale lumpato sul giorno di scadenza). Lente CIECA e GENEROSA — dichiarata."""
r = _vrp_combo_returns().dropna().sort_index()
if r.index.tz is None:
r.index = r.index.tz_localize("UTC")
return pd.DataFrame({"R": r.values, "m": r.values}, index=r.index)
def _d(s: pd.Series) -> pd.Series:
s = s.dropna().sort_index()
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
return s
_CRY_CACHE: dict = {}
def _crypto_stressed(w_tp: float, w_skh: float, m_tp: float, m_skh: float) -> pd.DataFrame:
"""Come `pc.crypto_daily_tuples` (minimo ESATTO sul path orario condiviso) ma con un taglio
del DRIFT applicato alla singola gamba PRIMA di comporre il path. Serve perche' il minimo
giornaliero non e' separabile per gamba: non si puo' tagliare il drift a valle."""
(J,) = pc._hourly_legs()
legs = {}
for nm, (rc, wc, mult) in {"tp": ("tp_r", "tp_w", m_tp), "sk": ("skh_r", "skh_w", m_skh)}.items():
r = J[rc].values.copy()
w = J[wc].values.copy()
if mult != 1.0:
cut = (1.0 - mult) * r.mean()
r = r - cut
w = w - cut
legs[nm] = (r, w)
r_h = w_tp * legs["tp"][0] + w_skh * legs["sk"][0]
w_h = w_tp * legs["tp"][1] + w_skh * legs["sk"][1]
idx = J.index
days = idx.floor("D")
rows = []
for day, pos in pd.Series(np.arange(len(idx)), index=days).groupby(level=0):
sl = pos.values
r = r_h[sl]
wk = w_h[sl]
cum = np.cumprod(1.0 + r)
cum_prev = np.concatenate(([1.0], cum[:-1]))
m = min(cum.min(), (cum_prev * (1.0 + wk)).min()) - 1.0
rows.append((day, cum[-1] - 1.0, min(m, cum[-1] - 1.0)))
return pd.DataFrame(rows, columns=["day", "R", "m"]).set_index("day")
def _crypto(w_tp: float, w_skh: float, m_tp: float = 1.0, m_skh: float = 1.0) -> pd.DataFrame:
key = (round(w_tp, 6), round(w_skh, 6), round(m_tp, 4), round(m_skh, 4))
if key not in _CRY_CACHE:
_CRY_CACHE[key] = (pc.crypto_daily_tuples(w_tp, w_skh) if (m_tp == 1.0 and m_skh == 1.0)
else _crypto_stressed(w_tp, w_skh, m_tp, m_skh))
return _CRY_CACHE[key]
@lru_cache(maxsize=4)
def _panel_index(start: str) -> pd.DatetimeIndex:
"""Griglia comune ai 5 sleeve (intersezione), dal `start` in poi."""
idx = _crypto(1.0, 0.0).index
for f in (pc.xsec_daily_tuples(), vrp_daily_tuples(), gtaa_daily_tuples()):
idx = idx.intersection(f.index)
return idx[idx >= pd.Timestamp(start, tz="UTC")]
_RG_CACHE: dict = {}
def book_RG(w: dict, start: str = START_WIN,
drift_mult: tuple = ()) -> tuple[np.ndarray, np.ndarray]:
"""(R, gap) giornalieri di un libro a pesi arbitrari sui 5 sleeve.
Crypto TP01+SKH01: minimo ESATTO sul path orario condiviso (r0725_prop_coupled).
XS01/VRP01/GTAA01: il loro minimo si SOMMA (worst simultaneo — convenzione del 25/07).
`drift_mult` = (("XS01", 0.5), ...): taglia il DRIFT di quello sleeve (non la vol) — serve
per lo stress giudiziale su uno sleeve scoperto DENTRO la finestra di misura."""
dm = dict(drift_mult)
key = (start, drift_mult) + tuple(round(float(w.get(nm, 0.0)), 6) for nm in SLEEVES)
if key in _RG_CACHE:
return _RG_CACHE[key]
idx = _panel_index(start)
w_tp, w_skh = w.get("TP01", 0.0), w.get("SKH01", 0.0)
R = np.zeros(len(idx))
M = np.zeros(len(idx))
if w_tp > 0 or w_skh > 0:
c = _crypto(w_tp, w_skh, dm.get("TP01", 1.0), dm.get("SKH01", 1.0)).reindex(idx)
R += c["R"].values
M += c["m"].values
for nm, fr in (("XS01", pc.xsec_daily_tuples()), ("VRP01", vrp_daily_tuples()),
("GTAA01", gtaa_daily_tuples())):
wt = w.get(nm, 0.0)
if wt > 0:
f = fr.reindex(idx)
rv = np.nan_to_num(f["R"].values)
mv = np.nan_to_num(f["m"].values)
mult = dm.get(nm, 1.0)
if mult != 1.0: # taglia il drift, lascia la forma della coda
cut = (1.0 - mult) * rv.mean() # NB: media PRIMA del taglio
rv = rv - cut
mv = mv - cut
R += wt * rv
M += wt * mv
R = np.nan_to_num(R)
M = np.nan_to_num(M)
_RG_CACHE[key] = (R, np.minimum(M, R) - R)
return _RG_CACHE[key]
def deluck(R: np.ndarray, factor: float = DELUCK) -> np.ndarray:
return R - (1.0 - factor) * R.mean()
# ================================================================== §1. SIMULATORE
def boot_idx(n_hist: int, n_days: int, n_paths: int, seed: int, block: int = BLOCK) -> np.ndarray:
rng = np.random.default_rng(seed)
n_blocks = int(np.ceil(n_days / block))
st = rng.integers(0, n_hist - block, size=(n_paths, n_blocks))
return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
def eval_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: str,
lens: str = "coupled") -> dict:
"""P(passare l'eval) entro l'orizzonte del bootstrap + giorni al pass."""
tgt, dd, dl = FIRMS[firm]["ev"]
r = R[idx] * lev
g = (G[idx] * lev) if lens == "coupled" else np.zeros_like(r)
P, D = r.shape
eq = np.ones(P)
alive = np.ones(P, bool)
passed = np.zeros(P, bool)
tpass = np.full(P, D, int)
for t in range(D):
act = alive & ~passed
if not act.any():
break
rt, gt = r[:, t], g[:, t]
eqp = eq
eq = np.where(act, eq * (1.0 + rt), eq)
eq_low = np.where(act, eqp * (1.0 + rt + gt), eq)
bust = act & ((eq_low < 1.0 - dd) | (rt + gt < -dl))
alive &= ~bust
ok = act & ~bust & (eq >= 1.0 + tgt)
tpass[ok] = t
passed |= ok
return dict(p_pass=float(passed.mean()), passed=passed,
t_pass=float(np.median(tpass[passed])) if passed.any() else np.nan)
def funded_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: str,
notional: float, lens: str = "coupled") -> dict:
"""P(vivo a fine orizzonte) + payout netto cumulato (prelievo mensile, base ripristinata)."""
ml, dl = FIRMS[firm]["fu"]
r = R[idx] * lev
g = (G[idx] * lev) if lens == "coupled" else np.zeros_like(r)
P, D = r.shape
eq = np.ones(P)
alive = np.ones(P, bool)
payout = np.zeros(P)
alive_at = {}
for t in range(D):
rt, gt = r[:, t], g[:, t]
eqp = eq
eq = np.where(alive, eq * (1.0 + rt), eq)
eq_low = np.where(alive, eqp * (1.0 + rt + gt), eq)
bust = alive & ((eq_low < 1.0 - ml) | (rt + gt < -dl))
alive &= ~bust
if (t + 1) % 30 == 0:
take = alive & (eq > 1.0)
payout += np.where(take, (eq - 1.0) * notional, 0.0) * SPLIT * (1.0 - TAX_RATE)
eq = np.where(take, 1.0, eq)
if (t + 1) % 365 == 0:
alive_at[(t + 1) // 365] = float(alive.mean())
return dict(p_alive=float(alive.mean()), alive_at=alive_at,
e_payout=float(payout.mean()), payout=payout, alive=alive)
def objective(w: dict, lev: float, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray,
lens: str = "coupled", factor: float = DELUCK,
notional: float = 100_000.0, drift_mult: tuple = ()) -> dict:
R, G = book_RG(w, drift_mult=drift_mult)
R = deluck(R, factor)
e = eval_sim(R, G, ev_idx, lev, firm, lens)
f = funded_sim(R, G, fu_idx, lev, firm, notional, lens)
ann = float(R.mean() * 365.0)
vol = float(R.std() * np.sqrt(365.0))
# stimatore per-percorso di J: eval e funded girano su indici di bootstrap INDIPENDENTI,
# quindi mean(passed_i * alive_i) stima p_pass*p_alive ed e' APPAIABILE fra configurazioni.
jvec = (e["passed"] & f["alive"]).astype(float)
return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"],
e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0),
vol=vol, drift=ann, t_pass=e["t_pass"], jvec=jvec)
def paired_delta(wA: dict, levA: float, wB: dict, levB: float, firm: str,
ev_idx: np.ndarray, fu_idx: np.ndarray,
drift_mult: tuple = ()) -> tuple[float, float]:
"""Differenza APPAIATA di J fra due configurazioni (stessi indici di bootstrap) + errore
standard della differenza. Il livello di J ha SE ~0.01; la DIFFERENZA molto meno.
⚠ `drift_mult` va passato: dimenticarlo confronta due configurazioni NON stressate e
stampa un delta che non c'entra con la riga in cui compare (errore commesso in sessione)."""
a = objective(wA, levA, firm, ev_idx, fu_idx, drift_mult=drift_mult)["jvec"]
b = objective(wB, levB, firm, ev_idx, fu_idx, drift_mult=drift_mult)["jvec"]
d = a - b
return float(d.mean()), float(d.std(ddof=1) / np.sqrt(len(d)))
# ================================================================== §2. IL CONTO ANALITICO
def p_hit_target_first(mu: float, sig: float, lev: float, T: float, D: float) -> float:
"""Gambler's ruin per moto browniano con drift: P(toccare +T prima di -D), SENZA limite di
tempo. theta = 2*mu/(k*sigma^2): la leva entra SOLO dividendo theta -> con leva -> inf la
probabilita' tende a D/(T+D) (37.5% con 10%/6%), qualunque sia lo Sharpe."""
if sig <= 0:
return float(mu > 0)
theta = 2.0 * mu / (lev * sig * sig)
if abs(theta) < 1e-12:
return D / (T + D)
return float((1.0 - np.exp(-theta * D)) / (1.0 - np.exp(-theta * (T + D))))
# ================================================================== §3. GRIGLIA / NULL
def simplex_grid(names: tuple, step_den: int) -> list[dict]:
"""Tutti i vettori di peso a passo 1/step_den sul simplesso di `names` (somma 1)."""
out = []
n = len(names)
def rec(prefix, rem, k):
if k == n - 1:
out.append(dict(zip(names, [p / step_den for p in prefix] + [rem / step_den])))
return
for v in range(rem + 1):
rec(prefix + [v], rem - v, k + 1)
rec([], step_den, 0)
return out
def wkey(w: dict) -> str:
return "/".join(f"{int(round(100 * w.get(nm, 0.0))):d}" for nm in CRYPTO_PROP)
def grid_scan(grid: list[dict], levs: tuple, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray,
lens: str = "coupled", factor: float = DELUCK) -> pd.DataFrame:
rows = []
for w in grid:
for lev in levs:
o = objective(w, lev, firm, ev_idx, fu_idx, lens, factor)
rows.append(dict(w=wkey(w), lev=lev,
**{k: v for k, v in o.items() if k != "jvec"}))
return pd.DataFrame(rows)
def random_weights(n: int, names: tuple, seed: int, floor: float = 0.0,
caps: dict | None = None) -> list[dict]:
"""Stesso campionatore di `portfolio.weights_tilt_null` (dirichlet uniforme sul simplesso +
rejection su floor/caps), ma qui serve a costruire il NULL della funzione obiettivo J."""
rng = np.random.default_rng(seed)
caps_v = np.array([(caps or {}).get(nm, 1.0) for nm in names], float)
out = []
while len(out) < n:
b = rng.dirichlet(np.ones(len(names)), size=4 * n + 64)
ok = (b >= floor).all(axis=1) & (b <= caps_v).all(axis=1)
out.extend(b[ok])
return [dict(zip(names, v)) for v in out[:n]]
# ================================================================== main
def hr(t: str = "") -> None:
print("\n" + "-" * 104)
if t:
print(f" {t}")
print("-" * 104)
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)
# ---------------------------------------------------------------- A. pannello + sanity
idx = _panel_index(START_WIN)
print(f"\n finestra comune ai 5 sleeve: {len(idx)} giorni {idx[0].date()} -> {idx[-1].date()}"
f" (XS01 nasce nel 2024: e' anche la sua finestra di SCOPERTA -> lo favorisce)")
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"],
"XS01": pc.xsec_daily_tuples()["R"], "VRP01": vrp_daily_tuples()["R"],
"GTAA01": gtaa_daily_tuples()["R"]}
print("\n sanity — ricostruzione vs sleeve ufficiale. XS01/VRP01/GTAA01 devono coincidere")
print(" BARRA PER BARRA; TP01/SKH01 no per costruzione (il recon e' un mark-to-market ORARIO,")
print(" lo sleeve marca a fine-trade) -> per loro il controllo giusto e' il log-totale.")
print(f" {'sleeve':>8} {'max|delta| barra':>18} {'log-tot recon':>15} "
f"{'log-tot sleeve':>16} {'scarto':>9}")
for nm in SLEEVES:
com = off[nm].index.intersection(rec[nm].index)
a = off[nm].reindex(com).values
b = rec[nm].reindex(com).values
dmax = float(np.abs(a - b).max())
la, lb = float(np.log1p(a).sum()), float(np.log1p(b).sum())
flag = "OK" if dmax < 1e-9 else ("lente MTM" if nm in ("TP01", "SKH01") else "DIVERGE")
print(f" {nm:>8} {dmax:>18.2e} {lb:>15.4f} {la:>16.4f} {lb-la:>+9.4f} {flag}")
hr("A. PROFILO DEGLI SLEEVE SOTTO BARRIERA — cio' che conta non e' lo Sharpe, e si vede qui")
print(f" {'sleeve':>8} {'Sharpe':>7} {'vol a':>7} {'drift a':>8} {'skew':>7} "
f"{'giorno peggiore':>16} {'gap p90':>9} {'gap peggiore':>13} {'g/vol':>7}")
prof = {}
for nm in SLEEVES:
R, G = book_RG({nm: 1.0})
Rd = deluck(R)
vol = Rd.std() * np.sqrt(365.0)
dr = Rd.mean() * 365.0
sk = float(pd.Series(Rd).skew())
prof[nm] = dict(sharpe=dr / vol if vol > 0 else 0.0, vol=vol, drift=dr, skew=sk,
worst=Rd.min(), gap90=np.percentile(-G, 90), gapw=G.min())
print(f" {nm:>8} {prof[nm]['sharpe']:>7.2f} {vol:>6.1%} {dr:>8.1%} {sk:>7.2f} "
f"{Rd.min():>15.2%} {-np.percentile(-G, 90)*100:>8.2f}pp {G.min()*100:>12.2f}pp "
f"{(-G.min()/vol) if vol > 0 else 0:>7.2f}")
print("\n lettura: `gap peggiore` e' l'escursione intraday che la lente close-only NON vede;")
print(" `g/vol` la misura in unita' di vol annua. VRP01 ha gap 0 **per cecita' del dato**,")
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, "
f"theta = 2*mu/(k*sigma^2):")
print(f" {'sleeve':>8} {'Sharpe':>7} | " + " ".join(f"{f'k={k:g}':>8}" for k in (0.25, 0.5, 1.0, 2.0, 4.0)))
for nm in SLEEVES:
p = prof[nm]
cells = " ".join(f"{p_hit_target_first(p['drift'], p['vol'], k, tgt, dd):>8.1%}"
for k in (0.25, 0.5, 1.0, 2.0, 4.0))
print(f" {nm:>8} {p['sharpe']:>7.2f} | {cells}")
print(f" {'limite k->inf':>16} | " + " ".join([f"{dd/(tgt+dd):>8.1%}"] * 5)
+ " <- D/(T+D): la leva alta CANCELLA l'edge")
print("\n Alla leva minima che porta a +T dentro l'orizzonte (k ~ T/mu) resta theta ~ 2*S^2/T:")
print(" la VOL sparisce e conta lo Sharpe AL QUADRATO. Percio' la previsione registrata prima")
print(" di misurare e': l'argmax di J sui PESI ~ l'argmax di Sharpe, e il contenuto vero e'")
print(" nella LEVA (che lo Sharpe non determina) e nella deviazione dal gaussiano.")
# ---------------------------------------------------------------- C. griglia
n_hist = len(idx)
EV_H, FU_H = 365, 365
ev_idx = boot_idx(n_hist, EV_H, N_PATHS, seed=8220)
fu_idx = boot_idx(n_hist, FU_H, N_PATHS, seed=8221)
LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50)
STEP = 8
grid = simplex_grid(CRYPTO_PROP, STEP)
hr(f"C. GRIGLIA — {len(grid)} vettori di peso (passo 1/{STEP}) x {len(LEVS)} leve "
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)")
best = G3.loc[G3["J"].idxmax()]
# argmax di SHARPE: lo Sharpe non dipende dalla leva -> si sceglie il peso, poi la leva
# migliore PER QUEL PESO secondo J (altrimenti non e' un confronto onesto).
w_sh = G3.loc[G3["sharpe"].idxmax(), "w"]
sub_sh = G3[G3["w"] == w_sh]
best_sh = sub_sh.loc[sub_sh["J"].idxmax()]
live = G3[(G3["w"] == "75/25/0")]
best_live = live.loc[live["J"].idxmax()]
print(f"\n {'':>22} {'pesi TP/SKH/XS':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} "
f"{'P(vivo12m)':>11} {'Sharpe':>7} {'vol':>6} {'E[pay/a]':>10} {'g.al pass':>10}")
for lab, row in (("argmax J", best), ("argmax Sharpe", best_sh),
("libro LIVE 75/25", best_live)):
print(f" {lab:>22} {row['w']:>16} {row['lev']:>6.3f} {row['J']:>7.3f} "
f"{row['p_pass']:>8.1%} {row['p_alive']:>11.1%} {row['sharpe']:>7.2f} "
f"{row['vol']:>6.1%} {row['e_payout']:>9,.0f}$ {row['t_pass']:>10.0f}")
dA, seA = paired_delta(w_argmax_dict := dict(zip(CRYPTO_PROP, [int(x)/100 for x in best["w"].split("/")])),
float(best["lev"]),
dict(zip(CRYPTO_PROP, [int(x)/100 for x in best_sh["w"].split("/")])),
float(best_sh["lev"]), "HYRO", ev_idx, fu_idx)
dB, seB = paired_delta(w_argmax_dict, float(best["lev"]),
{"TP01": 0.75, "SKH01": 0.25}, float(best_live["lev"]),
"HYRO", ev_idx, fu_idx)
print(f"\n DIFFERENZE APPAIATE (stessi indici di bootstrap; un livello di J ha SE ~0.01, "
f"una differenza molto meno):")
print(f" argmax J - argmax SHARPE : {dA:+.4f} +- {seA:.4f} "
f"-> {'INDISTINGUIBILI' if abs(dA) < 2*seA else 'differenza reale'}")
print(f" argmax J - libro LIVE : {dB:+.4f} +- {seB:.4f} "
f"-> {'INDISTINGUIBILI' if abs(dB) < 2*seB else 'differenza reale'}")
quota = abs(dA) / abs(dB) if dB != 0 else float("nan")
print(f" La prima riga E' il risultato del filone: riallocare per la BARRIERA invece che")
print(f" per lo SHARPE vale {dA:+.3f} di J, cioe' il {quota:.0%} del {dB:+.3f} che vale")
print(f" passare dal libro LIVE a un libro diversificato QUALSIASI. Il grosso del guadagno")
print(f" NON e' nella funzione obiettivo: e' nell'aggiungere lo sleeve che a $600 non entra.")
print("\n Le prime 8 celle per J:")
print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} "
f"{'Sharpe':>7} {'E[pay/a]':>10}")
for i, (_, r) in enumerate(G3.sort_values("J", ascending=False).head(8).iterrows(), 1):
print(f" {i:>3} {r['w']:>16} {r['lev']:>6.3f} {r['J']:>7.3f} {r['p_pass']:>8.1%} "
f"{r['p_alive']:>8.1%} {r['sharpe']:>7.2f} {r['e_payout']:>9,.0f}$")
print("\n E per E[payout] (obiettivo DIVERSO: un flusso, non una sopravvivenza):")
print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'E[pay/a]':>10} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8}")
for i, (_, r) in enumerate(G3.sort_values("e_payout", ascending=False).head(4).iterrows(), 1):
print(f" {i:>3} {r['w']:>16} {r['lev']:>6.3f} {r['e_payout']:>9,.0f}$ {r['J']:>7.3f} "
f"{r['p_pass']:>8.1%} {r['p_alive']:>8.1%}")
hr("C-bis. LA LEVA — la variabile che lo Sharpe non determina e che decide tutto")
print(f" pesi = argmax J ({best['w']}):")
print(f" {'leva':>7} {'J':>7} {'P(pass) 365g':>13} {'P(pass) 1095g':>14} {'P(vivo 12m)':>12} "
f"{'E[pay/a]':>10}")
ev_long = boot_idx(n_hist, 1095, N_PATHS, seed=8222)
w_best = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in best["w"].split("/")]))
Rb, Gb = book_RG(w_best)
Rb = deluck(Rb)
for lev in LEVS:
o = objective(w_best, lev, "HYRO", ev_idx, fu_idx)
pl_long = eval_sim(Rb, Gb, ev_long, lev, "HYRO")["p_pass"]
print(f" {lev:>7.3f} {o['J']:>7.3f} {o['p_pass']:>12.1%} {pl_long:>13.1%} "
f"{o['p_alive']:>11.1%} {o['e_payout']:>9,.0f}$")
print("\n A 365 giorni P(pass) e' a CAMPANA nella leva (a leva bassa il drift non arriva a +10%")
print(" in tempo); senza limite di tempo — che e' la regola vera di HyroTrader — e' MONOTONA")
print(" DECRESCENTE, come dice il conto analitico. Le due colonne dicono cose diverse e vanno")
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(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: {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%}")
print(f" percentile dell'argmax fra i casuali = {pctl:.1f}° contro la firma best-of-"
f"{len(grid)} = {bestofk:.1f}°")
print(f" ⚠ RISOLUZIONE: con {len(rw)} estrazioni un percentile vale a passi di "
f"{100/len(rw):.1f} punti -> {pctl:.1f}° e {bestofk:.1f}° NON sono distinguibili.")
print(f" E il fatto che decide: il MASSIMO dei {len(rw)} pesi casuali e' {Jr.max():.3f} contro"
f" {best['J']:.3f} dell'argmax")
print(f" della griglia -> un vettore estratto a caso lo {'EGUAGLIA' if Jr.max() >= best['J'] - 1e-9 else 'non lo raggiunge'}."
f" NESSUNA evidenza che quel vettore di pesi sia speciale:")
print(" cio' che conta non e' QUALE punto del simplesso, ma STARE nella regione diversificata.")
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 = {}
for w in grid:
R_, G_ = book_RG(w)
R_all[wkey(w)] = (deluck(R_), G_)
ev_h = boot_idx(half, EV_H, N_PATHS, seed=8224)
fu_h = boot_idx(half, FU_H, N_PATHS, seed=8225)
def _J(key, lev, sl):
R_, G_ = R_all[key]
e = eval_sim(R_[sl], G_[sl], ev_h, lev, "HYRO")["p_pass"]
f = funded_sim(R_[sl], G_[sl], fu_h, lev, "HYRO", 100_000.0)["p_alive"]
return e * f
IS, OOS = slice(0, half), slice(half, n_hist)
rows = []
for w in grid:
k = wkey(w)
for lev in LEVS:
rows.append((k, lev, _J(k, lev, IS), _J(k, lev, OOS)))
S = pd.DataFrame(rows, columns=["w", "lev", "J_is", "J_oos"])
pick = S.loc[S["J_is"].idxmax()]
oracle = S.loc[S["J_oos"].idxmax()]
live_oos = S[(S["w"] == "75/25/0")].sort_values("J_oos", ascending=False).iloc[0]
print(f" prima meta' {idx[0].date()}->{idx[half-1].date()} | seconda {idx[half].date()}->{idx[-1].date()}")
print(f" {'':>28} {'pesi':>16} {'leva':>6} {'J in-sample':>12} {'J out-of-sample':>16}")
print(f" {'scelto SUL SOLO in-sample':>28} {pick['w']:>16} {pick['lev']:>6.3f} "
f"{pick['J_is']:>12.3f} {S[(S['w']==pick['w'])&(S['lev']==pick['lev'])]['J_oos'].iloc[0]:>16.3f}")
print(f" {'oracolo (max sull OOS)':>28} {oracle['w']:>16} {oracle['lev']:>6.3f} "
f"{oracle['J_is']:>12.3f} {oracle['J_oos']:>16.3f}")
print(f" {'libro LIVE 75/25 (baseline)':>28} {live_oos['w']:>16} {live_oos['lev']:>6.3f} "
f"{live_oos['J_is']:>12.3f} {live_oos['J_oos']:>16.3f}")
rho = float(S["J_is"].corr(S["J_oos"], method="spearman"))
print(f" Spearman(J in-sample, J out-of-sample) su {len(S)} celle = {rho:+.2f}")
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")
print(" solo perche' e' meno esposta. La domanda che resta e': la MIGLIOR leva del libro LIVE")
print(" raggiunge il J dell'argmax?")
print(f" argmax J : {best['w']:>10} @ {best['lev']:g}x -> J = {best['J']:.3f}")
print(f" LIVE 75/25 al suo : {best_live['w']:>10} @ {best_live['lev']:g}x -> J = {best_live['J']:.3f}")
dJ = best["J"] - best_live["J"]
se = np.sqrt(0.25 / N_PATHS) * 2
print(f" delta = {dJ:+.3f} (errore MC su livelli ~+-{se:.3f}; le due celle condividono")
print(f" gli indici di bootstrap -> il delta e' APPAIATO e il suo errore e' piu' piccolo)")
# ---------------------------------------------------------------- E. banda d'ancora
hr("E. BANDA D'ANCORA — non ri-misurata sotto questa lente (costo); si usa il FATTORE misurato")
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). 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):
cells = []
for c in dict.fromkeys(cands):
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(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}")
hr("E-bis. LO STRESS CHE DECIDE — XS01 e' stato SCOPERTO in questa finestra")
print(" L'argmax mette il 50% su XS01, il cui unico campione (2024+) e' anche quello su cui e'")
print(" stato scoperto e affinato (blend di lookback + gate di dispersione). CLAUDE.md lo")
print(" dichiara: 'la TAGLIA del guadagno e' ottimista'. Qui si taglia il DRIFT di XS01 (non la")
print(" vol, non la coda) e si guarda se la conclusione operativa sopravvive.")
print(f"\n {'drift XS01':>12} {'config':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} "
f"{'P(vivo)':>8} {'E[pay/a]':>10}")
for mult in (1.00, 0.75, 0.50, 0.25, 0.00):
dmt = (("XS01", mult),)
rows = []
for w in top_w:
for lv in LEVS:
rows.append((w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)))
w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"])
lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx,
drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"])
dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", ev_idx,
fu_idx, drift_mult=dmt)
print(f" {mult:>12.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {o_['p_pass']:>8.1%} "
f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ LIVE {oL['J']:.3f} "
f"delta {dd_:+.3f} +-{sd_:.3f}")
print("\n La colonna che decide e' l'ULTIMA. Se il vantaggio svanisce col drift di XS01, il")
print(" risultato e' un'ipotesi sul RENDIMENTO ATTESO di XS01 misurato sulla sua finestra di")
print(" scoperta; se resta a drift 0, il vantaggio e' pura DECORRELAZIONE e non dipende da")
print(" quell'ipotesi. Il numero, non la narrazione, dice quale delle due.")
hr("E-quater. LA DISTORSIONE CHE MI RIGUARDA — l'ottimo si appoggia a SKH01, e SKH01 e' lo")
print(" sleeve con la FORTUNA D'ANCORA piu' grande da restituire. Il LOO del 26/07: l'ancora")
print(" canonica gli regala 2/3 del contributo FULL (+0.43 -> +0.14) e il 70% dell'hold-out;")
print(" l'audit 02/07 lo trova al 93-98° pctl di 23 offset, 'spike, non plateau'. Questa lente")
print(" gira sull'ancora CANONICA e la banda non e' ri-misurabile qui -> il mio ottimo e'")
print(" distorto A FAVORE di SKH01 e va detto PRIMA di leggerlo. Taglio del drift di SKH01:")
print(" Le ultime due righe sono il taglio CONGIUNTO su SKH01 E XS01 (i due sleeve che il")
print(" progetto misura come piu' fortunati / piu' recenti): e' il caso peggiore onesto.")
print(f"\n {'taglio drift':>21} {'config':>16} {'leva':>6} {'J':>7} {'J del LIVE':>11} "
f"{'delta appaiato':>17}")
for dmt in ((("SKH01", 1.00),), (("SKH01", 0.66),), (("SKH01", 0.50),), (("SKH01", 0.33),),
(("SKH01", 0.50), ("XS01", 0.50)), (("SKH01", 0.33), ("XS01", 0.00))):
mult = dict(dmt)
rows = [(w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt))
for w in top_w for lv in LEVS]
w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"])
lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx,
drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"])
dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO",
ev_idx, fu_idx, drift_mult=dmt)
lab = " ".join(f"{k.replace('01','')} x{v:.2f}" for k, v in mult.items())
assert abs(dd_ - (o_["J"] - oL["J"])) < 0.05, ( # guardia contro l'errore di sopra
f"delta appaiato {dd_:+.3f} incoerente con J {o_['J']:.3f} - {oL['J']:.3f}")
print(f" {lab:>21} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {oL['J']:>11.3f} "
f"{dd_:>+11.3f} +-{sd_:.3f}")
print(" NB: il taglio si applica anche al LIVE 75/25 (che ha SKH01 al 25%), quindi il delta e'")
print(" un confronto onesto: entrambe le colonne perdono, la domanda e' quale perde di piu'.")
hr("E-ter. IL BUCO CHE LA FINESTRA NON PUO' CHIUDERE — 2024-2026 non contiene un CRASH")
print(" L'ottimizzazione toglie peso a TP01 (Sharpe 0.79 su questa finestra). Ma TP01 e' lo")
print(" sleeve DIFENSIVO: il LOO del 26/07 misura il suo contributo hold-out NEGATIVO nel 99.1%")
print(" delle ancore e conclude 'uno sleeve difensivo si giudica sul SINISTRO, non sul premio'.")
print(" Su un conto funded la barriera morde PROPRIO nel sinistro. Qui la coppia crypto (che ha")
print(" storia dal 2019, quindi il 2022 dentro) e' ri-ottimizzata sulle due finestre:")
cry_full = pc.crypto_daily_tuples(1.0, 0.0).index
W2 = [{"TP01": i / 8, "SKH01": 1 - i / 8} for i in range(9)]
print(f"\n {'finestra':>26} {'giorni':>7} {'argmax TP/SKH':>15} {'leva':>6} {'J':>7} "
f"{'J del 75/25':>12} {'Sharpe argmax':>14}")
for st, lab in (("2019-03-14", "FULL 2019+ (col 2022)"), (START_WIN, "2024+ (senza crash)")):
pairs = {}
for w in W2:
c = pc.crypto_daily_tuples(w["TP01"], w["SKH01"])
c = c[c.index >= pd.Timestamp(st, tz="UTC")]
pairs[wkey(w)] = (deluck(c["R"].values), np.minimum(c["m"].values, c["R"].values) - c["R"].values)
nh = len(next(iter(pairs.values()))[0])
ev2 = boot_idx(nh, EV_H, N_PATHS, seed=8230)
fu2 = boot_idx(nh, FU_H, N_PATHS, seed=8231)
res = []
for k, (R_, G_) in pairs.items():
for lv in LEVS:
jj = (eval_sim(R_, G_, ev2, lv, "HYRO")["p_pass"]
* funded_sim(R_, G_, fu2, lv, "HYRO", 100_000.0)["p_alive"])
res.append((k, lv, jj, R_.mean() * 365 / (R_.std() * np.sqrt(365))))
bk, blv, bj, bsh = max(res, key=lambda x: x[2])
j75 = max(x[2] for x in res if x[0] == "75/25/0")
print(f" {lab:>26} {nh:>7d} {bk.rsplit('/', 1)[0]:>15} {blv:>6.3f} {bj:>7.3f} "
f"{j75:>12.3f} {bsh:>14.2f}")
print("\n Se anche sulla finestra che CONTIENE il 2022 l'ottimo scarta TP01, la sua sotto-")
print(" pesatura non e' un artefatto di finestra. Se invece li' TP01 pesa, allora la")
print(" raccomandazione del 2024+ e' una scommessa sull'assenza di crash — e su una barriera")
print(" di rovina quella scommessa la paghi una volta sola. **Questa finestra NON puo' dirlo")
print(" per XS01**, che nasce nel 2024: il buco resta aperto per lui, per costruzione.")
# ---------------------------------------------------------------- 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")
print(" (VRP01) ne' ETF azionari USA (GTAA01, che al retail UE e' pure bloccato dal PRIIPs).")
print(" Qui si misura comunque il CONTROFATTUALE: se fossero disponibili, aiuterebbero?")
print(f"\n {'aggiunta al ' + best['w']:>34} {'leva':>6} {'J':>7} {'P(pass)':>8} "
f"{'P(vivo)':>8} {'Sharpe':>7} {'E[pay/a]':>10}")
for extra, wx in [(None, 0.0), ("VRP01", 0.10), ("VRP01", 0.20),
("GTAA01", 0.10), ("GTAA01", 0.20)]:
w2 = {k: v * (1.0 - wx) for k, v in w_best.items()}
if extra:
w2[extra] = wx
rows = [(lv, objective(w2, lv, "HYRO", ev_idx, fu_idx)) for lv in LEVS]
lv, o = max(rows, key=lambda x: x[1]["J"])
lab = "nessuna (base)" if extra is None else f"{extra} @ {wx:.0%}"
print(f" {lab:>34} {lv:>6.3f} {o['J']:>7.3f} {o['p_pass']:>8.1%} {o['p_alive']:>8.1%} "
f"{o['sharpe']:>7.2f} {o['e_payout']:>9,.0f}$")
print("\n Le righe VRP01 e GTAA01 escono quasi IDENTICHE: non e' un errore, e' che sotto questa")
print(" funzione obiettivo i due sleeve sono lo stesso oggetto (vol ~6.2%, drift ~7.7%, corr ~0")
print(" col crypto). La barriera vede vol, drift e correlazione; la differenza fra i due — skew")
print(" -4.6 contro -0.4 — a peso 10-20% e' diluita sotto la soglia di daily-loss.")
print("\n ATTENZIONE: la riga VRP01 e' calcolata con **gap = 0** (il mark infra-settimana non")
print(" esiste nel dataset dello sleeve): e' un TETTO, non una stima. Con il mark misurato il")
print(" 30/07 (peggiore infra-settimana mediano -6.9%, minimo -81.7%) una barriera STATICA al")
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 = 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()]
print(f" {fm:>6} {b['w']:>16} {b['lev']:>6.3f} {b['J']:>7.3f} {b['p_pass']:>8.1%} "
f"{b['p_alive']:>11.1%} {b['e_payout']:>9,.0f}$ | {b2['lev']:>17.3f} {b2['J']:>7.3f}")
print("\n La barriera piu' larga di FTMO sposta l'ottimo di leva verso l'ALTO: il vincolo e' la")
print(" barriera, non il libro. E' la stessa lettura del 25/07 §4 ('sul canale funded il")
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=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']}")
except Exception as exc: # pragma: no cover
print(f" non girato: {exc}")
print(" NB: questo gate giudica lo SHARPE del book di ricerca, non J. Un cambio dei pesi del")
print(" LIBRO LIVE resta soggetto a lui; qui si progetta un conto SEPARATO (funded), che non")
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 = []
fu3 = boot_idx(n_hist, 1095, N_PATHS, seed=8226)
print(f" {'pesi TP/SKH/XS':>16} {'leva':>6} {'P(pass)':>8} {'P(vivo12m)':>11} "
f"{'E[payout 3a]':>13} {'EUR/g per conto':>16} {'conti per 50/g':>15}")
for _, r in top.iterrows():
w2 = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in r["w"].split("/")]))
R_, G_ = book_RG(w2)
R_ = deluck(R_)
f3 = funded_sim(R_, G_, fu3, float(r["lev"]), "HYRO", 100_000.0)
eur_day = f3["e_payout"] / EURUSD / 3.0 / 365.0
n_acct = (TARGET_EUR_DAY / eur_day) if eur_day > 0 else np.inf
rows_ops.append((r["w"], float(r["lev"]), f3["e_payout"], eur_day, n_acct))
print(f" {r['w']:>16} {r['lev']:>6.3f} {r['p_pass']:>8.1%} {r['p_alive']:>11.1%} "
f"{f3['e_payout']:>12,.0f}$ {eur_day:>15.2f} {n_acct:>15.1f}")
print("\n E[payout 3a] e' su UN conto funded da $100k, gia' netto split 80% e fisco 33%, e")
print(" MEDIA su tutti i percorsi (i morti contano zero). 'conti per 50/g' e' quindi un numero")
print(" ATTESO, non una garanzia: la distribuzione e' bimodale (vivo / bustato).")
cap_tot = sum(f["cap"] for f in pl.FIRMS)
print(f" ⚠ TETTO STRUTTURALE: cap per trader ${FIRMS['HYRO']['cap']:,.0f} (HYRO) + "
f"${FIRMS['FTMO']['cap']:,.0f} (FTMO) = ${cap_tot:,.0f} di nozionale su due firm")
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, "
f"morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, lente ACCOPPIATA")
pc.DELUCK = DELUCK # il 25/07 girava a 0.6 (poi ri-misurato troppo severo)
cfg_best = tuple(w_best.get(nm, 0.0) for nm in CRYPTO_PROP)
cfg_live = (0.75, 0.25, 0.0)
cfg_B = (0.55, 0.20, 0.25)
pols = {
f"argmax-J {best['w']} su tutti i conti": ([cfg_best], [0] * pl.MAX_CONCURRENT),
"LIVE 75/25 su tutti i conti (baseline)": ([cfg_live], [0] * pl.MAX_CONCURRENT),
"config B 55/20/25 (scelta a mano 25/07)": ([cfg_B], [0] * pl.MAX_CONCURRENT),
}
print(f"\n {'politica':>44} {'leva':>6} {'EUR/g med':>10} {'p90':>8} {'P(>=10/g)':>10} "
f"{'P(>=50/g)':>10} {'P(zero)':>8}")
for pname, (cfgs, slots) in pols.items():
for lev in sorted({round(float(best["lev"]), 3), 0.75}):
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%}")
print("\n ⚠ QUESTO NUMERO REVISIONA UN HEADLINE DI CLAUDE.md ('EUR600 -> 50 EUR/g ~ 6% in 3")
print(" anni, P(perdere i 600) 52-65%', 25/07). Due cose sono cambiate insieme: il fattore di")
print(" de-luck (0.60 -> 0.89, ri-misurato il 26/07) e l'ALLOCAZIONE. Vanno separate, o il")
print(" merito finisce sulla leva sbagliata. Decomposizione 2x2 (stessa macchina, stesso seme):")
print(f"\n {'de-luck':>9} {'allocazione':>26} {'EUR/g med':>10} {'P(>=50/g)':>10} {'P(zero)':>8}")
for fct in (0.60, 0.89):
pc.DELUCK = fct
for lab, cfg in (("LIVE 75/25 (25/07)", cfg_live), (f"argmax-J {best['w']}", cfg_best)):
r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75,
seed=20260822, lens="coupled", start=START_WIN)
print(f" {fct:>9.2f} {lab:>26} {r['med']:>10.2f} {r['p_target']:>10.1%} "
f"{r['p_zero']:>8.1%}")
print("\n ⚠ Il headline ~6% del 25/07 NON e' la riga 'LIVE su tutti i conti' (quella li' era")
print(" CONC-2SL, dichiarata 'la PEGGIORE'): era la politica MISTA, sleeve diversi su conti")
print(" diversi. Replica ESATTA di quella politica, stessa macchina, de-luck 0.60, leva 0.75:")
pc.DELUCK = 0.60
cfgs25 = [(0.75, 0.25, 0.0), (0.55, 0.20, 0.25), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
r25 = pc.simulate(cfgs25, [1, 4, 1, 2, 1, 4], n_paths=1500, lev=0.75, seed=20260822,
lens="coupled", start=START_WIN)
print(f" MISTO (25/07) @0.75x, de-luck 0.60 : EUR/g med {r25['med']:.2f} | "
f"P(>=50/g) {r25['p_target']:.1%} | P(zero) {r25['p_zero']:.1%}")
print(f" -> il 25/07 pubblico' P(>=50/g) 5.6-6.5% e P(zero) 52% a 0.75x. "
f"{'REPLICA OK' if 0.03 <= r25['p_target'] <= 0.10 else 'NON REPLICA — leggere con cautela'}")
pc.DELUCK = DELUCK
print("\n Lettura della 2x2: il de-luck e l'allocazione contribuiscono ENTRAMBI, e vanno citati")
print(" separati. Il fattore 0.60 era una stima a occhio poi RI-MISURATA troppo severa (26/07):")
print(" usarlo ancora sarebbe sbagliato, ma il salto da 1.7% a 41.7% e' il prodotto di due")
print(" correzioni, non di una scoperta.")
print("\n" + "=" * 104)
print(f" fatto in {time.time()-t0:.0f}s")
print("=" * 104)
if __name__ == "__main__":
main()