diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index e685821..e656596 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -14,7 +14,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | 11 | VRP-QUOTE-VERE | **SCARTATO** + **1 difetto di RACCOLTA in produzione** | il campione contiene l'**astensione** della strategia (0/19 settimane passano IV-rank>0.30) e il titolo non sopravvive all'ora d'ingresso; ma soprattutto: **`collect_chain.py` raccoglie la famiglia di contratti che il conto NON puo' marginare** | | 7 | TERM-STRUCTURE | **SCARTATO** + 1 LEAD stretto (decisione 2027-02-01) | la pendenza e' un **termometro contemporaneo** (picco CCF a lag −1h/0h); il carry non paga il bid-ask (**18,6 giorni di roll-down per pagare UN round-trip**); come gate e' la **5ª occorrenza** della ridondanza col trend | | 6 | OI-PIN | **SCARTATO** | il max-pain batte uno strike casuale ma **non batte mai (0/24) la media a 7 giorni dello spot**, un livello che non usa NESSUN dato di opzioni: non e' pinning, e' reversione verso il centro recente con l'OI come stimatore rumoroso di quel centro | -| 8 | VOL-SIZE | **LEAD** (gate 2026-12-22) + 1 falsificazione | dare a SKH01 una size per-trade regge a **23/23 ancore**, 8/8 anni, null di permutazione e trasferimento su V1 — ma vale **+0,07 di Sharpe di libro, un terzo della fortuna d'ancora del libro stesso (+0,196)**. Il vol-target di **libro** e' invece falsificato: compra peso SKH gia' respinto e peggiora l'eseguibilita' | +| 8 | VOL-SIZE | **LEAD RIDIMENSIONATO** — gate 22/12 **gia' fallito oggi** | dare a SKH01 una size per-trade regge a **23/23 ancore**, 8/8 anni, null di permutazione e trasferimento su V1 — ma vale **+0,07 di Sharpe di libro, un terzo della fortuna d'ancora del libro stesso (+0,196)**. Il vol-target di **libro** e' invece falsificato: compra peso SKH gia' respinto e peggiora l'eseguibilita' | | — | **XSR-REPRO** (integrita') | 🚨 **DIFETTO DI PRODUZIONE** | il numero 1.82 e' SPIEGATO e non era sbagliato (era su una **terza** lente, e su una barra non ancora chiusa) — ma cercandone la causa e' emerso che **`paper_xsr` registra ~41 minuti di mercato al giorno**, non un giorno. Tre gate pre-registrati leggono serie costruite cosi' | | 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** | @@ -252,13 +252,15 @@ recente, e l'OI e' solo uno stimatore rumoroso di quel centro. ### 8 — VOL-SIZE (r0822_vol_size.py, 28 trial + 23 ancore) ✅ Sanity **bit-exact**: ricomposizione trade-by-trade `max|diff| = 0.0` contro `run_asset` e contro `sleeves._skyhook_returns()`. -🚨 **DIFETTO DI METODO GENERALE, trovato dall'agente nella propria prima stesura e MISURATO invece +❌ **RITIRATO DALLO SCETTICO (vedi in fondo): il difetto qui sotto NON ESISTE su questo codice.** +Lasciato a verbale perche' la regola era stata pubblicata e va vista insieme alla sua smentita. +~~🚨 **DIFETTO DI METODO GENERALE, trovato dall'agente nella propria prima stesura e MISURATO invece che cancellato: su uno sleeve a equity a GRADINO, un overlay giornaliero e' NON CAUSALE.** Tutto il P&L di un trade SKH01 e' contabilizzato il giorno di **CHIUSURA**, fino a **3,83 giorni dopo l'ingresso**; moltiplicare la serie giornaliera per `L_t` (che usa solo dati <= t−1) **passa qualunque controllo di causalita' scritto sulla serie giornaliera** — il look-ahead sta nella CONTABILITA', non nella formula. Costo misurato: **+0,04 di Sharpe hold-out fantasma**. -**REGOLA: un overlay si applica alla data d'INGRESSO, non a quella in cui il P&L viene contabilizzato.** +**REGOLA: un overlay si applica alla data d'INGRESSO, non a quella in cui il P&L viene contabilizzato.**~~ **<- RITIRATA, vedi sotto** 🚨 **REGOLA 2 — il null del de-levering e' DEGENERE quando la variante e' una ri-scalatura** (`sh(k*base) === sh(base)`): il test che serve non e' sul DD ma **ISO-PESO sullo Sharpe**, altrimenti si sta solo misurando di quanto e' cambiato il peso di uno sleeve. Qui era decisivo: quasi ogni @@ -545,3 +547,41 @@ dall'ora scelta = **un'altra strategia, non un'ancora**; (c) **un gate che stamp verificato prima di dichiararlo "non girato"** — stampava le chiavi sbagliate. ✅ Disciplina di rete rispettata: <=2 req/s e **sospensione automatica nella finestra :25-:29** di `cron_chain` (verificato: si e' sospeso da solo per 264s). + +### SCETTICO su VOL-SIZE (r0822_volsize_skeptic.py, 43 valutazioni x 23 ancore) +✅ Replica **bit-exact** prima di ogni attacco: gamba 50/50 vs `sleeves._skyhook_returns()` +**max|diff| = 0,0**; e il titolo riprodotto **esattamente** (+0,070 ISO dShFULL 23/23, +0,101 HOLD). +🚨 **RITIRA UNA REGOLA PUBBLICATA POCHE ORE PRIMA — il difetto di contabilita' NON ESISTE.** +`backtest_signals` fa `equity[i_ent:i_ex+1] = capital`: il P&L e' contabilizzato al giorno +d'**INGRESSO** in **291/293 (99,3%)** dei trade multi-giorno, all'uscita in **4**. Troncando i dati +alla barra d'ingresso e ricalcolando le size da zero: **50/50 identiche, max|diff| = 0,0**. E la +"riparazione" e' un **no-op bit-exact** (max|diff| 6,7e-16, Sharpe identico a 4 decimali). +**Il "+0,04 fantasma" era interamente `[L sulla gamba COMBINATA] − [L PER ASSET]`** — due varianti +entrambe causali — **e la variante scartata come "non causale" era la MIGLIORE sull'hold-out +(+0,039)**. *Il costo di una regola derivata da un difetto inesistente e' gia' stato pagato.* +📌 **REGOLA CORRETTA (piu' debole e vera):** *"applicare un overlay alla data d'ingresso"* resta un +buon consiglio **generale**, ma su questo motore le due date **coincidono** — e va verificato con un +troncamento bit-exact **prima** di scartare un ramo, non dedotto dalla forma dell'equity. +🚨 **IL GATE PRE-REGISTRATO AL 22/12/2026 E' GIA' FALLITO OGGI, e nel modo piu' netto.** L'agente +aveva scritto: *"se l'effetto e' UGUALE su V1, la spiegazione e' sbagliata anche se il numero +regge"*. Misurato: |effetto| mediano di famiglia **V1 0,067 contro V2-DD 0,032** — l'inverse-vol +funziona **il DOPPIO dove lo stop e' gia' in ATR**, cioe' esattamente al contrario della previsione, +in **8 celle su 8** con **23/23 ancore positive**. E il test **ha potenza** (su V2-DD la famiglia +arriva a +0,067, quanto il LEAD). +📌 **La spiegazione avversaria e' CONFERMATA e la sua forma minima e' un binario.** `BIN L>=med` +(size **0/1** alla mediana espandente, nessuna magnitudine, 194/406 trade scartati) vale +**+0,080 FULL (20/23) = 115% del LEAD**. Controllo di segno superato: il binario **INVERSO** fa +**−0,159 (0/23)**. Canali: **FREQUENZA +0,051 (23/23)** contro **MAGNITUDINE +0,003 (17/23)**. +I trade tenuti hanno net medio **2,03% contro 0,36%** degli scartati. +✅ **Un attacco dello scettico e' FALLITO, e lo dichiara:** l'asse della finestra **non e' un picco** +— l'argmax in-sample-only cade su **w=90 = la cella pubblicata**, corr(IS, HOLD) fra le 9 finestre +**+0,63**, plateau da 45 a 270. Sotto w=45 la finestra contiene **1-3 barre attive** (colonna +misurata) e il segno si rovescia: non e' fragilita', e' campione vuoto. +**-> quindi anche la mia nota "picco, non plateau" nel filone 8 va letta come SUPERATA.** +Stima onesta se `w` e' scelto (mediana di famiglia): **+0,025 FULL**. +📌 **CONSEGUENZA:** il LEAD non ha piu' una condizione aperta da attendere — la sua gamba +esplicativa e' chiusa oggi. Se qualcuno vorra' portarlo avanti, **il candidato giusto non e' la +formula ma il binario**: meno gradi di liberta', **dimezza gli ordini** (migliora l'eseguibilita' a +$635), 115% sul FULL — ma paga in fragilita' d'ancora (20/23 contro 23/23, banda che include −0,061, +hold-out +0,072 contro +0,101). **Resta un terzo della fortuna d'ancora del libro stesso (+0,196): +niente qui giustifica di toccare pesi, cron o config.** diff --git a/scripts/research/r0822_alt_options.py b/scripts/research/r0822_alt_options.py index 1a433e3..7a6af36 100644 --- a/scripts/research/r0822_alt_options.py +++ b/scripts/research/r0822_alt_options.py @@ -356,9 +356,16 @@ def liquidita(rows: list[dict], tab: dict) -> None: s = stat[f] print(f"{f:<10} {s['oi']:>11} {rA[f]:>8} {s['puts']:>10} {s['two']:>10} " f"{s['pct']:>9.0%} {rB[f]:>8}") - print("\n 📌 LA CLASSIFICA SI ROVESCIA. SOL_USDC e' 1° per open interest e ULTIMO per quote") - print(" presenti; BTC_USDC, dichiarato 'praticamente morto' (5 strumenti con OI>=100), ha") - print(" quasi tutti i put quotati a due lati. L'open interest misura posizioni ACCUMULATE,") + # la correlazione di rango si CALCOLA, non si racconta: sulle sole famiglie lineari, + # cioe' quelle su cui la domanda del filone e' stata posta. + a = np.array([rA[f] for f in LINEAR], float) + c = np.array([rB[f] for f in LINEAR], float) + rho = float(np.corrcoef(a, c)[0, 1]) + print(f"\n 📌 LA CLASSIFICA SI ROVESCIA. Correlazione di rango fra i due criteri sulle sei") + print(f" famiglie lineari: {rho:+.2f}. SOL_USDC e' 1ª per open interest fra le lineari e") + print(" ULTIMA di tutte per quote presenti; BTC_USDC, dichiarato 'praticamente morto' (5") + print(" strumenti con OI>=100), ha quasi tutti i put quotati a due lati. L'open interest") + print(" misura posizioni ACCUMULATE,") print(" non la possibilita' di negoziare ADESSO — e sull'universo lineare le due cose non") print(" hanno quasi nulla a che vedere.") print(" ⚠️ Ricaduta sul codice di produzione: `collect_chain.py` prefiltra a OI>=100. Sulle") @@ -366,6 +373,28 @@ def liquidita(rows: list[dict], tab: dict) -> None: print(" suo nome lo lascia credere. 4ª occorrenza di 'una riga presente non e' un dato") print(" presente' — qui in versione: 'una posizione aperta non e' un prezzo negoziabile'.") + print("\n" + "-" * 104) + print("2b. E ALLORA DOV'E' LA LIQUIDITA' CHE SI VEDE NEL VOLUME? Nel mercato a BLOCCHI,") + print(" che ha un minimo d'ordine suo — e non e' raggiungibile con $635.") + print("-" * 104) + print(f"{'famiglia':<10} {'con volume 24h':>15} {'volume 24h $':>14} {'blocco min':>11} " + f"{'blocco min $':>13} {'x il conto':>11}") + for f in FAMS: + sub = [r for r in rows if r["fam"] == f] + if not sub: + continue + nv = sum(1 for r in sub if r["vol"] > 0) + vusd = sum(r["vol_usd"] for r in sub) + bm = sub[0]["blockmin"] + bm_usd = bm * tab[f]["S"] * (1.0 if not is_inv(f) else 1.0) + print(f"{f:<10} {nv:>15} {vusd:>14,.0f} {bm:>11g} {bm_usd:>13,.0f} " + f"{bm_usd/CAPITAL:>10.0f}x") + print("\n 📌 SOL_USDC ha molti piu' strumenti con volume nelle ultime 24h che quotati a due") + print(" lati: quelle opzioni si negoziano, ma via blocchi/RFQ. Il minimo di blocco e' un") + print(" fatto del venue, non un'opinione, e a $635 e' fuori portata di due ordini di") + print(" grandezza. **Non si conclude che il mercato alt non esista: si conclude che la parte") + print(" a cui questo conto ha accesso — il book continuo — e' quella sottile.**") + # ---------------------------------------------------------------- §3 la struttura @@ -461,9 +490,65 @@ def struttura(rows: list[dict]) -> dict: f"{len(gi):>12} {len(ge):>11}") print(" Se la colonna `eseg.` si allontana dal target -0.28/-0.10, la struttura che si") print(" comprerebbe NON e' quella richiesta: la griglia degli strike quotati la deforma.") + + print("\n --- CONTROLLO POSITIVO: questo snapshot e' di SABATO. E' atipico? ---") + print(" Non posso aspettare una settimana, ma posso puntare lo stesso strumento su una") + print(" grandezza che il progetto ha GIA' misurato su 19 settimane di giorni feriali:") + print(" attraversare lo spread sul canonico BTC/ETH inverse costa ~10% del credito (30/07).") + for f in INVERSE: + if f in res: + print(f" {f:<4} inverse: costo di attraversamento oggi = " + f"{(1-res[f]['fv'])*100:5.1f}% del credito (atteso ~10%)") + print(" Se coincide, il sabato non sta degradando i book che il progetto conosce, e la") + print(" lettura sulle famiglie nuove non e' un artefatto del giorno. **E' un controllo") + print(" debole** (una grandezza sola, un istante), non una prova di tipicita'.") + robustezza_dte(rows) return res +def robustezza_dte(rows: list[dict]) -> None: + """L'UNICO grado di liberta' di questo file, esposto invece che nascosto. + + VRP01 e' settimanale (tenor 7g, finestra 4-10). Qui la finestra e' stata allargata a 4-45 + per dare piu' scadenze alle famiglie alt, che ne hanno poche. Allargare una finestra e' + una scelta, e una scelta va misurata: se la conclusione dipendesse da essa, sarebbe la + conclusione a essere stata scelta. + """ + global DTE_HI + print("\n --- ROBUSTEZZA: la finestra DTE e' il mio unico grado di liberta' ---") + print(f" {'famiglia':<10} {'finestra':>9} {'scad':>5} {'f_venue':>8} {'spr% corta':>11} " + f"{'cred eseg$':>11} {'maxloss$':>9}") + orig = DTE_HI + try: + for hi in (10.0, 45.0): + DTE_HI = hi + for f in FAMS: + gg = scegli(rows, f, eseguibile=True) + fv, ss, ce, ml = [], [], [], [] + for g in gg: + s, lo = g["short"], g["long"] + cm = usd(s["mark"], s) - usd(lo["mark"], lo) + c = usd(s["bid"], s) - usd(lo["ask"], lo) + if cm <= 0: + continue + fv.append(c / cm); ss.append(rel_spread(s)); ce.append(c) + ml.append(loss_usd(s["K"], lo["K"], s) - c) + w = f"{DTE_LO:.0f}-{hi:.0f}g" + if not fv: + print(f" {f:<10} {w:>9} {len(gg):>5} {'n/d':>8}") + continue + print(f" {f:<10} {w:>9} {len(fv):>5} {np.median(fv):>8.3f} " + f"{np.nanmedian(ss)*100:>10.1f}% {np.median(ce):>11.2f} " + f"{np.median(ml):>9.2f}") + print() + finally: + DTE_HI = orig + print(" 📌 Da leggere sull'ORDINE, non sulla terza cifra: le quote si muovono fra un giro e") + print(" l'altro (su tre snapshot a pochi minuti ETH_USDC ha dato 0.947-0.983, le inverse") + print(" 0.847-0.942, le alt 0.79-0.87, SOL sempre negativo). Cio' che NON si muove e' la") + print(" graduatoria, e alla finestra VERA di VRP01 (4-10g) e' piu' netta, non meno.") + + def profondita(rows: list[dict], fams: list[str], b: Budget) -> None: print("\n" + "=" * 104) print("3b. PROFONDITA' IN CIMA AL BOOK (public/get_order_book depth=5) sulle gambe scelte") diff --git a/scripts/research/r0822_monitor_audit.py b/scripts/research/r0822_monitor_audit.py index a73f1c3..222d0b9 100644 --- a/scripts/research/r0822_monitor_audit.py +++ b/scripts/research/r0822_monitor_audit.py @@ -129,8 +129,10 @@ def read_equity_csv(p: Path, col: int = 1) -> tuple[list[pd.Timestamp], np.ndarr # della giornata e' finita nella serie": in radice di tempo il rapporto di VARIANZA fra le due # serie e' la frazione di giornata misurata (ipotesi: random walk sull'intervallo troncato). # -------------------------------------------------------------------------------------------- -def compare(rec: np.ndarray, rep: np.ndarray) -> dict: +def compare(rec: np.ndarray, rep: np.ndarray, mask: np.ndarray | None = None) -> dict: ok = np.isfinite(rec) & np.isfinite(rep) + if mask is not None: + ok &= mask a, b = rec[ok], rep[ok] n = len(a) if n < 3: @@ -145,18 +147,53 @@ def compare(rec: np.ndarray, rep: np.ndarray) -> dict: sh_rep=float(b.mean() / b.std() * ANN) if b.std() > 0 else float("nan")) -def show(tag: str, c: dict, note: str = "") -> None: +def show(T: pd.DatetimeIndex, rec: np.ndarray, rep: np.ndarray, note: str = "") -> dict: + """Stampa il confronto e ritorna la statistica sulla finestra PULITA (senza l'incidente).""" + c = compare(rec, rep) + fuori = ~((T >= FREEZE[0]) & (T <= FREEZE[1])) + cf = compare(rec, rep, mask=fuori) print(f" barre appaiate {c['n']} identiche entro 1e-6: {c['ident']}/{c['n']} " f"corr {c['corr']:+.4f}") print(f" vol annualizzata REGISTRATA {c['sd_rec']:6.2f}% RICALCOLATA {c['sd_rep']:6.2f}%" f" rapporto di varianza {c['vr']:.4f}") print(f" -> il monitor misura ~{c['minuti']:,.0f} minuti di mercato al giorno (su 1440){note}") + if cf["n"] != c["n"]: + print(f" ESCLUSO l'incidente del feed 09-15/07 ({c['n'] - cf['n']} barre recuperate a " + f"posteriori, quindi CHIUSE):") + print(f" barre {cf['n']} identiche {cf['ident']}/{cf['n']} corr {cf['corr']:+.4f}" + f" vol reg. {cf['sd_rec']:.2f}% vs ric. {cf['sd_rep']:.2f}%" + f" -> ~{cf['minuti']:,.0f} min/giorno") print(f" Sharpe della finestra: REGISTRATO {c['sh_rec']:+.2f} RICALCOLATO {c['sh_rep']:+.2f}" f" [SE ~{np.sqrt(365.0 / max(c['n'], 1)):.2f}: NON e' una lettura di gate]") + return dict(all=c, clean=cf) RES: dict[str, dict] = {} # riepilogo per la tabella finale +# -------------------------------------------------------------------------------------------- +# L'INCIDENTE DEL FEED (2026-07-09 -> 2026-07-15). Non e' un'assunzione: e' documentato nel +# sorgente di produzione (`rebuild_history.py`, commento "incidente 2026-07-15: feed fermo 7g" — +# `shutil.copy2` del backup falliva con EPERM e ABORTIVA il rebuild) ed e' verificabile nel log +# del cron. Conta qui per una ragione precisa: durante il congelamento i monitor NON trovavano +# barre nuove, e al ripristino hanno recuperato in un colpo solo barre gia' CHIUSE — cioe' le +# uniche barre corrette delle loro serie. Vanno escluse da ogni stima del difetto, o si finisce +# per attribuire al difetto un'attenuazione che viene da un guasto diverso. +FREEZE = (pd.Timestamp("2026-07-09", tz="UTC"), pd.Timestamp("2026-07-16", tz="UTC")) +CRONLOG = ROOT / "logs" / "cron_daily.log" + + +def freeze_evidence() -> dict[str, int]: + """Giorni in cui `rebuild_history` e' ABORTITO nel cron (letto dal log, non assunto).""" + out: dict[str, int] = {} + day = None + lines = CRONLOG.read_text(errors="replace").splitlines() if CRONLOG.exists() else [] + for i, ln in enumerate(lines): + if ln.startswith("===== 2026-") and "cron_daily" in ln: + day = ln.split()[1][:10] + if day and "rebuild_history.py" in ln and "Traceback" in "".join(lines[max(0, i - 3):i]): + out[day] = out.get(day, 0) + 1 + return out + # ============================================================================================ # A — PROVENIENZA: quanto e' completa l'ultima riga di ogni sorgente @@ -245,12 +282,14 @@ def b_xsr(): reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS}) px.advance(st0) rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")} - pairs = [(float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep] - c = compare(np.array([p[0] for p in pairs]), np.array([p[1] for p in pairs])) - print(f"\n pannello: `build_panel` -> `load_hl` -> parquet HL 1d (ultima riga = giorno in corso)") - show("xsr", c) - RES["paper_xsr"] = dict(parziale="SI", cmp=c, gate="XSR01 2026-10-23", - src="hl_*_1d.parquet", cad_h=24.0) + pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])]) + for r in rec if int(r["ts"]) in rep] + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True)) + print("\n pannello: `build_panel` -> `load_hl` -> parquet HL 1d (ultima riga = giorno in corso)") + print(f" inception {T[0].date()} = DOPO l'incidente del feed: la finestra e' gia' pulita.") + out = show(T, np.array([p[1] for p in pairs]), np.array([p[2] for p in pairs])) + RES["paper_xsr"] = dict(parziale="SI", cmp=out["clean"], gate="XSR01 2026-10-23", + src="hl_*_1d.parquet", cad_h=24.0, ric="SI (bit-exact)") def b_statarb(): diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index 80875ff..f226d64 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -186,10 +186,41 @@ def _d(s: pd.Series) -> pd.Series: _CRY_CACHE: dict = {} -def _crypto(w_tp: float, w_skh: float) -> pd.DataFrame: - key = (round(w_tp, 6), round(w_skh, 6)) +def _crypto_stressed(w_tp: float, w_skh: float, m_tp: float, m_skh: float) -> pd.DataFrame: + """Come `pc.crypto_daily_tuples` (minimo ESATTO sul path orario condiviso) ma con un taglio + del DRIFT applicato alla singola gamba PRIMA di comporre il path. Serve perche' il minimo + giornaliero non e' separabile per gamba: non si puo' tagliare il drift a valle.""" + (J,) = pc._hourly_legs() + legs = {} + for nm, (rc, wc, mult) in {"tp": ("tp_r", "tp_w", m_tp), "sk": ("skh_r", "skh_w", m_skh)}.items(): + r = J[rc].values.copy() + w = J[wc].values.copy() + if mult != 1.0: + cut = (1.0 - mult) * r.mean() + r = r - cut + w = w - cut + legs[nm] = (r, w) + r_h = w_tp * legs["tp"][0] + w_skh * legs["sk"][0] + w_h = w_tp * legs["tp"][1] + w_skh * legs["sk"][1] + idx = J.index + days = idx.floor("D") + rows = [] + for day, pos in pd.Series(np.arange(len(idx)), index=days).groupby(level=0): + sl = pos.values + r = r_h[sl] + wk = w_h[sl] + cum = np.cumprod(1.0 + r) + cum_prev = np.concatenate(([1.0], cum[:-1])) + m = min(cum.min(), (cum_prev * (1.0 + wk)).min()) - 1.0 + rows.append((day, cum[-1] - 1.0, min(m, cum[-1] - 1.0))) + return pd.DataFrame(rows, columns=["day", "R", "m"]).set_index("day") + + +def _crypto(w_tp: float, w_skh: float, m_tp: float = 1.0, m_skh: float = 1.0) -> pd.DataFrame: + key = (round(w_tp, 6), round(w_skh, 6), round(m_tp, 4), round(m_skh, 4)) if key not in _CRY_CACHE: - _CRY_CACHE[key] = pc.crypto_daily_tuples(w_tp, w_skh) + _CRY_CACHE[key] = (pc.crypto_daily_tuples(w_tp, w_skh) if (m_tp == 1.0 and m_skh == 1.0) + else _crypto_stressed(w_tp, w_skh, m_tp, m_skh)) return _CRY_CACHE[key] @@ -221,11 +252,8 @@ def book_RG(w: dict, start: str = START_WIN, R = np.zeros(len(idx)) M = np.zeros(len(idx)) if w_tp > 0 or w_skh > 0: - c = _crypto(w_tp, w_skh).reindex(idx) - cr = c["R"].values - if w_tp > 0 and dm.get("TP01", 1.0) != 1.0 or w_skh > 0 and dm.get("SKH01", 1.0) != 1.0: - raise NotImplementedError("stress sul drift crypto: il minimo e' path-esatto, non separabile") - R += cr + c = _crypto(w_tp, w_skh, dm.get("TP01", 1.0), dm.get("SKH01", 1.0)).reindex(idx) + R += c["R"].values M += c["m"].values for nm, fr in (("XS01", pc.xsec_daily_tuples()), ("VRP01", vrp_daily_tuples()), ("GTAA01", gtaa_daily_tuples())): @@ -702,6 +730,28 @@ def main() -> None: print(" scoperta; se resta a drift 0, il vantaggio e' pura DECORRELAZIONE e non dipende da") print(" quell'ipotesi. Il numero, non la narrazione, dice quale delle due.") + hr("E-quater. LA DISTORSIONE CHE MI RIGUARDA — l'ottimo si appoggia a SKH01, e SKH01 e' lo") + print(" sleeve con la FORTUNA D'ANCORA piu' grande da restituire. Il LOO del 26/07: l'ancora") + print(" canonica gli regala 2/3 del contributo FULL (+0.43 -> +0.14) e il 70% dell'hold-out;") + print(" l'audit 02/07 lo trova al 93-98° pctl di 23 offset, 'spike, non plateau'. Questa lente") + print(" gira sull'ancora CANONICA e la banda non e' ri-misurabile qui -> il mio ottimo e'") + print(" distorto A FAVORE di SKH01 e va detto PRIMA di leggerlo. Taglio del drift di SKH01:") + print(f"\n {'drift SKH01':>13} {'config':>16} {'leva':>6} {'J':>7} {'J del LIVE':>11} " + f"{'delta appaiato':>16}") + for mult in (1.00, 0.66, 0.50, 0.33): + dmt = (("SKH01", mult),) + rows = [(w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)) + for w in top_w for lv in LEVS] + w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"]) + lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, + drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"]) + dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", + ev_idx, fu_idx) + print(f" {mult:>13.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {oL['J']:>11.3f} " + f"{dd_:>+11.3f} +-{sd_:.3f}") + print(" NB: il taglio si applica anche al LIVE 75/25 (che ha SKH01 al 25%), quindi il delta e'") + print(" un confronto onesto: entrambe le colonne perdono, la domanda e' quale perde di piu'.") + hr("E-ter. IL BUCO CHE LA FINESTRA NON PUO' CHIUDERE — 2024-2026 non contiene un CRASH") print(" L'ottimizzazione toglie peso a TP01 (Sharpe 0.79 su questa finestra). Ma TP01 e' lo") print(" sleeve DIFENSIVO: il LOO del 26/07 misura il suo contributo hold-out NEGATIVO nel 99.1%") diff --git a/scripts/research/r0822_volsize_skeptic.py b/scripts/research/r0822_volsize_skeptic.py index 324327d..2559bfd 100644 --- a/scripts/research/r0822_volsize_skeptic.py +++ b/scripts/research/r0822_volsize_skeptic.py @@ -252,9 +252,10 @@ def main() -> None: FUN_V1["VTL tv20 w90 (LEAD)"] = size_vtl(FROZEN) FUN_V1["BIN L>=1 (0/1)"] = size_bin(FROZEN, "ge1") - NTRIAL = len(FUN_V2) + len(FUN_V1) + 2 # +2 baseline - print(f"\n GRIGLIA DICHIARATA: {len(FUN_V2)} celle su V2-DD + {len(FUN_V1)} su V1 " - f"+ 2 baseline = {NTRIAL} valutazioni x {len(offs)} ancore. Nessuna cella scelta") + NTRIAL = len(FUN_V2) + len(FUN_V1) + 2 + 4 # +2 baseline, +4 controlli 'L combinata' (§1c) + print(f"\n GRIGLIA DICHIARATA (contata al RIALZO): {len(FUN_V2)} celle su V2-DD + " + f"{len(FUN_V1)} su V1 + 4 controlli 'L combinata' (§1c) + 2 baseline = {NTRIAL} " + f"valutazioni x {len(offs)} ancore. Nessuna cella scelta") print(" dopo aver guardato: IV e' la famiglia PRE-REGISTRATA, VTL/w e' l'asse dichiarato") print(" fragile dall'agente, BIN/DROP sono controlli miei dichiarati sopra.")