1113 lines
72 KiB
Python
1113 lines
72 KiB
Python
#!/usr/bin/env python
|
|
"""r0822c_worst_day.py — IL PEGGIOR GIORNO CHE IL LIBRO PUO' AVERE, misurato invece che assunto.
|
|
|
|
PERCHE' ESISTE QUESTO FILE. Il gradino di leva 1,00x -> 1,25-1,50x vale 14,7 anni -> 12,9-11,6
|
|
al capitale-rendita (~EUR300/mese di versamenti equivalenti, a costo zero) ed e' bloccato da UN
|
|
solo parametro SCELTO A MANO in `r0822_growth_policy.py` sez. (4): *"un giorno -10% all'anno
|
|
porta k* a 2x"*. Nessuno ha mai misurato quel numero. Il critico dell'ondata ha fatto un
|
|
controllo di cinque minuti (sui 10 peggiori giorni del SOTTOSTANTE il libro guadagna, e il suo
|
|
peggior giorno non e' un giorno di crash) e ha indicato la misura come quella di maggior valore
|
|
mai fatta. Qui si VERIFICA o si SMENTISCE quel controllo, e si sostituisce il -10% con una stima.
|
|
|
|
COSA C'E' QUI
|
|
(0) IL CRITERIO, dichiarato PRIMA di guardare qualunque numero delle sezioni 4-6.
|
|
(1) REPLICA DI CONTROLLO — 5 numeri gia' pubblicati, riprodotti prima di scriverne di nuovi.
|
|
(2) LA DISTRIBUZIONE DELLE CHIUSURE del libro: coda sinistra per quantili, il peggiore
|
|
assoluto, QUALI giorni sono, e l'attribuzione esatta ai due sleeve (max|diff| = 0).
|
|
(3) LA LENTE ACCOPPIATA — il minimo INTRA-giorno (`r0725_prop_coupled`). La domanda "qual e'
|
|
il peggior giorno" ha DUE risposte e servono entrambe: la leva agisce su tutte e due, ma
|
|
il margine di mantenimento guarda il minimo.
|
|
(4) LA CODA CHE NON E' NEL DATASET — EVT/POT (GPD sopra soglia dichiarata) su chiusure E su
|
|
minimi, quantili a 1/1.000 e 1/10.000 giorni, IC bootstrap A BLOCCHI sui parametri.
|
|
(5) IL PEGGIOR GIORNO POSSIBILE PER COSTRUZIONE — aritmetica, non statistica: dato il cap di
|
|
`config/live.json`, il vol-target di TP01 e il gap-through misurato di SKH01.
|
|
(6) k* ONESTO — la curva di Kelly rifatta con la coda stimata al posto di quella a mano, e la
|
|
decomposizione dell'errore del -10% (taglia vs FREQUENZA).
|
|
(7) IL VERDETTO contro il criterio di (0).
|
|
|
|
ONESTA' APPLICATA (regole gia' codificate nel progetto):
|
|
· un fattore correttivo scelto a occhio va DECOMPOSTO prima di accettarlo o rifiutarlo (26/07);
|
|
· close-only e' CIECA su una regola a UN giorno (25/07) -> la lente accoppiata e' obbligatoria;
|
|
· un fit di coda su ~2.700 osservazioni ha incertezza grande: si riporta, o si sostituisce un
|
|
numero scelto a mano con uno finto-preciso, che e' peggio;
|
|
· un Monte Carlo ha una risoluzione: qui la curva di crescita e' calcolata in forma ESATTA
|
|
(E[log]) proprio per togliere di mezzo il rumore MC dello scetticato.
|
|
|
|
nice -n 19 timeout 900 uv run python scripts/research/r0822c_worst_day.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import warnings
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
from scipy import stats
|
|
|
|
warnings.filterwarnings("ignore", message=".*Sorting by default when concatenating.*")
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
|
|
|
import r0725_capcurve as CC # noqa: E402 book_series (lente LIVE), TAX_RATE
|
|
import r0725_prop_coupled as PC # noqa: E402 recon accoppiata (R, m)
|
|
import r0726_deposits as DP # noqa: E402 DELUCK = 0.89 MISURATO
|
|
import r0724_skh_live_weight as SKL # noqa: E402 gamba SKH01 sul path LIVE
|
|
|
|
from src.data.downloader import load_data # noqa: E402
|
|
from src.strategies.trend_portfolio import ( # noqa: E402
|
|
CANONICAL, TrendPortfolio, realized_vol, resample_1d, simple_returns)
|
|
from src.strategies.skyhook import SKH01_V2_DD # noqa: E402
|
|
|
|
ASSETS = ("BTC", "ETH")
|
|
W_TP, W_SKH = 0.75, 0.25
|
|
ANN = 365.0
|
|
SEED = 20260822
|
|
|
|
# ---- parametri EVT DICHIARATI PRIMA (criterio, non tuning) -------------------------------
|
|
EVT_Q = 0.95 # soglia = 95° pctl delle PERDITE. Criterio dichiarato: la scelta
|
|
# classica del compromesso bias/varianza (~5% di eccedenze, qui 136
|
|
# osservazioni). Le altre soglie sono girate come SENSITIVITA', non
|
|
# per scegliere la migliore: se il numero si muove molto, si dice.
|
|
EVT_Q_SENS = (0.90, 0.95, 0.975, 0.99)
|
|
EVT_BOOT = 400 # ricampionamenti a BLOCCHI (la serie ha vol-clustering: un bootstrap
|
|
EVT_BLOCK = 20 # iid stringerebbe l'IC di una coda che non e' iid)
|
|
RP = (1_000, 10_000) # periodi di ritorno in GIORNI (2,7 anni e 27 anni)
|
|
|
|
# ---- il numero sotto esame ----------------------------------------------------------------
|
|
STRESS_A_MANO = 0.10 # il "-10% in un giorno" di r0822_growth_policy sez.(4)
|
|
STRESS_FREQ_A_MANO = 1.0 # ...assunto UNA VOLTA L'ANNO (e 1 ogni 5 anni come variante)
|
|
|
|
# ---- costanti di venue (dichiarate, fonte in linea) --------------------------------------
|
|
MM_RATE = 0.01 # maintenance margin ~1% (Deribit perp lineari, valore prudente:
|
|
# il listino parte da 0,5% e cresce con la size; 1% e' conservativo
|
|
# NEL VERSO che rende la liquidazione PIU' vicina)
|
|
WIPE = 0.90 # convenzione ereditata da growth_policy/skeptic: perdita giornaliera
|
|
# >= 90% dell'equity = conto chiuso. Usata solo per replicare i loro numeri.
|
|
|
|
|
|
# ==========================================================================================
|
|
# (0) IL CRITERIO — congelato qui, valutato in (7). Nessuna delle sue soglie usa un numero
|
|
# delle sezioni 4-6: vengono dalle regole gia' scritte del progetto e dal listino del venue.
|
|
# ==========================================================================================
|
|
CRITERIO = {
|
|
"C1": ("STRUTTURALE — il peggior giorno POSSIBILE per costruzione, a leva k, deve lasciare "
|
|
"in piedi almeno META' dell'equity: k * |L_strutturale| <= 0.50. "
|
|
"(Ragione: zero e' assorbente e il conto e' unico; sotto meta' equity il piano non "
|
|
"e' 'in ritardo', e' finito.)"),
|
|
"C2": ("KELLY PESSIMISTICO — k <= 1/2 * k*(drift a -2 errori standard E coda troncata al "
|
|
"bound strutturale). Il mezzo-Kelly e' la prescrizione classica ed e' scelta A PRIORI, "
|
|
"non tarata su questo risultato; il -2 SE viene dalla fragilita' gia' misurata "
|
|
"(r0822_growth_policy sez. 1-bis: k* e' LINEARE nell'errore del drift)."),
|
|
"C3": ("LIQUIDAZIONE SUL MINIMO — con la lente ACCOPPIATA, il quantile a 1/10.000 giorni del "
|
|
"minimo INTRA-giorno a leva k deve stare almeno 3x lontano dalla soglia di "
|
|
"liquidazione del venue. (close-only e' CIECA su una regola a un giorno: 25/07.)"),
|
|
"C4": ("GUADAGNO ROBUSTO — g(k) - g(1.00) deve restare POSITIVO col drift a -2 SE. "
|
|
"Se il gradino paga solo al drift di punto, non e' un gradino, e' una scommessa "
|
|
"sulla stima del drift."),
|
|
"C5": ("COSTI NON MODELLATI — il costo che scala con k e non e' nei backtest (funding dei "
|
|
"perpetual, costo del margine oltre 1x) deve essere piccolo rispetto al guadagno."),
|
|
"C6": ("PROCESSO — deve ESISTERE una strada pulita per un puro cambio di SCALA. Regola del "
|
|
"progetto: passa dal cap di config, non da `target_vol` (che e' anche un tilt di pesi "
|
|
"e fallisce `weights_tilt_null`). Se quella strada non esiste, il gradino non e' "
|
|
"autorizzabile *nella forma in cui e' stato proposto*, qualunque cosa dicano C1-C5."),
|
|
}
|
|
|
|
|
|
def sezione(t: str) -> None:
|
|
print("\n" + "=" * 110)
|
|
print(f" {t}")
|
|
print("=" * 110)
|
|
|
|
|
|
def pct(x: float) -> str:
|
|
return f"{x:+.2%}" if np.isfinite(x) else " n/d"
|
|
|
|
|
|
# ==========================================================================================
|
|
# DATI
|
|
# ==========================================================================================
|
|
def serie_libro() -> dict:
|
|
"""Serie giornaliere del libro live e le sue due gambe, + il sottostante."""
|
|
book = CC.book_series("hourly")
|
|
skh = SKL.skh_book(0, "hourly")
|
|
if skh.index.tz is None:
|
|
skh.index = skh.index.tz_localize("UTC")
|
|
tp = CC.tp01_realistic(600.0 * W_TP)
|
|
und = {}
|
|
for a in ASSETS:
|
|
df = load_data(a, "1h")
|
|
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
|
|
s = pd.Series(df["close"].values.astype(float), index=idx).resample("1D").last()
|
|
und[a] = s.pct_change()
|
|
J = pd.concat({"book": book, "tp_leg": W_TP * tp, "skh_leg": W_SKH * skh,
|
|
"BTC": und["BTC"], "ETH": und["ETH"]}, axis=1, sort=True).dropna()
|
|
J["und"] = 0.5 * J["BTC"] + 0.5 * J["ETH"]
|
|
deluck = book - (1.0 - DP.DELUCK) * float(book.values.mean()) # = DP.deluck_returns(), con indice
|
|
return {"J": J, "deluck": deluck}
|
|
|
|
|
|
def gambe_tp01() -> dict:
|
|
"""Per asset: peso TENUTO da TP01, ritorno giornaliero, vol nota a close[d-1], z-score.
|
|
|
|
z = r / (vol_ann_prev/sqrt(365.25)) e' la grandezza che rende STRUTTURALE il bound di TP01:
|
|
il contributo di gamba e' `signal * (target_vol/vol) * r = signal * target_vol * z / sqrt(365.25)`,
|
|
cioe' INVARIANTE alla volatilita'. Un vol-target converte un crash in uno z-score.
|
|
"""
|
|
out = {}
|
|
for a in ASSETS:
|
|
df = resample_1d(load_data(a, "1h"))
|
|
c = df["close"].values.astype(float)
|
|
r = simple_returns(c)
|
|
tgt = np.nan_to_num(np.asarray(TrendPortfolio(**CANONICAL).target_series(df), float))
|
|
held = np.zeros(len(tgt))
|
|
held[1:] = tgt[:-1]
|
|
vol = realized_vol(r, CANONICAL["vol_win_days"], 365.25)
|
|
volp = np.concatenate(([np.nan], vol[:-1]))
|
|
with np.errstate(divide="ignore", invalid="ignore"):
|
|
z = r / (volp / np.sqrt(365.25))
|
|
lev_max = np.clip(np.where(volp > 0, CANONICAL["target_vol"] / volp, 0.0),
|
|
0.0, CANONICAL["leverage"])
|
|
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
|
|
prev = np.concatenate(([np.nan], c[:-1]))
|
|
r_low = df["low"].values.astype(float) / prev - 1.0 # escursione avversa INTRA-giorno
|
|
out[a] = pd.DataFrame({"held": held, "ret": r, "ret_low": r_low, "vol_prev": volp,
|
|
"z": z, "lev_max": lev_max}, index=idx)
|
|
return out
|
|
|
|
|
|
def estremi_orari() -> dict:
|
|
"""Movimento orario piu' avverso mai visto, per asset e per verso. E' il bound del
|
|
gap-through di SKH01: gli exit sono SOFTWARE e il fill arriva al close orario successivo
|
|
al tocco, quindi la perdita oltre lo stop e' l'escursione DENTRO quell'ora."""
|
|
out = {}
|
|
for a in ASSETS:
|
|
c = load_data(a, "1h")["close"].values.astype(float)
|
|
r = c[1:] / c[:-1] - 1.0
|
|
out[a] = {"giu": float(r.min()), "su": float(r.max())}
|
|
return out
|
|
|
|
|
|
# ==========================================================================================
|
|
# EVT / POT
|
|
# ==========================================================================================
|
|
def gpd_fit(loss: np.ndarray, q: float) -> dict:
|
|
"""GPD (peaks-over-threshold) sulle PERDITE. `loss` = -ritorni. Ritorna anche le funzioni
|
|
di quantile e di probabilita' di eccedenza in forma chiusa."""
|
|
n = len(loss)
|
|
u = float(np.quantile(loss, q))
|
|
ex = loss[loss > u] - u
|
|
nu = len(ex)
|
|
if nu < 15:
|
|
return {}
|
|
xi, _, beta = stats.genpareto.fit(ex, floc=0.0)
|
|
return {"u": u, "n": n, "nu": nu, "xi": float(xi), "beta": float(beta),
|
|
"rate": nu / n}
|
|
|
|
|
|
def gpd_quantile(f: dict, p_giorno: float) -> float:
|
|
"""Perdita superata con probabilita' `p_giorno` in un giorno (return level)."""
|
|
if not f:
|
|
return np.nan
|
|
xi, beta, u, rate = f["xi"], f["beta"], f["u"], f["rate"]
|
|
if abs(xi) < 1e-9:
|
|
return u + beta * np.log(rate / p_giorno)
|
|
return u + beta / xi * ((p_giorno / rate) ** (-xi) - 1.0)
|
|
|
|
|
|
def gpd_exceed(f: dict, x: float) -> float:
|
|
"""P(perdita > x) al giorno, per x >= u."""
|
|
if not f or x <= f["u"]:
|
|
return np.nan
|
|
xi, beta, u, rate = f["xi"], f["beta"], f["u"], f["rate"]
|
|
if abs(xi) < 1e-9:
|
|
return rate * np.exp(-(x - u) / beta)
|
|
base = 1.0 + xi * (x - u) / beta
|
|
if base <= 0:
|
|
return 0.0
|
|
return rate * base ** (-1.0 / xi)
|
|
|
|
|
|
def evt_bootstrap(loss: np.ndarray, q: float, n_boot: int, block: int,
|
|
rng: np.random.Generator) -> dict:
|
|
"""IC bootstrap A BLOCCHI su xi, beta e sui return level. A blocchi perche' la serie ha
|
|
vol-clustering: un bootstrap iid darebbe un IC piu' stretto del vero."""
|
|
n = len(loss)
|
|
nb = int(np.ceil(n / block))
|
|
xis, rl = [], {p: [] for p in RP}
|
|
for _ in range(n_boot):
|
|
st = rng.integers(0, n - block, size=nb)
|
|
idx = (st[:, None] + np.arange(block)[None, :]).ravel()[:n]
|
|
try:
|
|
f = gpd_fit(loss[idx], q)
|
|
except Exception:
|
|
continue
|
|
if not f:
|
|
continue
|
|
xis.append(f["xi"])
|
|
for p in RP:
|
|
rl[p].append(gpd_quantile(f, 1.0 / p))
|
|
return {"xi": np.array(xis), **{f"rl{p}": np.array(rl[p]) for p in RP}}
|
|
|
|
|
|
# ==========================================================================================
|
|
# CURVA DI CRESCITA — forma ESATTA (nessun rumore MC)
|
|
# ==========================================================================================
|
|
def g_exact(r: np.ndarray, k: float) -> float:
|
|
"""Crescita geometrica annua a leva k: exp(365 * E[log(1+k r)]) - 1.
|
|
Su orizzonti lunghi la mediana del moltiplicatore converge a questa quantita': e' la
|
|
stessa grandezza che lo scetticato stima in Monte Carlo, qui in forma chiusa."""
|
|
x = 1.0 + k * r
|
|
if np.any(x <= 0):
|
|
return -np.inf
|
|
return float(np.expm1(ANN * np.mean(np.log(x))))
|
|
|
|
|
|
def k_star(r: np.ndarray, kmax: float = 40.0, step: float = 0.05) -> tuple[float, float]:
|
|
ks = np.arange(step, kmax + step, step)
|
|
g = np.array([g_exact(r, k) for k in ks])
|
|
j = int(np.nanargmax(g))
|
|
return float(ks[j]), float(g[j])
|
|
|
|
|
|
def campione_semiparam(r: np.ndarray, f: dict, l_max: float, n_draw: int,
|
|
drift_annuo: float, rnd: dict) -> np.ndarray:
|
|
"""Corpo EMPIRICO + coda GPD TRONCATA a `l_max`, ri-centrato al drift dichiarato.
|
|
|
|
La troncatura non e' un artificio di comodo: una GPD con xi>0 ha supporto illimitato, quindi
|
|
E[log(1+kR)] sarebbe -inf per QUALSIASI k>0 e la domanda non avrebbe risposta. Il supporto
|
|
lo da' l'aritmetica della sezione (5), non la statistica: e' esattamente il posto in cui i
|
|
due pezzi di questo lavoro si incastrano.
|
|
"""
|
|
loss = -r
|
|
body = r[loss <= f["u"]]
|
|
out = body[rnd["ib"] % len(body)] # stessi indici in OGNI cella
|
|
m = rnd["um"] < f["rate"] # stessa maschera
|
|
nt = int(m.sum())
|
|
if nt:
|
|
fmax = stats.genpareto.cdf(max(l_max - f["u"], 1e-9), f["xi"], loc=0.0, scale=f["beta"])
|
|
y = stats.genpareto.ppf(rnd["ut"][:nt] * fmax, f["xi"], loc=0.0, scale=f["beta"])
|
|
out = out.copy()
|
|
out[m] = -(f["u"] + y)
|
|
return out - out.mean() + drift_annuo / ANN
|
|
|
|
|
|
# ==========================================================================================
|
|
def main() -> None:
|
|
t_start = time.time()
|
|
rng = np.random.default_rng(SEED)
|
|
print("=" * 110)
|
|
print(" r0822c — WORST-DAY: il peggior giorno del libro, MISURATO (chiusura e minimo), e")
|
|
print(" la coda che non e' nel dataset, STIMATA invece che scelta a mano")
|
|
print("=" * 110)
|
|
|
|
# ---------------------------------------------------------------- (0)
|
|
sezione("(0) IL CRITERIO — dichiarato PRIMA, valutato in (7)")
|
|
print("\n Il gradino di leva a 1,25x e' AUTORIZZABILE solo se passano TUTTE queste sei.")
|
|
print(" Nessuna soglia qui sotto usa un numero delle sezioni 4-6: vengono dalle regole")
|
|
print(" gia' scritte del progetto e dal listino del venue.\n")
|
|
for k, v in CRITERIO.items():
|
|
testo = v.replace(" — ", " — ")
|
|
print(f" {k}. {testo}")
|
|
print()
|
|
|
|
# ---------------------------------------------------------------- (1)
|
|
sezione("(1) REPLICA DI CONTROLLO — cinque numeri gia' pubblicati, riprodotti prima di scriverne di nuovi")
|
|
D = serie_libro()
|
|
J = D["J"]
|
|
TPQ_pre = gambe_tp01() # serve gia' nella sez.(2) per l'esposizione per bucket
|
|
book = J["book"].values.astype(float)
|
|
r_del = DP.deluck_returns()
|
|
mu, sig = float(r_del.mean()), float(r_del.std())
|
|
se_drift = sig * np.sqrt(ANN) / np.sqrt(len(r_del) / ANN)
|
|
|
|
print(f"\n serie: {len(J)} giorni, {J.index.min().date()} -> {J.index.max().date()}")
|
|
print(f"\n (a) GROWTH-POLICY sez.(0) — serie de-luckata x{DP.DELUCK}")
|
|
print(f" {'grandezza':>22}{'pubblicato':>14}{'qui':>14}")
|
|
for nome, pub, qui in (("drift/anno", "17,11%", f"{mu*ANN:.2%}"),
|
|
("vol/anno", "11,37%", f"{sig*np.sqrt(ANN):.2%}"),
|
|
("Sharpe", "1,51", f"{mu/sig*np.sqrt(ANN):.2f}"),
|
|
("peggior giorno", "-3,95%", f"{r_del.min():.2%}")):
|
|
print(f" {nome:>22}{pub:>14}{qui:>14}")
|
|
|
|
print(f"\n (b) SCOMPOSIZIONE ESATTA del libro nelle due gambe (non e' una stima: e' identita')")
|
|
diff = np.abs(J["book"].values - (J["tp_leg"].values + J["skh_leg"].values)).max()
|
|
print(f" max|book - (0.75*TP01 + 0.25*SKH01)| = {diff:.2e} <- 0 esatto, la gamba"
|
|
f" responsabile di ogni giorno e' LEGGIBILE")
|
|
|
|
print(f"\n (c) k* — lo scetticato lo stima in Monte Carlo (banda 10-14x, 'la POSIZIONE non e'")
|
|
print(f" risolta'). Qui la stessa quantita' in forma CHIUSA, senza rumore MC:")
|
|
ks_emp, gs_emp = k_star(r_del)
|
|
print(f" {'Kelly gaussiano mu/sigma²':>34}{mu/sig**2:>10.2f}x")
|
|
print(f" {'k* ESATTO argmax E[log(1+kR)]':>34}{ks_emp:>10.2f}x (g = {gs_emp:.1%}/anno)")
|
|
print(f" {'MC dello scetticato':>34}{'10-14x':>10} <- la forma chiusa cade DENTRO")
|
|
print(f" {'g(1.00x)':>34}{g_exact(r_del,1.0):>10.2%}/anno")
|
|
print(f" -> replica superata: la macchineria esatta e quella MC dicono la stessa cosa,")
|
|
print(f" e da qui in avanti il rumore MC non e' piu' una scusa per nessun numero.")
|
|
|
|
# ---- il controllo del critico, rifatto in modo indipendente
|
|
print(f"\n (d) IL CONTROLLO DEL CRITICO, verificato: i 10 peggiori giorni del SOTTOSTANTE")
|
|
print(f" (media 50/50 BTC-ETH) e cosa ha fatto il libro in quei giorni.")
|
|
worst_und = J.sort_values("und").head(10)
|
|
print(f"\n {'data':>12}{'BTC':>10}{'ETH':>10}{'sottost.':>10}{'LIBRO':>10}"
|
|
f"{' gamba TP01':>13}{'gamba SKH01':>13}")
|
|
for d, row in worst_und.iterrows():
|
|
print(f" {str(d.date()):>12}{pct(row.BTC):>10}{pct(row.ETH):>10}{pct(row.und):>10}"
|
|
f"{pct(row.book):>10}{pct(row.tp_leg):>13}{pct(row.skh_leg):>13}")
|
|
print(f"\n somma dei 10 giorni: LIBRO {worst_und.book.sum():+.2%} "
|
|
f"contro sottostante {worst_und.und.sum():+.2%}")
|
|
print(f" giorni in cui il libro GUADAGNA: {int((worst_und.book>0).sum())}/10 · "
|
|
f"peggiore dei dieci {worst_und.book.min():+.2%}")
|
|
print(f"\n -> IL CONTROLLO DEL CRITICO E' CONFERMATO NELLA SOSTANZA. Il 12 marzo 2020")
|
|
print(f" (BTC {J.loc[J.und.idxmin(),'BTC']:+.1%}, ETH {J.loc[J.und.idxmin(),'ETH']:+.1%}) il libro fa "
|
|
f"{J.und.min() and J.loc[J.und.idxmin(),'book']:+.2%}.")
|
|
|
|
# ---- il numero -3,38% del critico
|
|
b_can = CC.book_series("canonical")
|
|
print(f"\n (e) ⚠️ IL NUMERO DEL CRITICO NON SI RIPRODUCE — ma la sua LENTE si identifica,")
|
|
print(f" e la conclusione si rovescia proprio su quella.")
|
|
print(f" Il critico scrive 'peggior giorno assoluto -3,38%'. Le lenti del progetto danno:")
|
|
print(f" {'lente':>34}{'peggior giorno':>16}{'data':>14} che giorno era")
|
|
for nome, s in (("LIVE (book_series 'hourly')", J['book']),
|
|
("CANONICA (book_series 'canonical')", b_can),
|
|
("LIVE de-luckata x0.89", D["deluck"])):
|
|
i = s.idxmin()
|
|
u = J["und"].reindex([i]).iloc[0] if i in J.index else np.nan
|
|
tag = "RIMBALZO" if u > 0.05 else ("CRASH" if u < -0.05 else "giorno ordinario")
|
|
print(f" {nome:>34}{s.min():>16.2%}{str(i.date()):>14} {tag} (sottost. {u:+.1%})")
|
|
Jc = pd.concat({"b": b_can, "u": J["und"]}, axis=1).dropna()
|
|
wc = Jc.sort_values("u").head(10)["b"].values * 100
|
|
wl = J.sort_values("und").head(10)["book"].values * 100
|
|
print(f"\n LA LENTE DEL CRITICO SI PUO' IDENTIFICARE dai valori che ha pubblicato")
|
|
print(f" ('+0,97 · +0,72 · 0,00 · -1,98 · -2,24 · +0,23'). Sui 10 peggiori giorni del")
|
|
print(f" sottostante le due lenti danno:")
|
|
print(f" LIVE : " + " ".join(f"{x:+.2f}" for x in wl))
|
|
print(f" CANONICA : " + " ".join(f"{x:+.2f}" for x in wc))
|
|
print(f" -> **il +0,97 e il +0,72 sono nella riga CANONICA e in nessun'altra**: il critico")
|
|
print(f" ha usato `book_series('canonical')`, cioe' il BACKTEST di SKH01 (exit al")
|
|
print(f" livello), non il path LIVE. Il suo peggior giorno resta {b_can.min():.2%} contro il")
|
|
print(f" -3,38% pubblicato: **0,09pp di scarto**, dell'ordine del difetto gia' noto")
|
|
print(f" nel progetto ('`data/raw` e' gitignored e il cron riscrive i parquet ogni")
|
|
print(f" notte -> stesso codice, dati diversi', XS-LITE e GTAA/TLT).")
|
|
print(f"\n -> **QUINDI LA SUA CONCLUSIONE SI ROVESCIA SULLA SUA STESSA LENTE.** Sulla")
|
|
print(f" canonica — quella che ha usato — il peggior giorno E' un giorno di ribasso")
|
|
print(f" ({b_can.idxmin().date()}, sottostante {J['und'].reindex([b_can.idxmin()]).iloc[0]:+.1%}). La frase 'il suo peggior giorno")
|
|
print(f" assoluto non e' un giorno di crash' e' VERA solo sulla lente LIVE, che e'")
|
|
print(f" quella giusta per una decisione operativa ma non e' quella da cui viene il")
|
|
print(f" suo numero. **Aveva ragione, con la lente sbagliata.**")
|
|
|
|
# ---------------------------------------------------------------- (2)
|
|
sezione("(2) LA DISTRIBUZIONE COMPLETA DELLE CHIUSURE DEL LIBRO — e QUALI giorni sono i peggiori")
|
|
qs = (50, 25, 10, 5, 2.5, 1, 0.5, 0.2, 0.1)
|
|
print(f"\n (a) coda SINISTRA per quantili (lente LIVE, {len(book)} giorni = {len(book)/365:.1f} anni)")
|
|
print(f" {'quantile':>12}" + "".join(f"{f'p{q}':>10}" for q in qs) + f"{'min':>10}")
|
|
print(f" {'ritorno':>12}" + "".join(f"{np.percentile(book,q):>10.2%}" for q in qs)
|
|
+ f"{book.min():>10.2%}")
|
|
print(f" {'(destra)':>12}" + "".join(f"{np.percentile(book,100-q):>10.2%}" for q in qs)
|
|
+ f"{book.max():>10.2%}")
|
|
print(f"\n asimmetria {stats.skew(book):+.2f} · curtosi in eccesso {stats.kurtosis(book):+.1f}"
|
|
f" · giorni flat (|r|<1bps) {(np.abs(book)<1e-4).mean():.1%}")
|
|
print(f" ⚠️ la curtosi alta NON e' di per se' un allarme: la meta' dei giorni il libro e'")
|
|
print(f" quasi fermo (TP01 long-flat, SKH01 fuori mercato l'87,6% delle barre), e una")
|
|
print(f" distribuzione con un picco a zero e' leptocurtica per costruzione.")
|
|
|
|
print(f"\n (b) i 15 PEGGIORI GIORNI del libro, con l'attribuzione esatta e cosa faceva il mercato")
|
|
w = J.sort_values("book").head(15)
|
|
print(f"\n {'data':>12}{'LIBRO':>9}{'TP01':>9}{'SKH01':>9}{'BTC':>9}{'ETH':>9} tipo di giornata")
|
|
for d, row in w.iterrows():
|
|
if row.und > 0.05:
|
|
tag = "RIMBALZO (short squeeze)"
|
|
elif row.und < -0.05:
|
|
tag = "crash"
|
|
elif row.und < -0.02:
|
|
tag = "ribasso"
|
|
else:
|
|
tag = "giorno ordinario"
|
|
colpa = "TP01" if row.tp_leg < row.skh_leg else "SKH01"
|
|
print(f" {str(d.date()):>12}{row.book:>9.2%}{row.tp_leg:>9.2%}{row.skh_leg:>9.2%}"
|
|
f"{row.BTC:>9.1%}{row.ETH:>9.1%} {tag} [{colpa}]")
|
|
|
|
# meccanismo
|
|
crash = J[J.und < -0.05]
|
|
rimb = J[J.und > 0.05]
|
|
ord_ = J[(J.und >= -0.02) & (J.und <= 0.02)]
|
|
print(f"\n (c) IL MECCANISMO — il libro per tipo di giornata")
|
|
print(f" {'tipo':>28}{'giorni':>8}{'media':>10}{'peggiore':>11}{'quota TP01':>12}{'quota SKH01':>13}")
|
|
for nome, sub in (("CRASH (sottost. < -5%)", crash), ("RIMBALZO (sottost. > +5%)", rimb),
|
|
("ordinario (|sottost.|<2%)", ord_), ("tutti", J)):
|
|
if len(sub) == 0:
|
|
continue
|
|
print(f" {nome:>28}{len(sub):>8}{sub.book.mean():>10.3%}{sub.book.min():>11.2%}"
|
|
f"{sub.tp_leg.mean():>12.3%}{sub.skh_leg.mean():>13.3%}")
|
|
print(f"\n (d) ⚠️ QUI IL CONTROLLO DEL CRITICO VA CORRETTO, e la correzione E' il meccanismo.")
|
|
print(f" Sui **10 giorni piu' estremi** il libro guadagna (sopra). Ma sulla popolazione")
|
|
print(f" dei **160 giorni di crash** perde in media {crash.book.mean():+.3%}: la frase")
|
|
print(f" 'il libro guadagna nei crash' e' vera sugli estremi e FALSA in generale.")
|
|
print(f" La ragione e' misurabile — l'esposizione di TP01 DIPENDE dalla taglia del crollo:")
|
|
print(f" {'sottostante':>22}{'giorni':>8}{'espos. TP01':>14}{'libro medio':>14}"
|
|
f"{'peggiore':>11}{'protezione':>12}")
|
|
bucket = [(-1.00, -0.20, "< -20%"), (-0.20, -0.10, "-20% .. -10%"),
|
|
(-0.10, -0.05, "-10% .. -5%"), (-0.05, -0.02, "-5% .. -2%")]
|
|
espo = pd.concat({a: TPQ_pre[a]["held"] for a in ASSETS}, axis=1).mean(axis=1) * 0.5 * W_TP * 2
|
|
Jx = J.join(espo.rename("espo"), how="left")
|
|
for lo, hi, lbl in bucket:
|
|
sub = Jx[(Jx.und > lo) & (Jx.und <= hi)]
|
|
if len(sub) == 0:
|
|
continue
|
|
cell = "GUADAGNA" if sub.book.mean() > 0 else f"{abs(sub.und.mean()/sub.book.mean()):.0f}x"
|
|
print(f" {lbl:>22}{len(sub):>8}{sub.espo.mean():>14.3f}{sub.book.mean():>14.3%}"
|
|
f"{sub.book.min():>11.2%}{cell:>12}")
|
|
print(f"\n -> **il trend-follower e' gia' FLAT quando il crollo e' grande, ed e' ancora")
|
|
print(f" LONG quando e' medio**: l'esposizione scende in modo monotono al crescere del")
|
|
print(f" crollo, perche' un crollo grande arriva dopo che il trend a 30/90/180 giorni")
|
|
print(f" si e' gia' girato (e il vol-target ha gia' de-levered). Il rapporto di")
|
|
print(f" protezione peggiora nella fascia mite e migliora negli estremi. **Il libro")
|
|
print(f" non e' immune ai crash: e' immune ai crash GROSSI.**")
|
|
print(f"\n E nei RIMBALZI il libro guadagna in media ({rimb.book.mean():+.3%}) MA ci tiene il")
|
|
print(f" suo peggior giorno assoluto ({rimb.book.min():+.2%}): media e coda vanno in")
|
|
print(f" direzioni opposte. **La coda di questo libro non sta dove sta quella del")
|
|
print(f" mercato.** Il -10% di growth_policy e' una coda da buy&hold applicata a un")
|
|
print(f" libro difensivo: l'evento e' quello sbagliato, non solo la taglia.")
|
|
i_worst = J["book"].idxmin()
|
|
rw = J.loc[i_worst]
|
|
print(f"\n il peggior giorno in dettaglio: {i_worst.date()} libro {rw.book:+.2%}")
|
|
print(f" BTC {rw.BTC:+.1%} · ETH {rw.ETH:+.1%} (il giorno DOPO il -43,6% del 12 marzo)")
|
|
print(f" TP01 {rw.tp_leg:+.2%} (flat: un trend-follower long-only non compra un crash)")
|
|
print(f" SKH01 {rw.skh_leg:+.2%} = uno short con sl 2% che GAPPA attraverso lo stop")
|
|
skh_only = (J['skh_leg'] / W_SKH)
|
|
print(f" a livello di sleeve nudo SKH01 fa {skh_only.min():+.2%} in quel giorno,")
|
|
print(f" contro un secondo peggiore di {skh_only.nsmallest(2).iloc[-1]:+.2%}: **la coda")
|
|
print(f" di SKH01 e' UN giorno solo**, e quel giorno e' un rimbalzo, non un crollo.")
|
|
|
|
# ---------------------------------------------------------------- (3)
|
|
sezione("(3) LA LENTE ACCOPPIATA — il minimo INTRA-giorno (close-only e' CIECA su una regola a un giorno)")
|
|
tup = PC.crypto_daily_tuples(W_TP, W_SKH)
|
|
K = pd.concat({"R_live": J["book"], "R_rec": tup["R"], "m_rec": tup["m"],
|
|
"und": J["und"]}, axis=1).dropna()
|
|
gap = (K["m_rec"] - K["R_rec"]).values.astype(float)
|
|
m_acc = K["R_live"].values + gap # costruzione dello scettico: UN grado di liberta'
|
|
m_rec = K["m_rec"].values # sensitivita': recon pura
|
|
print(f"\n (a) COSTRUZIONE DICHIARATA (la stessa di r0822_leverage_skeptic, per confrontabilita'):")
|
|
print(f" chiusure dalla serie LIVE + GAP dalla recon ACCOPPIATA, appaiato per GIORNO.")
|
|
print(f" La recon pura (R e m entrambi canonici) e' riportata come SENSITIVITA'.")
|
|
print(f" n = {len(K)} giorni.")
|
|
print(f"\n (b) REPLICA delle marginali del gap (25/07 e scettico 22/08: p50 -0,16 / p90 -1,03 / p99 -2,71)")
|
|
print(f" {'':>14}{'p50':>10}{'p90':>10}{'p99':>10}{'min':>10}")
|
|
print(f" {'qui':>14}{np.percentile(gap,50)*100:>9.2f}pp{np.percentile(gap,10)*100:>9.2f}pp"
|
|
f"{np.percentile(gap,1)*100:>9.2f}pp{gap.min()*100:>9.2f}pp")
|
|
print(f" (i percentili del GAP si leggono dal basso: p90 del |gap| = p10 del gap firmato)")
|
|
|
|
print(f"\n (c) LE DUE RISPOSTE ALLA DOMANDA 'qual e' il peggior giorno'")
|
|
print(f" {'':>26}{'p1':>10}{'p0.5':>10}{'p0.1':>10}{'MINIMO':>11}{'data del minimo':>18}")
|
|
for nome, arr in (("CHIUSURA (live)", K["R_live"].values),
|
|
("MINIMO intra-giorno (acc.)", m_acc),
|
|
("MINIMO (recon pura, sens.)", m_rec)):
|
|
i = int(np.argmin(arr))
|
|
print(f" {nome:>26}{np.percentile(arr,1):>10.2%}{np.percentile(arr,0.5):>10.2%}"
|
|
f"{np.percentile(arr,0.1):>10.2%}{arr.min():>11.2%}{str(K.index[i].date()):>18}")
|
|
neg = K["R_live"].values < -1e-4
|
|
print(f"\n rapporto minimo/chiusura sul peggior giorno: "
|
|
f"{m_acc.min()/K['R_live'].values.min():.2f}x · mediana sui soli giorni in perdita "
|
|
f"({int(neg.sum())} giorni) {np.median(m_acc[neg]/K['R_live'].values[neg]):.2f}x")
|
|
print(f" -> **il peggior giorno del libro e' -{abs(K['R_live'].min()):.2%} in chiusura e")
|
|
print(f" -{abs(m_acc.min()):.2%} al minimo**. Le due risposte differiscono di un fattore ~2:")
|
|
print(f" chi valuta una regola sul MINIMO (margine, daily-loss, stop di conto) e usa")
|
|
print(f" la chiusura sbaglia di quel fattore, e nel verso ottimista.")
|
|
|
|
print(f"\n (d) ⚠️ LIMITE DICHIARATO DELLA COSTRUZIONE, e va detto proprio sul giorno che conta.")
|
|
j = int(np.argmin(K["R_live"].values))
|
|
print(f" La recon accoppiata usa la convenzione CANONICA di SKH01 (exit AL LIVELLO): nel")
|
|
print(f" giorno del gap-through cappa il wick allo stop. Il {K.index[j].date()} la chiusura")
|
|
print(f" live e' {K['R_live'].values[j]:+.2%} e quella canonica {K['R_rec'].values[j]:+.2%}"
|
|
f" (differenza {K['R_live'].values[j]-K['R_rec'].values[j]:+.2%}), mentre il gap canonico")
|
|
print(f" di quel giorno vale solo {gap[j]*100:.2f}pp -> il minimo LIVE di quel giorno e'")
|
|
print(f" plausibilmente piu' profondo di {m_acc[j]:+.2%}. **Il numero riportato sopra e' un")
|
|
print(f" LIMITE SUPERIORE (ottimista) del minimo nel giorno peggiore**, non una misura.")
|
|
print(f" Non lo si ripara qui: servirebbe rifare `skh_hourly_contrib` con l'uscita live,")
|
|
print(f" cioe' cambiare la macchineria sotto esame. Si dichiara, e la sezione (5) da' il")
|
|
print(f" bound per via aritmetica, che non ha questo problema.")
|
|
|
|
print(f"\n (e) CONTROLLO: il gap e' piu' profondo nei giorni che finiscono BENE? (finding 25/07)")
|
|
dec = pd.qcut(K["R_live"], 10, labels=False, duplicates="drop")
|
|
gp = pd.Series(gap, index=K.index).groupby(dec).mean()
|
|
print(f" gap medio per decile di chiusura (0 = peggiore). ⚠️ i decili collassano a"
|
|
f" {gp.shape[0]} bin")
|
|
print(f" perche' il {(np.abs(book)<1e-4).mean():.0%} dei giorni ha chiusura esattamente zero:")
|
|
print(" " + " ".join(f"d{int(i)}:{v*100:+.2f}" for i, v in gp.items()))
|
|
print(f" -> replica il verso del finding (il gap non e' concentrato nei giorni brutti):")
|
|
print(f" il decile PEGGIORE ha gap {gp.iloc[0]*100:+.2f}pp contro {gp.iloc[-1]*100:+.2f}pp"
|
|
f" del migliore.")
|
|
print(f" **E la conseguenza per NOI e' quella buona: un giorno di crollo chiude quasi sul")
|
|
print(f" proprio minimo, quindi la lente accoppiata NON peggiora lo stress di coda.**")
|
|
|
|
# ---------------------------------------------------------------- (4)
|
|
sezione("(4) LA CODA CHE NON E' NEL DATASET — EVT/POT, con l'incertezza dichiarata")
|
|
print(f"\n 7,4 anni con due tori e un orso non contengono necessariamente il peggior giorno")
|
|
print(f" possibile. Qui si STIMA la coda invece di sceglierla: GPD sopra una soglia")
|
|
print(f" dichiarata prima ({EVT_Q:.0%}° pctl delle perdite), su chiusure E su minimi.")
|
|
|
|
fits, boots = {}, {}
|
|
for nome, arr in (("CHIUSURA", K["R_live"].values), ("MINIMO (acc.)", m_acc)):
|
|
loss = -arr
|
|
print(f"\n --- {nome} ---")
|
|
print(f" {'soglia q':>10}{'u':>9}{'n ecced.':>10}{'xi':>9}{'beta':>9}"
|
|
+ "".join(f"{f'1/{p} g':>12}" for p in RP) + f"{' P(perdita>10%)':>18}")
|
|
for q in EVT_Q_SENS:
|
|
f = gpd_fit(loss, q)
|
|
if not f:
|
|
continue
|
|
pe = gpd_exceed(f, STRESS_A_MANO)
|
|
rp10 = (1.0 / pe / ANN) if (np.isfinite(pe) and pe > 0) else np.inf
|
|
marca = " <-- soglia dichiarata" if abs(q - EVT_Q) < 1e-9 else ""
|
|
print(f" {q:>10.3f}{f['u']:>9.2%}{f['nu']:>10}{f['xi']:>9.3f}{f['beta']:>9.4f}"
|
|
+ "".join(f"{-gpd_quantile(f,1.0/p):>12.2%}" for p in RP)
|
|
+ f"{f'1 ogni {rp10:.0f} anni' if np.isfinite(rp10) else 'mai':>18}{marca}")
|
|
if abs(q - EVT_Q) < 1e-9:
|
|
fits[nome] = f
|
|
f = fits[nome]
|
|
bo = evt_bootstrap(loss, EVT_Q, EVT_BOOT, EVT_BLOCK, rng)
|
|
boots[nome] = bo
|
|
print(f" IC bootstrap A BLOCCHI ({EVT_BOOT} ricampionamenti, blocchi {EVT_BLOCK}g):")
|
|
print(f" xi {f['xi']:>8.3f} IC95 [{np.percentile(bo['xi'],2.5):+.3f},"
|
|
f" {np.percentile(bo['xi'],97.5):+.3f}]")
|
|
for p in RP:
|
|
v = bo[f"rl{p}"]
|
|
print(f" 1/{p:<6} {-gpd_quantile(f,1.0/p):>8.2%} IC95 "
|
|
f"[{-np.percentile(v,97.5):>7.2%}, {-np.percentile(v,2.5):>7.2%}]"
|
|
f" (p95 pessimista {-np.percentile(v,95):.2%})")
|
|
|
|
print(f"\n (b) CROSS-CHECK INDIPENDENTE — stimatore di HILL dell'indice di coda.")
|
|
print(f" Non e' un ri-fit della stessa cosa: Hill stima xi dai soli ORDINE-STATISTICI")
|
|
print(f" superiori (rapporti di log), senza MLE e senza soglia di quantile. Se i due non")
|
|
print(f" concordano, il numero non e' pronto per una decisione.")
|
|
print(f" ⚠️ La strada ovvia — fittare una t di Student su TUTTA la serie — qui NON")
|
|
print(f" funziona e va detto perche': il {(np.abs(book)<1e-4).mean():.0%} dei giorni e' esattamente zero")
|
|
print(f" (libro flat), la MLE manda i gradi di liberta' a ~0 per adattarsi all'atomo in")
|
|
print(f" zero e sputa quantili assurdi (10^12). **Una distribuzione con un atomo non si")
|
|
print(f" fitta con una densita' continua**: il POT non ha il problema perche' guarda solo")
|
|
print(f" la coda. Riportata qui sotto la t sui soli giorni ATTIVI, come terza strada.")
|
|
print(f"\n {'serie':>18}{'k ordini':>10}{'xi Hill':>10}{'xi GPD':>9}"
|
|
+ "".join(f"{f'1/{p} g (Hill)':>16}" for p in RP))
|
|
for nome, arr in (("CHIUSURA", K["R_live"].values), ("MINIMO (acc.)", m_acc)):
|
|
loss = np.sort(-arr)[::-1]
|
|
loss = loss[loss > 0]
|
|
kk = max(int(0.05 * len(loss)), 20)
|
|
xi_h = float(np.mean(np.log(loss[:kk])) - np.log(loss[kk]))
|
|
n_ = len(arr)
|
|
# Weissman: quantile estremo dallo stimatore di Hill
|
|
wq = lambda p: loss[kk] * ((kk / (n_ * p)) ** xi_h) # noqa: E731
|
|
f = fits[nome]
|
|
print(f" {nome:>18}{kk:>10}{xi_h:>10.3f}{f['xi']:>9.3f}"
|
|
+ "".join(f"{-wq(1.0/p):>16.2%}" for p in RP))
|
|
print(f"\n {'serie':>18}{'gdl (t, attivi)':>18}{'sigma':>9}"
|
|
+ "".join(f"{f'1/{p} g (t)':>14}" for p in RP))
|
|
for nome, arr in (("CHIUSURA", K["R_live"].values), ("MINIMO (acc.)", m_acc)):
|
|
att = arr[np.abs(arr) > 1e-4]
|
|
df_, loc_, sc_ = stats.t.fit(att)
|
|
print(f" {nome:>18}{df_:>18.1f}{sc_:>9.4f}"
|
|
+ "".join(f"{stats.t.ppf(1.0/p*len(att)/len(arr), df_, loc_, sc_):>14.2%}" for p in RP))
|
|
|
|
fc, fm = fits["CHIUSURA"], fits["MINIMO (acc.)"]
|
|
evt_boot_close = boots["CHIUSURA"]
|
|
pe = gpd_exceed(fc, STRESS_A_MANO)
|
|
# la stessa domanda con lo stimatore di HILL (indipendente dal MLE della GPD)
|
|
_l = np.sort(-K["R_live"].values)[::-1]
|
|
_l = _l[_l > 0]
|
|
_k = max(int(0.05 * len(_l)), 20)
|
|
_xi = float(np.mean(np.log(_l[:_k])) - np.log(_l[_k]))
|
|
pe_hill = (_k / len(K)) * (STRESS_A_MANO / _l[_k]) ** (-1.0 / _xi)
|
|
print(f"\n (c) IL CONFRONTO CHE DECIDE: il -{STRESS_A_MANO:.0%} assunto a mano e' DENTRO o FUORI?")
|
|
print(f" · chiusura, quantile 1/1.000 giorni : {-gpd_quantile(fc,1e-3):>8.2%}")
|
|
print(f" · chiusura, quantile 1/10.000 giorni : {-gpd_quantile(fc,1e-4):>8.2%}")
|
|
print(f" · minimo, quantile 1/10.000 giorni : {-gpd_quantile(fm,1e-4):>8.2%}")
|
|
if pe > 0:
|
|
print(f" · il -{STRESS_A_MANO:.0%} a mano, in chiusura : P = {pe:.2e}/giorno"
|
|
f" = 1 ogni {1/pe/ANN:.0f} anni")
|
|
else:
|
|
print(f" · il -{STRESS_A_MANO:.0%} a mano, in chiusura : probabilita' NULLA nel modello")
|
|
print(f" · lo stesso, con lo stimatore di HILL : P = {pe_hill:.2e}/giorno"
|
|
f" = 1 ogni {1/pe_hill/ANN:.0f} anni")
|
|
print(f"\n -> **il -{STRESS_A_MANO:.0%} NON E' una taglia impossibile, e' una FREQUENZA impossibile.**")
|
|
print(f" growth_policy lo assume UNA VOLTA L'ANNO (1 ogni 5 anni come variante mite).")
|
|
print(f" I due stimatori indipendenti lo mettono a **1 ogni {min(1/pe/ANN,1/pe_hill/ANN):.0f}"
|
|
f" - {max(1/pe/ANN,1/pe_hill/ANN):.0f} anni**:")
|
|
print(f" **{np.log10(1/pe_hill/ANN):.0f}-{np.log10(1/pe/ANN):.0f} ordini di grandezza di scarto sulla FREQUENZA**.")
|
|
print(f" ⚠️ E i due stimatori NON concordano fra loro: xi GPD {fc['xi']:.3f} contro Hill")
|
|
print(f" {_xi:.3f}, cioe' un quantile a 1/10.000 di {-gpd_quantile(fc,1e-4):.2%} contro"
|
|
f" {-(_l[_k]*((_k/(len(K)*1e-4))**_xi)):.2%}.")
|
|
print(f" **Si cita la BANDA fra stimatori, non il numero di uno solo** — su una coda")
|
|
print(f" stimata da {len(K)} osservazioni la scelta dello stimatore vale quanto i dati.")
|
|
print(f"\n ⚠️ E QUI VA DETTO IL LIMITE DELL'EVT, che e' il vero motivo per cui la sezione (5)")
|
|
print(f" esiste: **una GPD stimata sui giorni vissuti estrapola il MECCANISMO vissuto.**")
|
|
print(f" Se il libro puo' assumere una configurazione di posizioni che nel campione non")
|
|
print(f" ha mai assunto, quella configurazione non e' nella coda stimata. Nessun")
|
|
print(f" intervallo di confidenza copre un modo di perdere che non e' nei dati: e'")
|
|
print(f" esattamente cio' che il gate `implausible_sharpe` chiama 'rischio FUORI dal")
|
|
print(f" dataset'. La risposta non e' un fit migliore, e' un conto ARITMETICO.")
|
|
|
|
# ---------------------------------------------------------------- (5)
|
|
sezione("(5) IL PEGGIOR GIORNO POSSIBILE PER COSTRUZIONE — aritmetica, non statistica")
|
|
cfg = json.loads((ROOT / "config" / "live.json").read_text())
|
|
frac = float(cfg["max_notional_per_asset_frac"])
|
|
TPQ = TPQ_pre
|
|
EX = estremi_orari()
|
|
sl_l, sl_s = SKH01_V2_DD.sl_pct, SKH01_V2_DD.sl_pct_short
|
|
tv, levcap = CANONICAL["target_vol"], CANONICAL["leverage"]
|
|
|
|
print(f"\n (a) LA FORMULA DEL LIBRO LIVE (src/live/book.book_net_target, letta dal codice):")
|
|
print(f" net_target[asset] = clamp( 0.5 * E * (0.75*tp_frac + 0.25*skh_sign), ±cap ),")
|
|
print(f" cap = E * {frac} -> per-asset |net| <= {frac:.2f}·E, LORDO massimo {2*frac:.2f}·E su 2 asset.")
|
|
print(f" In frazione di equity: gamba TP01 = {0.5*W_TP:.3f}·tp_frac, gamba SKH01 = {0.5*W_SKH:.3f}·skh_sign.")
|
|
|
|
print(f"\n (b) TP01 — il vol-target rende il bound INVARIANTE alla volatilita'.")
|
|
print(f" tp_frac = segnale * min(target_vol/vol, {levcap:g}) con segnale in {{0, 1/3, 2/3, 1}},")
|
|
print(f" quindi contributo di gamba = 0.375 * segnale * target_vol * z / sqrt(365.25)")
|
|
print(f" = {0.5*W_TP*tv/np.sqrt(365.25)*100:.4f}% * segnale * z. **Un crash entra solo attraverso il suo z-score.**")
|
|
print(f" ⚠️ La formula vale con tp_frac = target_vol/vol, cioe' solo dove il CAP non morde:")
|
|
print(f" 0.375*tp_frac + 0.125 <= {frac} richiede tp_frac <= 1, cioe' vol >= target_vol.")
|
|
print(f" Sotto quella soglia il contributo e' 0.375*|r| ed e' PIU' PICCOLO -> il bound")
|
|
print(f" si prende sullo z peggiore CON vol >= {tv:.0%} (auto-consistenza, non cherry-pick).")
|
|
print(f" {'asset':>7}{'z min (vol>=tv)':>17}{'quando':>13}{'ret':>9}{'vol prec.':>11}"
|
|
f"{'tp_frac':>9}{'contributo max':>16}{'z min GREZZO':>14}")
|
|
zmin, zmin_raw = {}, {}
|
|
for a in ASSETS:
|
|
d = TPQ[a].dropna(subset=["z"])
|
|
zmin_raw[a] = float(d["z"].min())
|
|
dv = d[d["vol_prev"] >= tv]
|
|
i = dv["z"].idxmin()
|
|
zmin[a] = float(dv["z"].min())
|
|
contr = 0.5 * W_TP * tv * abs(zmin[a]) / np.sqrt(365.25)
|
|
print(f" {a:>7}{zmin[a]:>17.1f}{str(i.date()):>13}{dv.loc[i,'ret']:>9.1%}"
|
|
f"{dv.loc[i,'vol_prev']:>11.1%}{min(tv/dv.loc[i,'vol_prev'],1.0):>9.2f}"
|
|
f"{-contr:>16.2%}{zmin_raw[a]:>14.1f}")
|
|
print(f" tp_frac MASSIMO mai tenuto: " + " · ".join(
|
|
f"{a} {TPQ[a]['held'].max():.3f}" for a in ASSETS)
|
|
+ f" (il cap di leva {levcap:g}x NON e' mai stato binding: la vol a 30g non e' mai scesa"
|
|
f" sotto {min(float(TPQ[a]['vol_prev'].min()) for a in ASSETS):.1%})")
|
|
|
|
print(f"\n (c) SKH01 — NON e' vol-targeted (misurato 26/07): la sua taglia e' fissa e la sua")
|
|
print(f" perdita e' limitata dallo STOP, ma gli exit sono SOFTWARE (fill al close orario")
|
|
print(f" successivo al tocco) -> il bound e' stop + escursione DENTRO quell'ora.")
|
|
print(f" {'asset':>7}{'1h peggiore giu':>18}{'1h peggiore su':>17}"
|
|
f"{'bound LONG (sl 4%)':>21}{'bound SHORT (sl 2%)':>21}{'peggio realizzato':>19}")
|
|
bound_l, bound_s = {}, {}
|
|
for a in ASSETS:
|
|
bound_l[a] = sl_l + abs(EX[a]["giu"])
|
|
bound_s[a] = sl_s + abs(EX[a]["su"])
|
|
real = SKL.skh_daily_asset(a, 0, "hourly").min()
|
|
print(f" {a:>7}{EX[a]['giu']:>18.2%}{EX[a]['su']:>17.2%}"
|
|
f"{-bound_l[a]:>21.2%}{-bound_s[a]:>21.2%}{real:>19.2%}")
|
|
print(f" -> il gap-through misurato dal progetto (sl 2% modellato -> -11/-23% realizzato)")
|
|
print(f" e' COERENTE con questo bound, che lo contiene senza esserne tarato.")
|
|
|
|
print(f"\n (d) I DUE SCENARI COERENTI. TP01 e' LONG-ONLY: perde solo in ribasso. SKH01 short")
|
|
print(f" perde solo in RIALZO. Non possono perdere al massimo lo stesso giorno se SKH e'")
|
|
print(f" short -> gli scenari sono due, e vanno tenuti separati.")
|
|
lim_tp = {a: 0.5 * W_TP * tv * abs(zmin[a]) / np.sqrt(365.25) for a in ASSETS}
|
|
crash_max = sum(lim_tp[a] + 0.5 * W_SKH * bound_l[a] for a in ASSETS)
|
|
squeeze_max = sum(0.5 * W_SKH * bound_s[a] for a in ASSETS)
|
|
print(f"\n SCENARIO A — CRASH (TP01 a piena convinzione su entrambi + SKH01 LONG stoppato)")
|
|
for a in ASSETS:
|
|
print(f" {a}: TP01 {-lim_tp[a]:>7.2%} SKH01 {-0.5*W_SKH*bound_l[a]:>7.2%}"
|
|
f" -> {-(lim_tp[a]+0.5*W_SKH*bound_l[a]):>7.2%}")
|
|
print(f" TOTALE LIBRO {-crash_max:>7.2%} <-- IL BOUND STRUTTURALE a k=1,00x")
|
|
print(f"\n SCENARIO B — RIMBALZO (TP01 flat per costruzione + SKH01 SHORT stoppato su due)")
|
|
for a in ASSETS:
|
|
print(f" {a}: SKH01 {-0.5*W_SKH*bound_s[a]:>7.2%}")
|
|
print(f" TOTALE LIBRO {-squeeze_max:>7.2%} realizzato il {i_worst.date()}:"
|
|
f" {J['book'].min():+.2%} ({J['book'].min()/-squeeze_max:.0%} del bound)")
|
|
print(f" -> lo scenario B e' gia' stato VISSUTO quasi al suo massimo: il bound e' credibile.")
|
|
|
|
print(f"\n (e) CONTROPROVA INDIPENDENTE — controfattuale sui movimenti VERI. Per ogni giorno")
|
|
print(f" storico si sostituisce a TP01 la piena convinzione e a SKH01 la posizione LONG")
|
|
print(f" su entrambi gli asset, e si applica il movimento realmente accaduto.")
|
|
CF = pd.concat({a: TPQ[a][["lev_max", "ret", "ret_low"]] for a in ASSETS}, axis=1).dropna()
|
|
tot = np.zeros(len(CF))
|
|
tot_min = np.zeros(len(CF)) # stessa cosa valutata sul MINIMO intra-giorno
|
|
for a in ASSETS:
|
|
rr = CF[(a, "ret")].values
|
|
rl = CF[(a, "ret_low")].values
|
|
hm = np.minimum(CF[(a, "lev_max")].values, 1.0) # cap: 0.375h+0.125<=0.5 -> h<=1
|
|
tot += 0.5 * W_TP * hm * rr + 0.5 * W_SKH * np.maximum(rr, -bound_l[a])
|
|
tot_min += 0.5 * W_TP * hm * rl + 0.5 * W_SKH * np.maximum(rl, -bound_l[a])
|
|
o = np.argsort(tot)[:5]
|
|
print(f" {'data':>12}{'perdita ctrf (chiusura)':>25}{'ctrf (minimo)':>16}{'BTC':>10}{'ETH':>10}")
|
|
for i in o:
|
|
print(f" {str(CF.index[i].date()):>12}{tot[i]:>25.2%}{tot_min[i]:>16.2%}"
|
|
f"{CF[('BTC','ret')].values[i]:>10.1%}{CF[('ETH','ret')].values[i]:>10.1%}")
|
|
print(f"\n n = {len(CF)} giorni · giorni sotto -10% nel controfattuale:"
|
|
f" {int((tot<-0.10).sum())} ({(tot<-0.10).mean()*365:.2f}/anno) · sotto -5%:"
|
|
f" {int((tot<-0.05).sum())} ({(tot<-0.05).mean()*365:.2f}/anno)")
|
|
L_STRUCT = max(crash_max, -float(tot.min()))
|
|
L_STRUCT_MIN = -float(tot_min.min())
|
|
print(f"\n -> DUE STRADE, STESSA RISPOSTA: il bound analitico da' {-crash_max:.2%},")
|
|
print(f" il controfattuale sui movimenti veri {tot.min():.2%}. **Si usa"
|
|
f" L_strutturale = {-L_STRUCT:.2%}** in CHIUSURA.")
|
|
print(f" E sul MINIMO intra-giorno (stesso controfattuale valutato al low della giornata,")
|
|
print(f" che e' la grandezza che guarda il margine): **{tot_min.min():.2%}**, cioe'"
|
|
f" {L_STRUCT_MIN/L_STRUCT:.2f}x la chiusura.")
|
|
print(f" ⚠️ Questo numero NON eredita il limite della sez.(3d): non passa dalla recon")
|
|
print(f" canonica, e' costruito sui low veri del feed certificato.")
|
|
print(f"\n 📌 LA RISPOSTA ALLA DOMANDA DEL BRIEF: il -10% NON e' fuori dal possibile —")
|
|
print(f" sta al {STRESS_A_MANO/L_STRUCT:.0%} del massimo strutturale. **Cio' che e' sbagliato non e'")
|
|
print(f" la taglia, e' la FREQUENZA**: il controfattuale — in cui la strategia")
|
|
print(f" difensiva e' massimamente in torto su entrambi gli asset — sfonda il -10%")
|
|
print(f" {int((tot<-0.10).sum())} giorno/i su {len(CF)}; il libro VERO non l'ha mai")
|
|
print(f" nemmeno avvicinato ({J['book'].min():+.2%}). **La riserva dello scettico e' mal")
|
|
print(f" tarata sulla frequenza, non falsificata sulla taglia.**")
|
|
zx = 1.5
|
|
print(f"\n ⚠️ COSA NON E' CHIUSO: z_max e' esso stesso un massimo IN CAMPIONE. Con uno z")
|
|
print(f" piu' cattivo del {zx:g}x osservato il bound dello scenario A diventa"
|
|
f" {-(sum(zx*lim_tp[a] + 0.5*W_SKH*bound_l[a] for a in ASSETS)):.2%}.")
|
|
print(f" Il bound e' condizionato a 'nessun giorno peggiore in z di quelli visti', e")
|
|
print(f" va citato cosi'. Non e' un massimo assoluto: e' un massimo CONDIZIONATO.")
|
|
|
|
# ---- il cap di config
|
|
print(f"\n (f) 🚨 IL CAP DI CONFIG NON E' UN KNOB DI SCALA — e questo cambia la DOMANDA.")
|
|
print(f" La regola del progetto dice che un cambio di SCALA passa dal cap di config e non")
|
|
print(f" da `target_vol`. Ma il cap e' un CLAMP, non un moltiplicatore: agisce solo nei")
|
|
print(f" giorni in cui il libro vuole PIU' del cap. Quanti sono?")
|
|
print(f" {'asset':>7}{'giorni':>9}{'net max voluto':>17}{'giorni sopra cap 0.50':>24}{'quota':>10}")
|
|
for a in ASSETS:
|
|
h = TPQ[a]["held"].values
|
|
net = 0.5 * W_TP * h + 0.5 * W_SKH # SKH long = il caso che spinge di piu'
|
|
n_over = int((net > frac).sum())
|
|
print(f" {a:>7}{len(h):>9}{net.max():>17.4f}{n_over:>24}{n_over/len(h):>10.2%}")
|
|
print(f"\n -> **il cap morde su 3 giorni su ~2.900, e per meno dell'1% del target.** Alzare")
|
|
print(f" `max_notional_per_asset_frac` da 0.50 a 0.625 NON produce k=1,25: produce")
|
|
print(f" k=1,00 in 2.928 giorni su 2.931 e un pelo di piu' in tre. **Il gradino di leva")
|
|
print(f" NON e' oggi implementabile con la strada che la regola dichiara pulita**, e in")
|
|
print(f" `config/live.json` NON esiste una chiave di scala: servirebbe toccare")
|
|
print(f" `WEIGHT`/`W_TP01`/`W_SKH` in `src/live/book.py` (codice, non config) oppure")
|
|
print(f" aggiungere una chiave nuova. **Due mesi di discussione su un knob inesistente.**")
|
|
print(f" ⚠️ E il moltiplicatore x1,25 sui pesi e' un puro cambio di scala SOLO se applicato")
|
|
print(f" a entrambi: applicato a uno solo e' un tilt, e li' `weights_tilt_null` c'e' gia'.")
|
|
|
|
# ---------------------------------------------------------------- (6)
|
|
sezione("(6) k* ONESTO — la curva di Kelly con la coda STIMATA al posto di quella a mano")
|
|
f_close = fits["CHIUSURA"]
|
|
drift_pt = mu * ANN
|
|
scenari_drift = (("punto (de-luck x0.89)", drift_pt),
|
|
("-1 errore standard", drift_pt - se_drift),
|
|
("-2 errori standard", drift_pt - 2 * se_drift))
|
|
code = (("empirica (nessun modello)", None),
|
|
(f"EVT troncata a {-L_STRUCT:.1%} (strutturale)", L_STRUCT),
|
|
(f"EVT troncata a {-STRESS_A_MANO:.0%} (a mano)", STRESS_A_MANO),
|
|
(f"EVT troncata a {-1.5*L_STRUCT:.1%} (z x1.5)", 1.5 * L_STRUCT))
|
|
print(f"\n Errore standard del drift = vol/sqrt(T) = {se_drift:.2%}/anno su"
|
|
f" {len(r_del)/ANN:.1f} anni.")
|
|
print(f" La coda EVT ha supporto ILLIMITATO se xi>0 -> E[log(1+kR)] sarebbe -inf per ogni k:")
|
|
print(f" il supporto lo da' l'ARITMETICA della sezione (5). E' qui che i due pezzi si")
|
|
print(f" incastrano — l'EVT da' la frequenza, la struttura da' il massimo.")
|
|
print(f"\n {'coda':>34}{'drift':>24}{'k*':>9}{'g(1.00x)':>11}{'g(1.25x)':>11}{'g(1.50x)':>11}")
|
|
ks_tab = {}
|
|
NDRAW = 400_000
|
|
# numeri casuali APPAIATI fra tutte le celle: la tabella e' un confronto, non 12 stime
|
|
# indipendenti. Senza questo, differenze di ±0,5x fra celle sono rumore MC e sembrano segnale.
|
|
_rg = np.random.default_rng(SEED + 1)
|
|
RND = {"ib": _rg.integers(0, 10 ** 9, NDRAW), "um": _rg.random(NDRAW),
|
|
"ut": _rg.random(NDRAW)}
|
|
for nome_c, lmax in code:
|
|
for nome_d, dr in scenari_drift:
|
|
if lmax is None:
|
|
rr = r_del - r_del.mean() + dr / ANN
|
|
else:
|
|
rr = campione_semiparam(r_del, f_close, lmax, NDRAW, dr, RND)
|
|
kk, _ = k_star(rr)
|
|
ks_tab[(nome_c, nome_d)] = kk
|
|
print(f" {nome_c:>34}{nome_d:>24}{kk:>8.2f}x{g_exact(rr,1.00):>11.2%}"
|
|
f"{g_exact(rr,1.25):>11.2%}{g_exact(rr,1.50):>11.2%}")
|
|
|
|
print(f"\n -> **LA FORMA DELLA CODA QUASI NON CONTA, IL DRIFT CONTA TUTTO.** Fra la coda")
|
|
print(f" empirica e una GPD troncata a {-1.5*L_STRUCT:.0%} k* si muove di ~1-2 unita'; fra il")
|
|
print(f" drift di punto e quello a -2 SE si muove di {ks_tab[('empirica (nessun modello)','punto (de-luck x0.89)')]-ks_tab[('empirica (nessun modello)','-2 errori standard')]:.1f}. **Modellare meglio la coda non e'")
|
|
print(f" la leva: la leva e' quanti anni di storia ha il drift.** (Ed e' coerente col")
|
|
print(f" fatto gia' pubblicato che k* e' LINEARE nell'errore del drift.)")
|
|
print(f" ⚠️ Le celle sono APPAIATE (stessi numeri casuali): le differenze fra righe sono")
|
|
print(f" confrontabili, i livelli assoluti hanno l'incertezza del fit di coda.")
|
|
print(f"\n (b) LO STRESS A MANO DELLO SCETTICATO, riprodotto e poi ri-calibrato.")
|
|
print(f" growth_policy: 'un giorno -10% ALL'ANNO porta k* a 2x'.")
|
|
print(f" ⚠️ CONVENZIONE, ed e' quella che decide il numero: qui i giorni di coda si")
|
|
print(f" AGGIUNGONO e il drift NON si ri-centra. E' la lettura conservativa, la stessa")
|
|
print(f" implicita in growth_policy (che SOSTITUISCE giorni veri con il crash): se un")
|
|
print(f" -10% annuo esiste ed e' fuori campione, allora il drift osservato era FORTUNATO,")
|
|
print(f" e togliergli il tail event senza toccare il drift conterebbe due volte la")
|
|
print(f" fortuna. La tabella (a) sopra ri-centra di proposito, perche' li' il drift e' una")
|
|
print(f" COLONNA e si vuole isolare la forma della coda; qui no.")
|
|
p_evt = gpd_exceed(f_close, STRESS_A_MANO)
|
|
freq_cf = max((tot < -0.10).mean(), 1.0 / len(CF))
|
|
print(f"\n {'evento':>36}{'freq':>16}{'drift':>9}{'k*':>8}{'1/2 Kelly':>11}"
|
|
f"{'g(1.00x)':>10}{'g(1.25x)':>10}{'g(1.50x)':>10}")
|
|
stress_k = {}
|
|
for lbl, p_giorno in ((f"-{STRESS_A_MANO:.0%} 1 volta l'ANNO (a mano)", 1.0 / ANN),
|
|
(f"-{STRESS_A_MANO:.0%} 1 ogni 5 anni (a mano, mite)", 1.0 / (5 * ANN)),
|
|
(f"-{STRESS_A_MANO:.0%} alla freq. del CONTROFATTUALE", freq_cf),
|
|
(f"-{STRESS_A_MANO:.0%} alla freq. stimata EVT", p_evt),
|
|
("nessuno stress (riferimento)", 0.0)):
|
|
rr = r_del.copy()
|
|
n_add = int(round(p_giorno * len(rr) / max(1e-12, 1 - p_giorno))) if p_giorno > 0 else 0
|
|
rr2 = np.concatenate([rr, np.full(n_add, -STRESS_A_MANO)]) if n_add > 0 else rr
|
|
kk, _ = k_star(rr2)
|
|
stress_k[lbl] = kk
|
|
fr = f"1 ogni {1/p_giorno:.0f}g" if p_giorno > 0 else "-"
|
|
print(f" {lbl:>36}{fr:>16}{rr2.mean()*ANN:>9.2%}{kk:>7.2f}x{0.5*kk:>10.2f}x"
|
|
f"{g_exact(rr2,1.00):>10.2%}{g_exact(rr2,1.25):>10.2%}{g_exact(rr2,1.50):>10.2%}")
|
|
k_mano = stress_k[f"-{STRESS_A_MANO:.0%} 1 volta l'ANNO (a mano)"]
|
|
KSG = (1.00, 1.50, 2.00, 3.50, 5.00, 10.00, 18.00) # la griglia COARSE di growth_policy
|
|
rr = np.concatenate([r_del, np.full(int(round(len(r_del)/(ANN-1))), -STRESS_A_MANO)])
|
|
gg = [g_exact(rr, k) for k in KSG]
|
|
print(f"\n REPLICA PARZIALE, dichiarata: sulla griglia COARSE di growth_policy questo")
|
|
print(f" motore mette l'argmax a {KSG[int(np.nanargmax(gg))]:g}x (loro: 2x); a griglia fine {k_mano:.2f}x.")
|
|
print(f" **La direzione e l'ordine di grandezza si replicano** (k* crolla da"
|
|
f" {ks_emp:.1f}x a ~3x),")
|
|
print(f" la cella esatta no — e' la risoluzione MC che loro stessi hanno dichiarato.")
|
|
print(f"\n -> **LA TAGLIA DEL -10% COSTA POCO, LA FREQUENZA COSTA TUTTO.** Lo stesso evento,")
|
|
print(f" spostato dalla frequenza ASSUNTA a quella STIMATA, muove k* da {k_mano:.1f}x a"
|
|
f" {stress_k['nessuno stress (riferimento)']:.1f}x.")
|
|
print(f" Il parametro scelto a mano che blocca il gradino non e' '-10%': e' 'OGNI ANNO'.")
|
|
print(f"\n 📌 E IL PUNTO CHE CAMBIA LA DECISIONE: **anche accettando lo stress a mano**")
|
|
print(f" **alla lettera**, k* = {k_mano:.2f}x -> mezzo-Kelly = {0.5*k_mano:.2f}x. **1,25x ci sta")
|
|
print(f" sotto**, 1,50x e' esattamente al bordo. Cioe': la riserva dello scettico, presa")
|
|
print(f" per buona senza ri-calibrarla, non basta a bocciare il gradino a 1,25x —")
|
|
print(f" bocciava 1,50x, che e' il gradino di cui si stava discutendo.")
|
|
print(f"\n ⚠️ SENSITIVITA' FUORI DAL CRITERIO (dichiarata dopo, non conta per il verdetto):")
|
|
rr3 = np.concatenate([r_del - 2 * se_drift / ANN,
|
|
np.full(int(round(len(r_del) / (ANN - 1))), -STRESS_A_MANO)])
|
|
k3, _ = k_star(rr3)
|
|
print(f" se si credono ENTRAMBE le riserve insieme (drift -2 SE **E** -10% ogni anno)")
|
|
print(f" il drift atteso diventa {rr3.mean()*ANN:+.2%}/anno e k* = {k3:.2f}x.")
|
|
print(f" **A drift {'negativo' if rr3.mean()<0 else 'quasi nullo'} il problema non e' il gradino: e' che il libro non")
|
|
print(f" andrebbe tradato affatto, nemmeno a 1,00x.** Chi usa questa combinazione per")
|
|
print(f" bocciare 1,25x sta implicitamente chiedendo di spegnere anche il conto di oggi:")
|
|
print(f" e' una posizione legittima, ma va dichiarata per quello che e'.")
|
|
|
|
print(f"\n (c) LIQUIDAZIONE — la domanda va posta sul MINIMO, non sulla chiusura.")
|
|
print(f" Con nozionale lordo G·E e margine di mantenimento {MM_RATE:.1%}, il conto e'")
|
|
print(f" liquidato quando il movimento avverso x soddisfa x ≈ (1-mm·G)/(G·(1-mm)).")
|
|
g_avg = sum(0.5 * W_TP * TPQ[a]["held"].mean() for a in ASSETS) + W_SKH * 0.124
|
|
g_max = sum(0.5 * W_TP * TPQ[a]["held"].max() + 0.5 * W_SKH for a in ASSETS)
|
|
print(f" lordo MEDIO del libro {g_avg:.3f}·E · lordo MASSIMO mai voluto {g_max:.3f}·E")
|
|
m_worst_unit = max(abs(gpd_quantile(fm, 1e-4)), L_STRUCT_MIN)
|
|
print(f" il 'peggior minimo' usato qui e' il PIU' PESSIMISTA fra EVT 1/10.000"
|
|
f" ({-abs(gpd_quantile(fm,1e-4)):.2%})")
|
|
print(f" e il bound strutturale sul minimo ({-L_STRUCT_MIN:.2%}) = {-m_worst_unit:.2%} a k=1.")
|
|
print(f"\n {'k':>7}{'G max':>9}{'x liquidaz.':>14}{'min vissuto':>14}"
|
|
f"{'min EVT 1/10^4':>16}{'min STRUTT.':>14}{'margine':>10}")
|
|
for k in (1.00, 1.25, 1.50, 2.00, 3.00):
|
|
G = min(g_max * k, 2 * frac * k)
|
|
x_liq = (1 - MM_RATE * G) / (G * (1 - MM_RATE))
|
|
print(f" {k:>6.2f}x{G:>9.3f}{x_liq:>14.1%}{abs(m_acc.min())*k:>14.2%}"
|
|
f"{abs(gpd_quantile(fm,1e-4))*k:>16.2%}{L_STRUCT_MIN*k:>14.2%}"
|
|
f"{x_liq/(m_worst_unit*k):>9.1f}x")
|
|
kliq = WIPE / abs(m_acc.min())
|
|
print(f"\n replica dello scetticato ('servirebbe 12,3x sul peggior giorno vissuto',")
|
|
print(f" convenzione WIPE {WIPE:.0%}): {WIPE:.2f}/{abs(m_acc.min()):.4f} = {kliq:.1f}x <-- riprodotto")
|
|
|
|
# ---------------------------------------------------------------- (7)
|
|
sezione("(7) IL VERDETTO — il criterio di (0), valutato")
|
|
esiti = {}
|
|
|
|
# C1
|
|
c1_val = 1.25 * L_STRUCT
|
|
esiti["C1"] = (c1_val <= 0.50,
|
|
f"1,25 x {L_STRUCT:.2%} = {c1_val:.2%} di equity persa nel peggior giorno "
|
|
f"POSSIBILE, contro la soglia del 50%. Margine {0.50/c1_val:.1f}x.")
|
|
# C2
|
|
k_pess = ks_tab[(f"EVT troncata a {-L_STRUCT:.1%} (strutturale)", "-2 errori standard")]
|
|
esiti["C2"] = (1.25 <= 0.5 * k_pess,
|
|
f"k* pessimistico (drift -2 SE, coda troncata al bound strutturale) = "
|
|
f"{k_pess:.2f}x -> mezzo-Kelly = {0.5*k_pess:.2f}x. 1,25 e' il "
|
|
f"{1.25/k_pess:.0%} di quel k*.")
|
|
# C3
|
|
G125 = min(g_max * 1.25, 2 * frac * 1.25)
|
|
x_liq125 = (1 - MM_RATE * G125) / (G125 * (1 - MM_RATE))
|
|
m_evt125 = m_worst_unit * 1.25
|
|
esiti["C3"] = (x_liq125 >= 3 * m_evt125,
|
|
f"peggior minimo intra-giorno (il piu' pessimista fra EVT 1/10.000 "
|
|
f"{-abs(gpd_quantile(fm,1e-4)):.2%} e bound strutturale {-L_STRUCT_MIN:.2%}) "
|
|
f"x1,25 = {-m_evt125:.2%} contro una soglia di liquidazione a {x_liq125:.1%}: "
|
|
f"margine {x_liq125/m_evt125:.0f}x (richiesto 3x).")
|
|
# C4
|
|
r_2se = r_del - r_del.mean() + (drift_pt - 2 * se_drift) / ANN
|
|
dg = g_exact(r_2se, 1.25) - g_exact(r_2se, 1.00)
|
|
esiti["C4"] = (dg > 0,
|
|
f"col drift a -2 SE ({drift_pt-2*se_drift:.2%}/anno): g(1,25x)-g(1,00x) = "
|
|
f"{dg:+.2%}/anno.")
|
|
# C5
|
|
fund = {}
|
|
for a in ASSETS:
|
|
d = pd.read_parquet(ROOT / "data" / "raw" / f"hlfund_{a.lower()}_1h.parquet")
|
|
fund[a] = float(d["funding"].values.mean()) * 8760.0
|
|
esp_long = sum(0.5 * W_TP * TPQ[a]["held"].mean() for a in ASSETS)
|
|
costo_k1 = esp_long * np.mean(list(fund.values()))
|
|
guad = g_exact(r_del, 1.25) - g_exact(r_del, 1.00)
|
|
esiti["C5"] = (0.25 * costo_k1 < guad / 3.0,
|
|
f"funding medio (proxy Hyperliquid, 3 anni): "
|
|
f"BTC {fund['BTC']:.1%}/a, ETH {fund['ETH']:.1%}/a; esposizione LONG media di "
|
|
f"TP01 {esp_long:.3f}·E -> costo {costo_k1:.2%}/anno a k=1, "
|
|
f"costo extra al gradino +{0.25*costo_k1:.2%}/anno contro un guadagno extra di "
|
|
f"{guad:+.2%}/anno (soglia dichiarata: costo extra < 1/3 del guadagno extra = "
|
|
f"{guad/3:.2%}). ⚠️ E il costo NON e' modellato NEMMENO a k=1: il libro paga gia' "
|
|
f"~{costo_k1:.2%}/anno che nessun backtest del progetto sottrae — e' un difetto "
|
|
f"che il gradino non crea ma amplifica del 25%.")
|
|
# C6
|
|
n_cap = sum(int(((0.5 * W_TP * TPQ[a]["held"].values + 0.5 * W_SKH) > frac).sum())
|
|
for a in ASSETS)
|
|
n_tot = sum(len(TPQ[a]) for a in ASSETS)
|
|
esiti["C6"] = (False,
|
|
f"il cap di config e' un CLAMP, non un moltiplicatore: morde su {n_cap} "
|
|
f"osservazioni-asset su {n_tot} ({n_cap/n_tot:.3%}). Alzarlo NON produce k=1,25. "
|
|
f"In `config/live.json` non esiste una chiave di scala.")
|
|
|
|
print()
|
|
for k in ("C1", "C2", "C3", "C4", "C5", "C6"):
|
|
ok, spieg = esiti[k]
|
|
print(f" {k} {'PASSA ' if ok else 'FALLISCE'} {spieg}")
|
|
print()
|
|
|
|
passa = all(v[0] for v in esiti.values())
|
|
print(f" ESITO COMPLESSIVO: {sum(v[0] for v in esiti.values())}/6 criteri passati"
|
|
f" -> {'AUTORIZZABILE' if passa else 'NON autorizzabile come proposto'}")
|
|
|
|
print(f"""
|
|
LETTURA IN CHIARO
|
|
|
|
1. IL CONTROLLO DEL CRITICO E' CONFERMATO NELLA DIREZIONE, CORRETTO NELLA PORTATA, E IL SUO
|
|
NUMERO NON SI RIPRODUCE. Sui 10 giorni peggiori del sottostante il libro fa
|
|
{worst_und.book.sum():+.2%} in totale contro {worst_und.und.sum():+.1%} del mercato e guadagna in
|
|
{int((worst_und.book>0).sum())} di essi; il 12 marzo 2020 (-43,6%) fa {J.loc[J.und.idxmin(),'book']:+.2%}. **Ma
|
|
sui 160 giorni di crash il libro perde in media {crash.book.mean():+.3%}**: 'il libro guadagna
|
|
nei crash' vale sugli ESTREMI, non in generale, e l'esposizione di TP01 scende in modo
|
|
monotono al crescere del crollo ({Jx[(Jx.und>-0.05)&(Jx.und<=-0.02)].espo.mean():.3f} nella fascia -5/-2%,
|
|
{Jx[Jx.und<=-0.20].espo.mean():.3f} sotto il -20%). **Il libro non e' immune ai crash: e' immune ai crash
|
|
GROSSI**, perche' li' il trend si e' gia' girato.
|
|
⚠️ E la sua LENTE si identifica dai valori pubblicati (+0,97 e +0,72 compaiono SOLO
|
|
nella riga canonica): ha usato `book_series('canonical')`, il BACKTEST di SKH01. Su
|
|
quella lente **il peggior giorno E' un giorno di ribasso** ({b_can.min():+.2%}, {b_can.idxmin().date()}),
|
|
quindi la frase 'il peggior giorno non e' un giorno di crash' e' vera solo sulla lente
|
|
LIVE — che e' quella giusta per decidere, ma non e' quella da cui viene il suo numero.
|
|
**Aveva ragione, con la lente sbagliata.** Si cita **-3,47% (canonica) / -3,94% (live)**;
|
|
lo scarto residuo di 0,09pp col suo -3,38% e' dell'ordine del difetto gia' noto
|
|
('`data/raw` gitignored, il cron riscrive i parquet' — stesso codice, dati diversi).
|
|
|
|
2. LA CODA DEL LIBRO ESISTE, MA STA DALL'ALTRA PARTE DEL MERCATO. Il peggior giorno in
|
|
chiusura e' {J['book'].min():+.2%} il {i_worst.date()}, ed e' un **RIMBALZO** (+20,9%/+27,6%): SKH01
|
|
short con stop al 2% che gappa a {skh_only.min():.2%} di sleeve, mentre TP01 e' flat. Secondo
|
|
peggiore dello sleeve: {skh_only.nsmallest(2).iloc[-1]:.2%} — **la coda di SKH01 e' UN giorno solo**.
|
|
⚠️ E la caratterizzazione si INVERTE con la lente: sulla canonica il peggior giorno e'
|
|
un ribasso ({b_can.min():+.2%}, {b_can.idxmin().date()}). Va citata con la lente.
|
|
|
|
3. LE DUE RISPOSTE ALLA DOMANDA 'QUAL E' IL PEGGIOR GIORNO' DIFFERISCONO DI ~2x:
|
|
**{K['R_live'].min():+.2%} in chiusura, {m_acc.min():+.2%} al minimo intra-giorno** (mediana del rapporto
|
|
sui giorni in perdita {np.median(m_acc[neg]/K['R_live'].values[neg]):.2f}x). Chi valuta margine, daily-loss o stop di
|
|
conto sulla chiusura sbaglia di quel fattore, nel verso ottimista. ⚠️ E il minimo del
|
|
giorno peggiore e' un LIMITE SUPERIORE: la recon accoppiata cappa il wick allo stop
|
|
proprio nel giorno del gap-through.
|
|
|
|
4. LA STIMA DELLA CODA, CON LA SUA INCERTEZZA: chiusura **{-gpd_quantile(f_close,1e-3):.2%} a 1/1.000 giorni**
|
|
e **{-gpd_quantile(f_close,1e-4):.2%} a 1/10.000** (GPD, soglia {EVT_Q:.0%}, IC95 bootstrap a blocchi
|
|
[{-np.percentile(evt_boot_close['rl10000'],97.5):.2%}, {-np.percentile(evt_boot_close['rl10000'],2.5):.2%}]); Hill dice {-(_l[_k]*((_k/(len(K)*1e-4))**_xi)):.2%} a 1/10.000.
|
|
**I due stimatori non concordano** (xi {f_close['xi']:.2f} vs {_xi:.2f}) e si cita la BANDA: un fit di coda
|
|
su {len(K)} osservazioni e' un ordine di grandezza, non un numero. Il -10% in chiusura esce
|
|
a **1 ogni {min(1/pe/ANN,1/pe_hill/ANN):.0f}-{max(1/pe/ANN,1/pe_hill/ANN):.0f} anni**, contro 'ogni anno' assunto.
|
|
|
|
5. IL CONTO ARITMETICO, che l'EVT non puo' fare perche' estrapola solo il meccanismo
|
|
vissuto: dato il cap di config, il vol-target di TP01 (che converte un crash nel suo
|
|
**z-score**, rendendo il bound invariante alla volatilita') e il gap-through di SKH01,
|
|
il peggior giorno POSSIBILE a k=1,00x e' **{-L_STRUCT:.2%}** in chiusura e **{-L_STRUCT_MIN:.2%}** al
|
|
minimo. Bound analitico e controfattuale sui movimenti veri danno lo stesso numero —
|
|
⚠️ ma NON sono indipendenti: convergono perche' entrambi sono guidati dal 12 marzo 2020.
|
|
**Quindi il -10% sta al {STRESS_A_MANO/L_STRUCT:.0%} del massimo strutturale: la TAGLIA e' plausibile,
|
|
la FREQUENZA no.** La riserva dello scettico e' RIDIMENSIONATA di 2-4 ordini di
|
|
grandezza, non falsificata.
|
|
|
|
6. E ANCHE PRENDENDO LA RISERVA ALLA LETTERA IL GRADINO A 1,25x PASSA: con -10% ogni anno
|
|
k* = {k_mano:.2f}x, cioe' mezzo-Kelly = {0.5*k_mano:.2f}x. **1,25x ci sta sotto; 1,50x e' al bordo** —
|
|
cioe' lo stress a mano bocciava il gradino DOPPIO, non quello singolo. Se invece si
|
|
credono ENTRAMBE le riserve insieme (drift -2 SE **e** -10% annuo) il drift atteso e'
|
|
{rr3.mean()*ANN:+.2%}/anno: a quel punto non e' il gradino a essere sbagliato, e' il libro — e chi
|
|
usa quella combinazione sta chiedendo di spegnere anche il conto di oggi.
|
|
|
|
7. 🚨 IL RISULTATO CHE NESSUNO CERCAVA, ED E' QUELLO CHE DECIDE: **il gradino non e'
|
|
implementabile con la strada che la regola del progetto dichiara pulita.** Il cap di
|
|
`config/live.json` e' un CLAMP, non un moltiplicatore: morde su {n_cap} osservazioni-asset
|
|
su {n_tot} ({n_cap/n_tot:.3%}). Alzarlo da 0,50 a 0,625 lascerebbe il libro a k=1,00x
|
|
in {n_tot-n_cap} giorni-asset su {n_tot}. Non esiste una chiave di SCALA in config: per k=1,25
|
|
servirebbe cambiare `WEIGHT`/`W_TP01`/`W_SKH` in `src/live/book.py`, cioe' CODICE su un
|
|
percorso con soldi veri. **La domanda 'il gradino e' autorizzabile' era mal posta: prima
|
|
di essere autorizzato, il gradino va COSTRUITO** — e la costruzione e' la parte che
|
|
merita una revisione, non l'aritmetica del rischio, che passa con margine.
|
|
|
|
8. SOTTOPRODOTTO NON RICHIESTO, e vale piu' del gradino: **il funding dei perpetual non e'
|
|
modellato in NESSUN backtest del progetto**. Proxy Hyperliquid su 3 anni: {np.mean(list(fund.values())):.1%}/anno
|
|
pagato dai long, esposizione long media di TP01 {esp_long:.3f}·E -> **~{costo_k1:.2%}/anno** che il
|
|
libro paga davvero e che nessuna serie sottrae. E' ~{costo_k1/(mu*ANN):.0%} del drift de-luckato,
|
|
cioe' dello stesso ordine della correzione d'ancora x0,89 che il progetto ha misurato
|
|
con cura. Non lo crea il gradino: il gradino lo amplifica del 25%.
|
|
|
|
COSA MI SMENTIREBBE, in ordine di gravita'
|
|
(a) un giorno in cui il libro perde piu' di ~{L_STRUCT*0.7:.1%} in chiusura: vorrebbe dire che
|
|
l'aritmetica della sez.(5) ha una gamba che non ho contato (un terzo strumento, un
|
|
ordine non nettato, uno stop non eseguito, il cap non applicato);
|
|
(b) uno z giornaliero peggiore di {zmin['BTC']:.1f} su BTC o {zmin['ETH']:.1f} su ETH con vol >= {tv:.0%}:
|
|
il bound e' CONDIZIONATO a quel massimo in campione e scala linearmente con esso;
|
|
(c) una configurazione in cui TP01 tiene piena convinzione ENTRANDO in un crash: nel
|
|
campione non succede mai, ed e' l'unica ragione per cui il controfattuale sfonda il
|
|
-10% e la realta' no. La mia frequenza stimata assume che resti rara;
|
|
(d) SKH01 in un regime in cui sta short molto piu' a lungo: la coda del libro e' la sua, e
|
|
la misuro su un campione in cui e' fuori mercato l'87,6% del tempo;
|
|
(e) un funding medio molto diverso su Deribit-USDC rispetto al proxy Hyperliquid: il
|
|
punto 8 e' misurato su un venue che non e' quello dove il libro esegue.
|
|
|
|
GATE — cosa e' girato e cosa no
|
|
· `marginal_vs_tp01` NON APPLICABILE: non c'e' un flusso nuovo, k e' uno scalare (corr 1,00).
|
|
· `deflated_sharpe` NON APPLICABILE: lo Sharpe e' INVARIANTE a k. Questo lavoro non chiude
|
|
il buco strutturale, lo AGGIRA: misura il peggior giorno, che e'
|
|
l'unica grandezza del rischio su cui la scala non e' invariante.
|
|
· `implausible_sharpe` NON GIRATO come funzione (darebbe lo stesso verdetto a ogni k). Il
|
|
controllo di plausibilita' e' stato fatto a mano: nessuna cella qui
|
|
promette zero perdite, e la sez.(5) e' proprio la ricerca del
|
|
'rischio FUORI dal dataset' che quel gate cerca.
|
|
· null del de-levering NON e' un gate qui: e' l'OGGETTO (la leva percorsa nei due versi).
|
|
· fortuna d'ancora EREDITATA (x{DP.DELUCK} misurato). ⚠️ NON RI-GIRATA su questo taglio, e va
|
|
detto: il peggior giorno e' un evento SINGOLO, e una banda d'ancora
|
|
su un massimo non e' la stessa statistica di una banda su una media.
|
|
Cio' che ne risente e' il drift (gia' de-luckato), non il bound.
|
|
· causalita' EREDITATA: nessun segnale nuovo, solo serie gia' validate.
|
|
· replica di controllo SUPERATA: drift/vol/Sharpe/peggior giorno de-luckati (4/4), k* MC vs
|
|
forma chiusa, scomposizione libro=gambe (max|diff| 0,0e+00),
|
|
marginali del gap (p50/p90/p99 identiche), 12,3x di liquidazione.
|
|
PARZIALE su 'k*=2x sotto stress' (qui 2,95x fine / 3,5x sulla loro
|
|
griglia): stessa direzione e ordine, cella diversa.
|
|
· risoluzione la curva di crescita e' in forma CHIUSA (E[log]): zero rumore MC.
|
|
Le celle della tabella (6a) sono APPAIATE. L'incertezza residua e'
|
|
quella del FIT di coda, ed e' negli IC bootstrap e nella banda
|
|
fra stimatori.
|
|
""")
|
|
n_boot_tot = 2 * EVT_BOOT
|
|
n_celle = (2 * len(EVT_Q_SENS)) + n_boot_tot + 2 + 2 + len(code) * len(scenari_drift) \
|
|
+ 5 + len(KSG) + 1 + 5 + 3 + 2
|
|
print(f"""
|
|
GRIGLIA DICHIARATA — {n_celle} valutazioni, contate al RIALZO:
|
|
{2*len(EVT_Q_SENS)} fit GPD (2 serie x {len(EVT_Q_SENS)} soglie) · {n_boot_tot} ri-fit bootstrap · 2 Hill · 2 t di Student
|
|
{len(code)*len(scenari_drift)} celle k* (coda x drift, APPAIATE) · 5 righe di stress · {len(KSG)} celle della griglia
|
|
coarse di growth_policy · 1 sensitivita' combinata · 5 righe di liquidazione ·
|
|
3 lenti del peggior giorno · 2 varianti di controfattuale (chiusura e minimo).
|
|
⚠️ **Nessuna cella e' stata SELEZIONATA**: la soglia EVT e' dichiarata a priori ({EVT_Q:.0%}) e le
|
|
altre tre girano come sensitivita'; il criterio di (0) e' congelato prima delle sezioni 4-6.
|
|
Percio' non c'e' un deflated-Sharpe da calcolare: non c'e' un massimo scelto su cui barare.
|
|
""")
|
|
print("\n" + "=" * 110)
|
|
print(" IL GRADINO DI LEVA A 1,25x: AUTORIZZABILE A CONDIZIONE DI COSTRUIRE PRIMA UNA CHIAVE DI SCALA ESPLICITA IN CONFIG")
|
|
print("=" * 110)
|
|
print(f"\n [tempo totale {time.time()-t_start:.0f}s]")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|