research(wave-0822): lo scettico conferma il gradino di leva ma corregge due contorni — e la lente close-only e' cieca solo sulle regole a UN giorno

This commit is contained in:
Adriano Dal Pastro
2026-08-22 17:36:45 +00:00
parent 5fe931e00d
commit 32ed18222e
3 changed files with 134 additions and 34 deletions
+58
View File
@@ -19,6 +19,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato.
| 5 | SKEW | **SCARTATO** (Q1, Q2) + **LEAD** (Q3, gate 2027-02-22) | il prezzo muove lo skew (t 3,1-9,4 su 8/8 test), **non il contrario** (max |t| in avanti 2,35 contro 2,08 atteso dal rumore). Ma Q3 e' grosso: **il f=0,73 di VRP01 e' per il 42% STRUTTURA A TERMINE e solo per il 25% skew** |
| — | **HL-EXEC** (audit di fatto) | **3 falsificazioni misurate** | il pavimento vero e' **$10 (non $5)** e il taker **4,50 bps (non 5,0)** — ma il *"XS01 serve ~$20k"* e' **refutato del tutto** (nessuna soglia da min-order), il *"XSR01 ~$5.000"* e' **conservativo di 1,7x** (vero ~$3.000), e lo **slippage "rischio #1" di XSR01 e' refutato** con margine **21x** |
| — | **SLIP-AUDIT** (audit di fatto) | **SCARTATO** = nessun costo nascosto a questa taglia | i backtest a 10 bps RT restano **conservativi di ~1,5 bps/lato**; nessuna evidenza di impatto sopravvive al null (p=0,274; estremi avversi **1/18 contro 4,7 attesi**). **Ma la misura ha una data di scadenza** |
| — | **SCETTICO su GROWTH-POLICY** | **REGGE** (condizione bloccante DECADUTA) | la lente wick accoppiata **non cambia il gradino**: e' un ricarico moltiplicativo **costante ~3,5%** sul maxDD, non un'amplificazione (Δ/k **cala**). Ma su una regola a **UN giorno** close-only e' **esattamente cieca** (rapporto INF) -> la lezione 25/07 e' vera e **non si trasferisce** |
## Note che sopravvivono ai singoli filoni
@@ -441,3 +442,60 @@ occorrenza** dello schema `fee_watch`: misurare lo slippage sul feed certificato
⚠️ Con n=18 **la media in bps e' inservibile** — un solo giorno (19/08, ETH +14% in 6h) la ribalta:
si cita la **mediana**. E `REF-A` (50 bps) **non e' slippage**: e' la differenza di FASE fra l'ora
del cron e la chiusura giornaliera, gia' misurata bene il 26/07 su 24 ancore appaiate.
### SCETTICO su GROWTH-POLICY (r0822_leverage_skeptic.py, ~175 celle, griglia ridotta a k<=5x e dichiarato)
**Replica superata TRE volte** prima di ogni numero nuovo: marginali del gap 25/07 (p50 0,16 /
p90 1,03 / p99 2,71 contro 0,17 / 1,03 / 2,70), meccanismo del decile (~3x, pubblicato ~3x),
e €500/mese -> **14,7a / P<=20a 99%** al numero pubblicato.
**Controllo positivo superato:** una lente *indipendente* (gap rimescolato) sovrastima il maxDD di
+0,13pp su +0,45 e i breach daily-loss di **1,5-2,4x** — stesso verso e taglia del finding 25/07
("2,0-2,9x"). **La macchineria ha potenza, quindi il suo "nessuna differenza" vale.**
✅ Risoluzione MC dichiarata: **0,01%/a** nella regione eseguibile, 0,05pp sul Δ appaiato di maxDD ->
le differenze riportate (0,45-0,76pp) sono **9-15x la risoluzione**.
📌 **RISPOSTA ALLA DOMANDA: NO, l'effetto e' SOTTO-proporzionale.** Il rapporto fra le due maxDD e'
**piatto a 1,025-1,037** su tutta la griglia e Δ/k **cala** (0,478% -> 0,217%). **Meccanismo: il
maxDD e' una statistica MULTI-GIORNO, il wick e' un fatto di UN giorno.** Tempo al bersaglio
**identico** fra le due lenti; guadagno appaiato 1,00x->1,50x = **2,88 anni** in entrambe.
Nessuna liquidazione a nessun k<=5x sotto nessuna lente (servirebbe **12,3x** sul peggior giorno vissuto).
🚨 **IL RISULTATO TRASFERIBILE — la lezione del 25/07 e' vera e NON si trasferisce.** Su una regola a
**UN giorno** l'accoppiamento cambia tutto: a soglia daily-loss 5% e k=1,00-1,25 close-only conta
**0,00 breach/anno** dove il vero ne conta **0,40 e 1,21** (`acc./close = INF`, cecita' esatta); a
soglia 3% e k=1, **0,40 contro 2,28 = 5,7x**. Su una statistica **multi-giorno** lo stesso difetto
vale **3,5%**. ⚠️ **Non assolve close-only in generale: sul canale PROP/FUNDED, dove il vincolo
binding E' una regola di daily-loss per-conto, la lente accoppiata resta OBBLIGATORIA.**
⚠️ **CORREZIONE a una spiegazione pubblicata (il fatto regge, la spiegazione no):** il **26,8%** di
giorni con `m == R` **non** sono "il giorno brutto che chiude sul minimo" — stanno nei decili
**CENTRALI** (56-88%), i giorni in cui il libro e' **flat**. Nel decile peggiore chiude sul minimo
il **4%**. E un giorno di crash **chiude quasi sul proprio minimo** (gap medio nel 5% peggiore
0,62% contro 1,75% nel 5% migliore) -> **la lente accoppiata non peggiora lo stress di coda.**
📌 **Il gradino 1,00->1,50 resta NON AUTORIZZATO** — cade **una** delle tre riserve, non le tre.
Restano: drift stimato su 7,4 anni, e coda assente dal dataset (un giorno 10%/anno porta k* a 2x).
Unica colonna a favore della prudenza: **P(sfondare una soglia FISSA) e' amplificata piu' del maxDD**
(1,26x a k=1) perche' vicino alla soglia la densita' e' fitta — **e' quella la colonna con cui si
costruisce una frontiera iso-rischio, non il maxDD mediano**.
📌 **ATTACCO 2 — il 12,14% di TP01 e' VERO, ma il meccanismo dichiarato e' META', e la strada
implicita e' REFUTATA.** Scomposizione esatta, verificata sullo sleeve di **produzione**:
`20,00% x 0,682 (dentro la gamba) x 0,890 (diversificazione BTC/ETH, corr 0,59) = 12,14%`.
E il x0,682 sono **due meta' quasi uguali**: flat nel **44,3%** delle barre (x0,746) **e** convinzione
**parziale** quando non e' flat (`tsmom_blend` = media di 3 segni: 1/3 o 2/3) (x0,789).
**"TP01 e' long-flat" spiega circa META'** — e la differenza non e' accademica: essendo
*convinzione*, **alzare `target_vol` aumenta la size proprio nei giorni in cui il segnale e' piu'
debole**. Il terzo fattore (x0,89) **non e' un difetto d'etichetta: e' diversificazione**, ed e'
cio' che si vuole.
⚠️ **E TP01 e' un caso particolare, non la regola: XS01 dichiara lo stesso 20% e ne realizza 20,59%.**
🚨 **`target_vol` e `k` NON sono la stessa leva** (e non per il leverage cap, che non morde: 0-1% di
barre al cap fino a tv=60%): **`target_vol` scala il 75% del libro e lascia fermo il 25%** -> a
tv=30% il libro non e' piu' 75/25 ma **82/18**, a 60% **90/10**. E' simultaneamente una leva **e un
tilt di pesi**, e il tilt **FALLISCE `weights_tilt_null`** (gate_pass=False, delta_insample 0,0815
a tv=30% / 0,2187 a tv=60%). A pari scala 1,50 il libro via `target_vol` fa Sharpe **1,72** contro
**1,82** via k. -> **la strada pulita per un puro cambio di SCALA e' il cap di config, non il target_vol.**
**Cosa cambia dei numeri pubblicati: NIENTE di misurato** — ogni Sharpe/maxDD/muro/traiettoria e'
calcolato sulla **serie realizzata**; `target_vol` non compare in nessuna formula di rischio a valle.
**Ipotesi dell'agente refutata dal suo stesso numero:** i contributi di rischio sono **73/27**
contro pesi 75/25. **Cio' che resta:** sbaglia chi legge l'**etichetta**, non chi legge i **pesi**
un lettore che prendesse "20%" per buono attribuirebbe a TP01 l'**89%** del rischio invece del
**73%**, cioe' sbaglierebbe di 16 punti proprio la domanda *"quale gamba mi fa male"*.
📌 Il test di guardia `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **non protegge "la leva
e' pericolosa"**: protegge la conclusione del 26/07 secondo cui la **liquidation fee 1% di Deribit e'
irrilevante**, che vale interamente perche' a <=1x servirebbe un movimento avverso ~100%. Chi alza il
cap deve **rifare `r0726_fee_sensitivity`**, non aggirare l'assert.
+12 -6
View File
@@ -638,8 +638,10 @@ def stats(daily: pd.Series) -> dict:
cagr = float(eq.iloc[-1] ** (1 / yrs) - 1) if yrs > 0.5 and eq.iloc[-1] > 0 else float("nan")
ho = r[r.index >= HOLDOUT]
sho = float(ho.mean() / ho.std() * math.sqrt(365.25)) if len(ho) > 30 and ho.std() > 0 else float("nan")
yrs_ = max(len(r) / 365.25, 1e-9)
return dict(sharpe=sh, holdout=sho, maxdd=dd, cagr=cagr, n=len(r),
vol=float(r.std() * math.sqrt(365.25)))
vol=float(r.std() * math.sqrt(365.25)),
se_ann=float(r.std() * math.sqrt(365.25) / math.sqrt(yrs_)))
def delevering_null(cand: pd.Series, base: pd.Series) -> dict:
@@ -737,7 +739,7 @@ def main() -> None:
# ---------------- STEP 4: griglia ----------------
print("\n[4] GRIGLIA — dichiarata PRIMA, contata AL RIALZO")
FAM = {"CAL": ["static_long", "static_short", "zrev", "zmom"],
"FvP": ["carry", "static_long", "zrev", "zmom"]}
"FvP": ["carry", "static_long", "static_short", "zrev", "zmom"]}
WINS = [720, 2160, 4320] # 30 / 90 / 180 giorni in ore
ROLLS = [7, 21]
THRS = [0.0, 1.0]
@@ -794,7 +796,7 @@ def main() -> None:
print("\n LE CELLE CANONICHE (quelle che un lettore chiede per nome), roll=7g:")
for fam, sig, lab in [
("FvP", "carry", "vendi il datato quando e' in CONTANGO / compra perp (il carry classico)"),
("FvP", "static_short", "SEMPRE corto il datato / lungo perp"),
("FvP", "static_short", "SEMPRE corto il datato / lungo perp = LA RACCOLTA DEL CONTANGO"),
("FvP", "static_long", "SEMPRE lungo il datato / corto perp (il contrario)"),
("CAL", "static_long", "calendar: lungo back / corto front"),
("CAL", "static_short", "calendar: corto back / lungo front")]:
@@ -802,8 +804,8 @@ def main() -> None:
if len(sub):
r = G.loc[sub.index[0]]
print(f" {fam}/{sig:12s} Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} "
f"CAGR {r.cagr*100:+6.2f}% DD {r.maxdd*100:5.2f}% vol {r.vol*100:5.2f}%"
f" <- {lab}")
f"CAGR {r.cagr*100:+6.2f}% (+-{r.se_ann*100:.2f} SE) DD {r.maxdd*100:5.2f}% "
f"vol {r.vol*100:5.2f}% <- {lab}")
print("\n migliori 8 celle per Sharpe FULL (NON e' la selezione — solo panorama):")
for i in G.sharpe.nlargest(8).index:
@@ -826,7 +828,11 @@ def main() -> None:
print(f" cella scelta AL BUIO: {b.family} {b.sig} W={int(b.win)}h roll={int(b.roll)}g thr={b.thr}")
print(f" Sharpe in-sample {ins[best_i]:+.2f} -> FULL {bs['sharpe']:+.2f} "
f"HOLD-OUT {bs['holdout']:+.2f} maxDD {bs['maxdd']*100:.2f}% "
f"CAGR {bs['cagr']*100:+.2f}% vol {bs['vol']*100:.2f}%")
f"CAGR {bs['cagr']*100:+.2f}% (+-{bs['se_ann']*100:.2f} SE annuo) "
f"vol {bs['vol']*100:.2f}%")
print(f" NB con {bs['n']/365.25:.1f} anni e vol {bs['vol']*100:.1f}% l'errore standard del "
f"rendimento annuo e' {bs['se_ann']*100:.2f}pp: sotto ~{2*bs['se_ann']*100:.1f}pp/anno "
f"NIENTE e' distinguibile da zero, ne' in un verso ne' nell'altro.")
best_full = G.sharpe.idxmax()
print(f" (per confronto, la cella col miglior FULL sarebbe stata "
f"{G.loc[best_full,'family']} {G.loc[best_full,'sig']} "
+64 -28
View File
@@ -271,6 +271,15 @@ def main() -> None:
("V1", EXV1, FUN_V1, BK1, CT1)):
lg = legs(EXX, FUN)
base = lg["BASE"]
if tag == "V2":
# controlli di §1(c): L calcolata sulla gamba COMBINATA (cio' che fa il ramo
# NAIVE del bersaglio), applicata nei DUE modi. Se coincidono, il "fantasma"
# non e' un problema di QUANDO si applica la size.
for w in (30, 90):
Lc = V.leverage_series(base, **dict(FROZEN, w=w))
lg[f"NAIVEcomb w{w}"] = base * Lc.reindex(base.index).fillna(1.0)
lg[f"COMBingr w{w}"] = V.leg_daily(
EXX, {a: V.sizes_from_L(EXX[a], Lc) for a in ASSETS})
for nm, s in lg.items():
b, c, m_s = iso_pair(TP, s, base)
BKd.setdefault(nm, {})[o] = b
@@ -386,32 +395,29 @@ def main() -> None:
print(f" {a}: max|dif| = {np.max(np.abs(j['a']-j['b'])):.2e} "
f"Sh ingresso {sh(s_in):.4f} vs Sh moltiplicazione {sh(s_mul):.4f}")
print("\n (c) ALLORA COS'E' IL '+0,04 FANTASMA'? Il controllo NAIVE del bersaglio calcola L")
print(" sulla gamba 50/50 COMBINATA; la versione 'causale' la calcola PER ASSET.")
print(" Terza variante di controllo: L combinata, applicata come size all'INGRESSO.")
lg_base = {o: None for o in offs}
d_comb, d_perass = [], []
for o in offs[: min(6, len(offs))]:
E2 = {a: V.extract(a, o) for a in ASSETS}
base = V.leg_daily(E2, {a: V.size_flat(E2[a]) for a in ASSETS})
Lc = V.leverage_series(base, **FROZEN)
naive = base * Lc.reindex(base.index).fillna(1.0)
comb_in = V.leg_daily(E2, {a: V.sizes_from_L(E2[a], Lc) for a in ASSETS})
per_ass = V.leg_daily(
E2, {a: V.sizes_from_L(E2[a], V.leverage_series(E2[a]["base_daily"], **FROZEN))
for a in ASSETS})
j = pd.concat({"a": naive, "b": comb_in}, axis=1, join="inner").dropna()
d_comb.append(float(np.max(np.abs(j["a"] - j["b"]))))
b1, c1, _ = iso_pair(TP, naive, base)
b2, c2, _ = iso_pair(TP, per_ass, base)
d_perass.append(sh(hold(b1)) - sh(hold(c1)) - (sh(hold(b2)) - sh(hold(c2))))
del E2
print(f" L combinata: 'moltiplica la serie' vs 'size all'ingresso' -> "
f"max|dif| mediano {np.median(d_comb):.2e} (identiche)")
print(f" differenza ISO dShHOLD [L combinata] - [L per asset], mediana appaiata su "
f"{len(d_perass)} ancore: {np.median(d_perass):+.3f}")
print(" -> il divario NAIVE/CAUSALE non e' QUANDO si applica la size, e' SU QUALE")
print(" SERIE si calcola L (gamba combinata vs per asset).")
print("\n (c) ALLORA COS'E' IL '+0,04 FANTASMA'? Il ramo NAIVE del bersaglio calcola L")
print(" sulla gamba 50/50 COMBINATA e MOLTIPLICA la serie; il ramo 'causale' calcola L")
print(" PER ASSET e la applica all'INGRESSO. Sono DUE differenze in una. Le separo:")
for w in (30, 90):
d = []
for o in offs:
j = pd.concat({"a": BK2[f"NAIVEcomb w{w}"][o], "b": BK2[f"COMBingr w{w}"][o]},
axis=1, join="inner").dropna()
d.append(float(np.max(np.abs(j["a"] - j["b"]))))
print(f" w{w}: [L combinata, MOLTIPLICA] vs [L combinata, size all'INGRESSO] -> "
f"max|dif| mediano {np.median(d):.2e} = IDENTICHE")
print(f"\n {'coppia':<38}{'dShFULL':>10}{'dShHOLD':>10} (delta GREZZI vs baseline nudo,")
print(f" {'':<38}{'':>10}{'':>10} la convenzione del §4 del bersaglio)")
for w in (30, 90):
for nm in (f"NAIVEcomb w{w}", f"COMBingr w{w}",
(lead if w == 90 else f"VTL tv20 w{w}")):
gF = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o: BK2["BASE"][o], offs,
metric=sh)
gH = A.anchor_luck_delta(lambda o, n=nm: hold(BK2[n][o]),
lambda o: hold(BK2["BASE"][o]), offs, metric=sh)
print(f" {nm:<38}{gF['median_paired']:>+10.3f}{gH['median_paired']:>+10.3f}")
print(" -> il 'fantasma' e' interamente [L combinata] - [L per asset]: una scelta di")
print(" SERIE DI RIFERIMENTO, non un look-ahead. Il ramo NAIVE non e' non-causale.")
# ------------------------------------------------------------------ §2 TEST PRE-REGISTRATO
print("\n" + "-" * L)
@@ -560,7 +566,34 @@ def main() -> None:
# ------------------------------------------------------------------ §5 sintesi
print("\n" + "-" * L)
print(" 5. SINTESI DEI NUMERI CHE CONTANO")
print(" 5. GATE (per il contratto di consegna)")
print("-" * L)
can = offs[0]
legs_can = {}
for nm in ("BASE", lead, "BIN L>=med (0/1)"):
legs_can[nm] = (V.leg_daily(EXV2, {a: V.size_flat(EXV2[a]) for a in ASSETS})
if nm == "BASE"
else V.leg_daily(EXV2, {a: FUN_V2[nm](EXV2[a]) for a in ASSETS}))
nlos = int((EXV2["BTC"]["net"] < 0).sum() + (EXV2["ETH"]["net"] < 0).sum())
ntot = int(len(EXV2["BTC"]["net"]) + len(EXV2["ETH"]["net"]))
for nm, s_ in legs_can.items():
m = A.marginal_vs_tp01(s_)
imp = A.implausible_sharpe(s_, n_trades=ntot, n_losing_trades=nlos)
print(f" {nm:<22} marginal {str(m.get('marginal_verdict')):<9} corr {m.get('corr_full')}"
f" implausible={imp['implausible']} (perdite/attive "
f"{imp.get('loss_frac', 0)*100:.1f}%)")
trials = [float(np.median([sh(BK2[n][o]) for o in offs])) for n in BK2]
dsr_c, sr0 = A.deflated_sharpe(float(np.median([sh(BK2[lead][o]) for o in offs])), trials,
BK2[lead][can])
dsr_b, _ = A.deflated_sharpe(med_F, trials, BK2["BASE"][can])
print(f" deflated-Sharpe: candidato {dsr_c:.3f} / BASELINE {dsr_b:.3f} su {len(trials)} "
f"trial (sd {np.std(trials, ddof=1):.4f}) -> VACUO, lo passa anche il baseline")
print(" null de-levering: DEGENERE per costruzione (sh(k*base) == sh(base)); il suo")
print(" sostituto e' il confronto ISO-PESO usato in tutte le tabelle sopra.")
print(" causalita': §1(a) 50/50 size ricalcolate da dati troncati, max|dif| 0.00e+00.")
print("\n" + "-" * L)
print(" 6. SINTESI DEI NUMERI CHE CONTANO")
print("-" * L)
print(f" fortuna d'ancora del LIBRO BASE: ShFULL canonico "
f"{sh(BK2['BASE'][0]) if 0 in BK2['BASE'] else float('nan'):.3f} vs mediana "
@@ -569,7 +602,10 @@ def main() -> None:
f"{dLEADH['median_paired']:+.3f} HOLD")
print(f" riprodotto dal FILTRO BINARIO: {RES['BIN L>=1 (0/1)']['dF']:+.3f} FULL / "
f"{RES['BIN L>=1 (0/1)']['dH']:+.3f} HOLD")
print(f" controllo di segno (INVERSO): {RES['BIN L<1 (INVERSO)']['dF']:+.3f} FULL")
print(f" riprodotto dal BINARIO alla MEDIANA:{RES['BIN L>=med (0/1)']['dF']:+.3f} FULL / "
f"{RES['BIN L>=med (0/1)']['dH']:+.3f} HOLD ({RES['BIN L>=med (0/1)']['nF']}/{len(offs)} ancore)")
print(f" controllo di segno (INVERSO): {RES['BIN L<1 (INVERSO)']['dF']:+.3f} FULL "
f"({RES['BIN L<1 (INVERSO)']['nF']}/{len(offs)}) -> il filtro e' reale, non un artefatto")
print(f"\n fatto in {time.time()-t0:.0f}s")
print("=" * L)