diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 1f1849c..384f49d 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -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. diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py index b36fac0..22d2ddc 100644 --- a/scripts/research/r0822_basis_calendar.py +++ b/scripts/research/r0822_basis_calendar.py @@ -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']} " diff --git a/scripts/research/r0822_volsize_skeptic.py b/scripts/research/r0822_volsize_skeptic.py index 49efa1b..95d9c8d 100644 --- a/scripts/research/r0822_volsize_skeptic.py +++ b/scripts/research/r0822_volsize_skeptic.py @@ -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)