976 lines
54 KiB
Python
976 lines
54 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.")
|
|
|
|
hr("I-ter. LA STESSA SCALA, MA CON GLI STRESS DI E-bis/E-quater DENTRO")
|
|
print(" La tabella qui sopra gira al drift PIENO di XS01 e SKH01. E-bis mostra che il vantaggio")
|
|
print(" vive quasi tutto sul drift di XS01; E-quater che il taglio congiunto lo azzera. Il")
|
|
print(" numero operativo va quindi dato con gli stress dentro, non a parte.")
|
|
print(" (meccanica: le tuple stressate vengono INIETTATE nella cache di `pc.book_tuples`, cosi'")
|
|
print(" la simulazione della scala e' letteralmente la stessa del 25/07, con un input diverso.)")
|
|
print(f"\n {'stress':>24} {'allocazione':>16} {'leva':>6} {'EUR/g med':>10} "
|
|
f"{'P(>=10/g)':>10} {'P(>=50/g)':>10} {'P(zero)':>8}")
|
|
for dmt, lab in (((), "nessuno (pieno)"),
|
|
((("SKH01", 0.50), ("XS01", 0.50)), "SKH x0.50 XS x0.50"),
|
|
((("SKH01", 0.33), ("XS01", 0.00)), "SKH x0.33 XS x0.00")):
|
|
for cfg, alab in ((cfg_best, f"argmax-J {best['w']}"), (cfg_live, "LIVE 75/25")):
|
|
wdic = dict(zip(CRYPTO_PROP, cfg))
|
|
R_, G_ = book_RG(wdic, drift_mult=dmt)
|
|
key = (float(cfg[0]), float(cfg[1]), float(cfg[2]), "shared")
|
|
saved = pc._TUP_CACHE.pop(key, None)
|
|
pc._TUP_CACHE[key] = (R_, G_, _panel_index(START_WIN))
|
|
r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75,
|
|
seed=20260822, lens="coupled", start=START_WIN)
|
|
pc._TUP_CACHE.pop(key, None)
|
|
if saved is not None:
|
|
pc._TUP_CACHE[key] = saved
|
|
print(f" {lab:>24} {alab:>16} {0.75:>6.2f} {r['med']:>10.2f} "
|
|
f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}")
|
|
print("\n Questa e' la riga da citare: non il 41.7% pieno, ma la BANDA fra la riga piena e la")
|
|
print(" riga del taglio congiunto. Lo stress non e' un'ipotesi pessimista arbitraria: e' la")
|
|
print(" fortuna d'ancora MISURATA su SKH01 (26/07) e la finestra di scoperta di XS01 (dichiarata")
|
|
print(" in CLAUDE.md come 'taglia del guadagno ottimista').")
|
|
|
|
print("\n" + "=" * 104)
|
|
print(f" fatto in {time.time()-t0:.0f}s")
|
|
print("=" * 104)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|