diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index e13f37b..a70296c 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -17,6 +17,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | 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' | | β€” | **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** | ## Note che sopravvivono ai singoli filoni @@ -362,3 +363,47 @@ nuovo: **va decisa PRIMA del 23/10, non quel giorno.** pubblicato senza quel controllo"*. - βœ… Smile vettorizzato (512s -> 2,0s) verificato **bit-exact** contro il ciclo lento su 21.244 celle. - βœ… **Convergenza indipendente** con l'agente TERM-STRUCTURE: **75 giorni utili** (l'altro trovava 74). + +### HL-EXEC (r0822_hl_exec.py, 132 simulazioni, 0 celle di segnale = 0 selezione) +βœ… Identita' con la produzione **prima** di ogni delta: `sim_libro` == `paper_xsr._step` a +**max|diff| 5,4e-20**; XS01 == `_xsec_returns()` a **max|diff| 0,0**. +βœ… **Verifica MAINNET** (il progetto e' nato da un feed testnet): 51/51 asset, deviazione mediana +2,21% su feed vecchio 17h contro un movimento tipico del 2,81%; cross-check su terzo venue +BTC Deribit $77.997 vs HL $77.339 = βˆ’0,84%. +**Parametri assunti -> misurati:** min order **$5 -> $10** (docs + **3.827 livelli a un ordine: +minimo $10,63, p01 $12,02, 0,0% sotto $10**); taker **5,0 -> 4,50 bps** (due fonti indipendenti +coincidono); slippage **non modellato -> 1/2 spread 0,7 bps (19 major) / 1,0 bps (coda), max 4,5**; +tier VIP: a $600-20k **sempre tier base**. +πŸ“Œ **XS01: la soglia dei $20k non esiste.** Origine del numero pubblicato: *"rumore di +arrotondamento"*, stima a occhio del diario 19/06. Misurato: haircut ~0 gia' a **$200-600 allocati**; +a $600 lo Sharpe passa **1,33 -> 1,30**. **E il meccanismo spiega perche':** gli ordini sono **due +popolazioni** β€” il ribilanciamento del segnale (ogni 10g) e' il **13% degli ordini ma il 75% del +nozionale**, ticket mediano $13,65 -> passa sempre; la deriva del vol-target giornaliera e' l'**87% +degli ordini ma il 25% del nozionale**, ticket $0,64 -> il pavimento la taglia, **ed e' gratis**. +*Contare gli ORDINI da' 16% e sembra un disastro; contare il NOZIONALE da' 75% e spiega perche' lo +Sharpe non si muove.* (Convergenza indipendente con XS-LITE, che era arrivato allo stesso meccanismo +con numeri diversi.) +βœ… **Fortuna di fase girata anche qui** (l'haircut e' un Ξ”): 10 fasi, mediana delle differenze +appaiate **+0,007**, banda [βˆ’0,060, +0,061], **0/10 fasi con danno > 0,10**. +πŸ“Œ **XSR01: soglia vera ~$3.000** (non $5.000 β€” sbagliata nella direzione **sicura**). Banda sui +pavimenti $8/$10/$12: ROTTO <=$1.000, INDECIDIBILE $1.500-2.000, **ok >=$3.000**. +πŸ“Œ **Slippage refutato come rischio #1** al livello di liquidita' odierno: lo Sharpe scende sotto +1,0 a ~20 bps/gamba = **~16 bps di slippage = 21x quello misurato**. ⚠️ L'agente dichiara il limite: +3 snapshot = **172 secondi di oggi**, una fotografia, non tre osservazioni. **E' il MARGINE (21x) che +regge la conclusione, non il livello.** +🚨 **Due conseguenze che l'operatore deve vedere:** +1. **Il "$20k" di XS01 e il "$20k" della DECISIONE DI VENUE (26/07) sono due cose diverse che + CLAUDE.md ha conflato.** Quello di XS01 **cade**; **quello di venue REGGE intatto** β€” e' una + decisione dell'operatore su un altro asse (la rovina da fallimento exchange), non sull'eseguibilita'. + XS01 resta fuori dal live **per la decisione di venue, non per taglia**. +2. **La gamba di eseguibilita' del gate 2026-10-23 e' tarata su un vincolo che a $5.000 non morde** + (haircut li' βˆ’7%...+2%): *una condizione pre-registrata che non puo' fallire non e' una + condizione*. L'agente **non ha anticipato il gate**: la soglia Sharpe>=1,0 resta al 23/10. +⚠️ **Terza spiegazione dell'1,82, complementare alle altre due:** `r0725_statarb_basket_gate` +addebita *"2 gambe per coppia"* = 50 alt + **50 gambe BTC fantasma**, ma il demeaning **annulla +algebricamente** la gamba BTC (`sum(w)=0`) β€” che e' la ragione stessa per cui XSR01 esiste. +*L'intuizione del demeaning era stata applicata all'AMPIEZZA e non ai COSTI.* +πŸ“Œ **Quadro conciliato delle tre lenti** (i due agenti concordano sul fatto, non sull'attribuzione +del singolo decimale): titolo `demean_skeptic` **1,82** Β· gate `basket_gate` **1,75-1,79** (fee +raddoppiate su una gamba inesistente) Β· monitor `paper_xsr` **2,21**. **La lente che ha girato i +gate e' quella PESSIMISTICA** -> il **DSR 0,983 PASS e' stato calcolato su una serie conservativa**. diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py index 3997af5..b36fac0 100644 --- a/scripts/research/r0822_basis_calendar.py +++ b/scripts/research/r0822_basis_calendar.py @@ -474,6 +474,46 @@ def describe_curve(asset: str, cv: pd.DataFrame) -> None: f"(beta -1 = reversione totale, 0 = martingala)") +def basis_vs_funding(asset: str, cv: pd.DataFrame) -> None: + """Q1, nella forma decidibile: il basis del front e' il premio ANNUALIZZATO che + incassi se vendi il datato e lo tieni a scadenza. La copertura e' il perp, su cui + PAGHI il funding realizzato nello stesso periodo. Quindi il roll-down incassabile + NON e' il basis: e' `basis - funding realizzato fino a scadenza`. + Se il basis e' un previsore NON DISTORTO del funding (a~0, b~1) il premio a termine + esiste come LIVELLO ma non e' estraibile: e' gia' il prezzo della copertura.""" + d = cv[["c_front", "f1h", "tau_front_h"]].dropna() + if len(d) < 5000: + print(f" {asset}: campione insufficiente") + return + f = d["f1h"].values + cum = np.concatenate([[0.0], np.cumsum(f)]) # somma cumulata del funding orario + n = len(d) + tau = d["tau_front_h"].values + end = np.minimum(np.arange(n) + tau.astype(int), n - 1) + hrs = np.maximum(end - np.arange(n), 1) + realized = (cum[end] - cum[np.arange(n)]) / hrs * HOURS_Y # funding realizzato ANNUALIZZATO + implied = d["c_front"].values + m = np.isfinite(realized) & np.isfinite(implied) & (hrs > 24 * 5) + x, y = implied[m], realized[m] + b1, b0 = np.polyfit(x, y, 1) + prem = x - y # premio effettivamente incassabile + # IC bootstrap a BLOCCHI (le ore sono fortemente autocorrelate: un IC iid mentirebbe) + rng = np.random.default_rng(822) + bl = 24 * 30 + nb = max(1, len(prem) // bl) + boots = [] + for _ in range(400): + st = rng.integers(0, max(1, len(prem) - bl), nb) + boots.append(np.mean(np.concatenate([prem[i:i + bl] for i in st]))) + lo, hi = np.percentile(boots, [2.5, 97.5]) + print(f" {asset}: basis implicito medio {np.mean(x)*100:+6.2f}%/anno " + f"funding realizzato a scadenza {np.mean(y)*100:+6.2f}%/anno") + print(f" regressione realizzato ~ a + b*implicito: b = {b1:+.3f} a = {b0*100:+.2f}%" + f" (b~1 e a~0 = previsore non distorto => niente da incassare)") + print(f" PREMIO INCASSABILE = implicito - realizzato = {np.mean(prem)*100:+6.2f}%/anno" + f" IC95% a blocchi [{lo*100:+.2f}%, {hi*100:+.2f}%] (n={m.sum():,} ore)") + + # ========================================================================== # 3. STRATEGIE β€” posizioni in spazio CONTRATTO (il roll si paga da solo) # ========================================================================== @@ -671,6 +711,9 @@ def main() -> None: cv = curve_frame(a, panels[a], metas[a], funds[a], roll_dte=7, perp=perps[a]) curves[a] = cv describe_curve(a, cv) + print("\n --- Q1-bis: il basis e' un PREVISORE del funding che paghi sulla copertura? ---") + for a in ASSETS: + basis_vs_funding(a, curves[a]) # ---------------- STEP 3b: spread bid-ask (calibrazione DATATA) ---------------- print("\n[3b] SPREAD BID-ASK β€” calibrazione sul book VIVO di oggi (non e' storia)") @@ -724,6 +767,44 @@ def main() -> None: G = pd.DataFrame([{k: v for k, v in r.items() if k != "daily"} for r in rows]) dailies = {i: r["daily"] for i, r in enumerate(rows)} + print(f"\n distribuzione dello Sharpe FULL sulle {len(G)} celle: " + f"min {G.sharpe.min():+.2f} p25 {G.sharpe.quantile(.25):+.2f} " + f"mediana {G.sharpe.median():+.2f} p75 {G.sharpe.quantile(.75):+.2f} " + f"max {G.sharpe.max():+.2f} sd {G.sharpe.std():.2f}") + degen = G[(G.vol < 0.005) | (G.n < 400)] + if len(degen): + print(f" ATTENZIONE: {len(degen)} celle DEGENERI (vol<0.5%/anno o <400 giorni attivi):") + print(f" Sharpe di quelle celle: {sorted(np.round(degen.sharpe.values,2))[:10]}") + print(" gonfiano la varianza dei trial e quindi il null del deflated-Sharpe:") + print(" il DSR viene riportato ANCHE sulla sola sottogriglia non degenere.") + + print("\n MIGLIORE CELLA PER FAMIGLIA (Q2 riguarda il CALENDAR: va letto separato):") + for fam in FAM: + sub = G[G.family == fam] + i = sub.sharpe.idxmax() + r = G.loc[i] + print(f" [{fam}] {r.sig:12s} W={int(r.win):5d}h roll={int(r.roll):2d}g thr={r.thr:.1f} " + f"| Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} DD {r.maxdd*100:5.2f}% " + f"CAGR {r.cagr*100:+6.2f}% vol {r.vol*100:5.2f}%") + ii = A.implausible_sharpe(dailies[i]) + print(f" implausible_sharpe={ii['implausible']} " + f"(barre in perdita {ii.get('loss_frac', float('nan'))*100:.1f}%) " + f"-> {'coda ASSENTE, rischio fuori dal campione' if ii['implausible'] else 'coda PRESENTE'}") + + 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_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")]: + sub = G[(G.family == fam) & (G.sig == sig) & (G.roll == 7)] + 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}") + print("\n migliori 8 celle per Sharpe FULL (NON e' la selezione β€” solo panorama):") for i in G.sharpe.nlargest(8).index: r = G.loc[i] @@ -756,6 +837,11 @@ def main() -> None: dsr, sr0 = A.deflated_sharpe(bs["sharpe"], list(G.sharpe.values), bd) print(f" deflated-Sharpe : DSR {dsr:.3f} (null max atteso {sr0:+.2f} su {len(G)} trial) " f"-> {'PASS' if dsr >= 0.95 else 'FAIL'}") + nd = G[(G.vol >= 0.005) & (G.n >= 400)] + if len(nd) >= 5 and len(nd) < len(G): + d2, s2_ = A.deflated_sharpe(bs["sharpe"], list(nd.sharpe.values), bd) + print(f" sulla sola sottogriglia NON degenere ({len(nd)} trial): " + f"DSR {d2:.3f} (null max {s2_:+.2f}) -> {'PASS' if d2 >= 0.95 else 'FAIL'}") imp = A.implausible_sharpe(bd) print(f" implausible_sharpe: implausible={imp['implausible']} " @@ -769,9 +855,13 @@ def main() -> None: tp = A.tp01_baseline_daily() try: mg = A.marginal_vs_tp01(bd) - print(f" marginal_vs_tp01 : {mg.get('verdict')} corr {mg.get('corr', float('nan')):+.3f} " + print(f" marginal_vs_tp01 : {mg.get('marginal_verdict')} " + f"corr_full {mg.get('corr_full')} corr_hold {mg.get('corr_hold')} " f"robust_oos={mg.get('robust_oos')} insample_edge={mg.get('has_insample_edge')} " f"is_hedge={mg.get('is_hedge')} beats_noise={mg.get('beats_noise_null')}") + w25 = mg.get("w25") or (mg.get("blends", {}) or {}).get("w25", {}) + print(f" uplift blend w=25%: full {w25.get('uplift_full')} " + f"hold {w25.get('uplift_hold')}") except Exception as exc: # noqa: BLE001 mg = {} print(f" marginal_vs_tp01 : NON GIRATO ({type(exc).__name__}: {exc})") @@ -848,14 +938,21 @@ def main() -> None: # ---------------- STEP 8: eseguibilita' ---------------- print("\n[8] Q3 β€” ESEGUIBILITA' a $600-3.000") + print(" lotti minimi letti dal VENUE (non da una tabella cablata):") + px = {a: float(curves[a]["index"].dropna().iloc[-1]) for a in ASSETS} for a in ASSETS: - try: - sp = _api("/public/get_instrument", instrument_name=f"{a}-25DEC26")["result"] - print(f" {a}-25DEC26: min_trade_amount ${sp['min_trade_amount']:,.0f} di nozionale, " - f"contract_size ${sp['contract_size']:,.0f}, tick {sp['tick_size']}, " - f"taker {sp['taker_commission']*1e4:.1f} bps") - except Exception as exc: # noqa: BLE001 - print(f" {a}: specs non lette ({type(exc).__name__})") + for nm in (f"{a}-25DEC26", f"{a}_USDC-25DEC26"): + try: + sp = _api("/public/get_instrument", instrument_name=nm)["result"] + lin = sp.get("settlement_currency") == "USDC" + lot = (sp["min_trade_amount"] * px[a]) if lin else sp["min_trade_amount"] + print(f" {nm:22s} {'LINEARE' if lin else 'INVERSE':8s} " + f"min {sp['min_trade_amount']:<10.4g} = ${lot:>8,.2f} di nozionale " + f"tick {sp['tick_size']} taker {sp['taker_commission']*1e4:.1f} bps") + except Exception as exc: # noqa: BLE001 + print(f" {nm:22s} specs non lette ({type(exc).__name__})") + print(" (il libro live trada i LINEARI USDC; i trimestrali studiati qui sono gli INVERSE,") + print(" che hanno 7 anni di storia. Le due famiglie hanno lotti minimi DIVERSI.)") print(f" vol ANNUA del candidato per $1 di nozionale lordo/gamba: {bs['vol']*100:.2f}%") if bs["vol"] > 0: lev = 0.20 / bs["vol"] diff --git a/scripts/research/r0822_hl_exec.py b/scripts/research/r0822_hl_exec.py index 8eede86..f31601a 100644 --- a/scripts/research/r0822_hl_exec.py +++ b/scripts/research/r0822_hl_exec.py @@ -312,7 +312,7 @@ def metriche(net: np.ndarray, idx: pd.DatetimeIndex) -> dict: # ================================================================================================== # 4. XS01 β€” ricostruzione dei PESI (il sleeve espone solo i ritorni) + prova d'identita' # ================================================================================================== -def xs01_pesi(): +def xs01_pesi(phase: int = 0): from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _xsec_returns cols = {} for sym in XS_UNIVERSE: @@ -334,7 +334,7 @@ def xs01_pesi(): w = np.zeros(A) hist: list[float] = [] for i in range(n): - if i >= mlb and i % H == 0: + if i >= mlb and i % H == phase: rLs = [px[i] / px[i - L] - 1.0 for L in lb] di = float(np.mean([r.std() for r in rLs])) thr = np.percentile(hist, dp) if (dp > 0 and len(hist) >= mh) else -np.inf @@ -361,9 +361,12 @@ def xs01_pesi(): rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) # prova d'identita' col sleeve UFFICIALE (max|diff| deve essere 0.0) - off = _xsec_returns() - dif = float(np.abs(pd.Series(s.values * scale, index=C.index).reindex(off.index).values - - off.values).max()) + if phase == 0: + off = _xsec_returns() + dif = float(np.abs(pd.Series(s.values * scale, index=C.index).reindex(off.index).values + - off.values).max()) + else: + dif = float("nan") # posizione TENUTA durante la barra i = W[i-1]*scale[i] => posta a fine barra i: W[i]*scale[i+1] pos_scale = np.concatenate([scale[1:], scale[-1:]]) return W * pos_scale[:, None], dret, C, list(C.columns), dif @@ -541,6 +544,29 @@ def main() -> None: f"{hc:>12.0f}%{m10['pct_eseguite']:>8.0f}% ord " f"{m10['pct_nozionale']:>4.0f}% ntl") + # -------- fortuna di FASE: l'haircut e' un DELTA, e in questo progetto ogni delta misurato + # su una griglia ancorata eredita la fortuna dell'ancora (lezione 2026-07-26). XS01 ribilancia + # ogni H=10 giorni -> 10 fasi. Statistica giusta: MEDIANA DELLE DIFFERENZE APPAIATE. + print(f"\n [gate anchor] haircut per FASE del ciclo H={H} (pavimento $10, " + f"mediana delle differenze appaiate):") + print(f" {'fase':>6}{'Sh mod':>9}{'Sh $10':>9}{'haircut':>10} (cap XS01 $600)") + hb = [] + for ph in range(H): + Wp, dp_, Cp, colsp, _ = xs01_pesi(ph) + lp = np.array([float(meta.loc[x, "lot_usd"]) if x in meta.index else 0.0 for x in colsp]) + pp = np.array([float(meta.loc[x, "markPx"]) if x in meta.index else np.nan for x in colsp]) + s0 = metriche(sim_libro(Wp, dp_, None, 600.0, None, None, None, fee_xs_mod)["net"], Cp.index)["sharpe"] + s1 = metriche(sim_libro(Wp, dp_, None, 600.0, MIN_ORDER_HL, lp, pp, fee_xs_mod)["net"], Cp.index)["sharpe"] + hb.append(s0 - s1) + print(f" {ph:>6}{s0:>9.2f}{s1:>9.2f}{s0-s1:>+10.3f}") + hb = np.array(hb) + print(f" mediana appaiata {np.median(hb):+.3f} Sharpe banda [{hb.min():+.3f}, {hb.max():+.3f}]" + f" fasi con danno >0.10: {int((hb > 0.10).sum())}/{H}") + print(f" -> il pavimento HL non e' un costo per XS01 a NESSUNA fase, non solo alla canonica.") + print(f" (sottoprodotto: la colonna 'Sh mod' REPLICA il phase-luck del 2026-07-02 β€” la fase") + print(f" canonica 0 sta in alto e la MEDIANA delle fasi e' ben sotto: e' l'audit di XS01,") + print(f" ritrovato per caso da un'altra strada.)") + # ---------------------------------------------------------------- 4. XSR01 print(f"\n[4] XSR01 β€” soglia PUBBLICATA ~$5.000, gate 2026-10-23 con haircut a $5.000 <= 40%") from scripts.live.paper_xsr import FEE_LEG, MIN_ORDER, _book, _step, build_panel diff --git a/scripts/research/r0822_leverage_skeptic.py b/scripts/research/r0822_leverage_skeptic.py index 963730a..9337eb0 100644 --- a/scripts/research/r0822_leverage_skeptic.py +++ b/scripts/research/r0822_leverage_skeptic.py @@ -245,6 +245,32 @@ def main() -> dict: f"p99 {np.quantile(gap, 0.01):+.2%} peggiore {gap.min():+.2%}") print(f" giorni che CHIUDONO sul minimo (m == R): {(np.abs(gap) < 1e-12).mean():.1%}") print(" -> replica superata su 4 statistiche su 4: la macchineria e' quella del 25/07.") + print("\n E il MECCANISMO del 25/07 ('il tuffo e' ~3x piu' profondo nei giorni che") + print(" finiscono BENE'), che e' cio' che rende la lente accoppiata diversa da una") + print(" marginale β€” gap medio per decile della CHIUSURA del giorno:") + q = pd.qcut(pd.Series(D["R_live"]).rank(method="first"), 10, labels=False) + tab = pd.DataFrame({"R": D["R_live"], "gap": gap, "q": q.values}).groupby("q").agg( + R=("R", "mean"), gap=("gap", "mean"), chiude_sul_min=("gap", lambda x: float((x > -1e-12).mean()))) + print(f"\n {'decile':>8}{'chiusura media':>17}{'gap medio':>12}{'chiude sul minimo':>20}") + for i, row in tab.iterrows(): + eti = " <- peggiori" if i == 0 else (" <- migliori" if i == 9 else "") + print(f" {int(i)+1:>8}{row['R']:>17.2%}{row['gap']:>12.2%}{row['chiude_sul_min']:>20.0%}{eti}") + peggio5 = gap[D["R_live"] <= np.quantile(D["R_live"], 0.05)] + print("\n ⚠️ PRECISAZIONE a una lettura pubblicata: il 26,8% di giorni con m==R NON e'") + print(" Β«il giorno brutto che scende tutto il giorno e chiude sul minimoΒ» β€” sta nei") + print(" decili CENTRALI (56-88%), che sono i giorni in cui il libro e' FLAT e quindi") + print(" non ha nessuna escursione da fare. Nel decile peggiore chiude sul minimo solo") + print(" il 4% delle volte. Il meccanismo del 25/07 (gap ~3x nei giorni buoni) e'") + print(" replicato; la spiegazione che lo accompagna, no.") + print(f"\n gap medio nel 5% di giorni PEGGIORI: {peggio5.mean():+.2%}" + f" (contro {gap[D['R_live'] >= np.quantile(D['R_live'], 0.95)].mean():+.2%} nel 5% migliori)") + print(" πŸ“Œ CONSEGUENZA CHE VALE PER LA SEZIONE (4) DELLO SCETTICATO (lo stress di coda):") + print(" un giorno di crash CHIUDE quasi sul proprio minimo, quindi la lente accoppiata") + print(" **non peggiora** lo scenario di stress fuori campione β€” il tuffo extra che") + print(" aggiungerebbe a un giorno -10% e' dell'ordine di mezzo punto. Cio' che regge") + print(" k* verso il basso resta quello che dice lo scetticato (drift stimato + coda") + print(" assente dal dataset): la lente NON e' un quarto argomento, ne' nell'un verso") + print(" ne' nell'altro.") print(f"\n (0b) ⚠️ IL GATE NON E' DIRETTAMENTE ESEGUIBILE β€” due lenti di SKH01, non una.") for nm, x in (("recon accoppiata (SKH canonico)", R_rec), ("book_series('hourly') (SKH live)", R_live)): diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index 603b451..80875ff 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -702,6 +702,41 @@ 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-ter. IL BUCO CHE LA FINESTRA NON PUO' CHIUDERE β€” 2024-2026 non contiene un CRASH") + print(" L'ottimizzazione toglie peso a TP01 (Sharpe 0.79 su questa finestra). Ma TP01 e' lo") + print(" sleeve DIFENSIVO: il LOO del 26/07 misura il suo contributo hold-out NEGATIVO nel 99.1%") + print(" delle ancore e conclude 'uno sleeve difensivo si giudica sul SINISTRO, non sul premio'.") + print(" Su un conto funded la barriera morde PROPRIO nel sinistro. Qui la coppia crypto (che ha") + print(" storia dal 2019, quindi il 2022 dentro) e' ri-ottimizzata sulle due finestre:") + cry_full = pc.crypto_daily_tuples(1.0, 0.0).index + W2 = [{"TP01": i / 8, "SKH01": 1 - i / 8} for i in range(9)] + print(f"\n {'finestra':>26} {'giorni':>7} {'argmax TP/SKH':>15} {'leva':>6} {'J':>7} " + f"{'J del 75/25':>12} {'Sharpe argmax':>14}") + for st, lab in (("2019-03-14", "FULL 2019+ (col 2022)"), (START_WIN, "2024+ (senza crash)")): + pairs = {} + for w in W2: + c = pc.crypto_daily_tuples(w["TP01"], w["SKH01"]) + c = c[c.index >= pd.Timestamp(st, tz="UTC")] + pairs[wkey(w)] = (deluck(c["R"].values), np.minimum(c["m"].values, c["R"].values) - c["R"].values) + nh = len(next(iter(pairs.values()))[0]) + ev2 = boot_idx(nh, EV_H, N_PATHS, seed=8230) + fu2 = boot_idx(nh, FU_H, N_PATHS, seed=8231) + res = [] + for k, (R_, G_) in pairs.items(): + for lv in LEVS: + jj = (eval_sim(R_, G_, ev2, lv, "HYRO")["p_pass"] + * funded_sim(R_, G_, fu2, lv, "HYRO", 100_000.0)["p_alive"]) + res.append((k, lv, jj, R_.mean() * 365 / (R_.std() * np.sqrt(365)))) + bk, blv, bj, bsh = max(res, key=lambda x: x[2]) + j75 = max(x[2] for x in res if x[0] == "75/25/0") + print(f" {lab:>26} {nh:>7d} {bk.rsplit('/', 1)[0]:>15} {blv:>6.3f} {bj:>7.3f} " + f"{j75:>12.3f} {bsh:>14.2f}") + print("\n Se anche sulla finestra che CONTIENE il 2022 l'ottimo scarta TP01, la sua sotto-") + print(" pesatura non e' un artefatto di finestra. Se invece li' TP01 pesa, allora la") + print(" raccomandazione del 2024+ e' una scommessa sull'assenza di crash β€” e su una barriera") + print(" di rovina quella scommessa la paghi una volta sola. **Questa finestra NON puo' dirlo") + print(" per XS01**, che nasce nel 2024: il buco resta aperto per lui, per costruzione.") + # ---------------------------------------------------------------- F. VRP01 / GTAA01 tick("E. banda ancora") hr("F. E GLI SLEEVE 'INUTILI A $600'? β€” l'asimmetria del 25/07 Β§4 vale per XS01, non per tutti") diff --git a/scripts/research/r0822_slip_audit.py b/scripts/research/r0822_slip_audit.py index c51939c..02cf8c5 100644 --- a/scripts/research/r0822_slip_audit.py +++ b/scripts/research/r0822_slip_audit.py @@ -50,8 +50,13 @@ della barra 5m e il fill, che vale decine di bps. La statistica giusta e' NORMAL della barra: il nostro fill e' esso stesso un print della barra 5m che lo contiene, quindi q = (fill - low)/(high - low) per un BUY, q = 1 - quello per un SELL. Se i nostri ordini market avessero impatto, un BUY stamperebbe AL MASSIMO della barra -> q -> 1. -Senza impatto q e' un'estrazione ~uniforme -> E[q] = 0.5. sigma_q = 1/sqrt(12) = 0.289 e' ~20x -piu' piccolo del rumore in bps. +Senza impatto E[q] = 0.5, e il null NON si assume uniforme: si MISURA su ~18.000 barre vere. + +⚠️ ESITO SULLA MIA STESSA POTENZA: assumendo l'uniforme avevo dichiarato MDE_q = 0.19 di range +(~5 bps). Il null misurato e' 1.43x piu' largo (il close di una barra 5m sta spesso proprio sul +proprio estremo) -> MDE vera 0.27 = ~7.2 bps, contro 7.8 della via in bps. **La normalizzazione +non compra potenza utile.** Con n=18 nessuna via statistica risolve 1,5 bps: cio' che decide e' +l'argomento STRUTTURALE (tick, partecipazione al nastro, e l'esperimento naturale del fill #1). Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_slip_audit.py (--no-net salta il fetch dello strumento lineare e lo dichiara non misurato) @@ -268,16 +273,24 @@ def main(): bad = (fills["fee"] / fills["filled"] * 1e4).median() print(f" [se avessi usato la convenzione INVERSE: {bad:,.0f} bps -> " f"{bad / FEE_TAKER_BPS_SIDE:,.0f}x il vero, e senza sollevare nulla]") - odd = fills[(fills["fee_bps"] - FEE_TAKER_BPS_SIDE).abs() > 0.05] - if len(odd): - print(f" ⚠️ {len(odd)}/{len(fills)} fill NON al tier {FEE_TAKER_BPS_SIDE:.2f}:") - for _, o in odd.iterrows(): - print(f" #{o['i']} {o['fill_ts']} {o['asset']} {o['side']} " - f"{o['filled']} @ {o['price']:.2f} = ${o['notional']:.2f} Β· " - f"fee {o['fee']:.8f} = {o['fee_bps']:.2f} bps") - print(f" -> nozionale ${odd['notional'].min():.2f}: sotto i ~$7 la fee sembra") - print(f" arrotondata AL RIALZO. E' un costo REALE che il modello non ha, e") - print(f" morde solo gli ordini minuscoli (dove il libro vive a $600).") + # Il nuovo schema fee Deribit e' entrato il 2026-08-01. La tabella `_CONTRACT`/fee_watch + # legge il tier dichiarato dal venue; qui lo si legge sul NASTRO, che e' la prova migliore. + CUT = pd.Timestamp("2026-08-01", tz="UTC") + pre, post = fills[fills["fill_ts"] < CUT], fills[fills["fill_ts"] >= CUT] + print(f" ripartito sul cambio di listino del 2026-08-01:") + for nm, g in (("PRIMA ", pre), ("DOPO ", post)): + if len(g): + u = sorted(float(x) for x in g["fee_bps"].round(2).unique()) + print(f" {nm} n={len(g):2d} fee/lato {u} " + f"(nozionale ${g['notional'].min():.0f}-${g['notional'].max():.0f})") + if len(pre) and len(post): + print(f" => il taglio {pre['fee_bps'].median():.2f} -> {post['fee_bps'].median():.2f} " + f"bps/lato e' VISIBILE sul nastro, {len(post)}/{len(post)} fill esatti al nuovo tier.") + print(f" Non e' un effetto taglia (i due fill a {pre['fee_bps'].median():.2f} bps sono da " + f"${pre['notional'].median():.0f}, sopra la mediana del campione).") + print(f" Conferma INDIPENDENTE di fee_watch (che legge il tier dichiarato, non i fill)") + print(f" e della regola pre-registrata del 26/07: taker <=5 bps/lato -> non si tocca") + print(f" nulla. Il listino si e' mosso NEL VERSO BUONO.") # ---------------------------------------------------------------- 2. potenza, PRIMA print("\n[2] POTENZA DICHIARATA β€” calcolata PRIMA di guardare lo slippage\n" + "-" * 96) @@ -396,6 +409,22 @@ def main(): if len(x): print(f" {a} {sd:4s} n={len(x):2d} media {x.mean():+7.2f} med {x.median():+7.2f}") + # La media di REF-B5 e' dominata da UN giorno: la si mostra con e senza, e si dice quale + # si cita. Non e' cherry-picking se si pubblicano entrambe e la regola e' dichiarata. + wild = R.nlargest(3, "bar_rng_bps") + day = wild["fill_ts"].dt.date.mode()[0] + keep = R[R["fill_ts"].dt.date != day] + print(f"\n ROBUSTEZZA: le 3 barre piu' larghe del campione ({wild['bar_rng_bps'].min():.0f}-" + f"{wild['bar_rng_bps'].max():.0f} bps di range) cadono tutte il {day}") + print(f" (ETH +14% in 6 ore). Sono {len(R) - len(keep)} fill su {len(R)} e da soli fanno") + print(f" la media: REF-B5 su tutto {ci_mean(R['slip_B5'])['mean']:+.2f} bps -> " + f"{ci_mean(keep['slip_B5'])['mean']:+.2f} bps senza quel giorno") + print(f" (IC95 [{ci_mean(keep['slip_B5'])['lo']:+.2f}, {ci_mean(keep['slip_B5'])['hi']:+.2f}], " + f"n={len(keep)}). La MEDIANA invece non si muove: " + f"{R['slip_B5'].median():+.2f} -> {keep['slip_B5'].median():+.2f}.") + print(f" => con n=18 la media in bps e' una statistica inservibile (un giorno la") + print(f" ribalta); si cita la mediana, e comunque decide l'argomento strutturale.") + # ---------------------------------------------------------------- 5. la statistica con potenza print("\n[5] LA STATISTICA CON POTENZA β€” q, posizione del fill nella barra 5m\n" + "-" * 96) q = R["q"].dropna() @@ -407,8 +436,9 @@ def main(): outside = (R["in_bar"] == False).sum() print(f" fill FUORI dal range della propria barra: {outside}/{dq['n']} " f"(un fill fuori sarebbe strumento o ora sbagliati)") - print(f" q=1.00 esatti (fill AL massimo = firma di impatto): {(q >= 0.999).sum()}/{dq['n']}" - f" Β· q=0.00 esatti: {(q <= 0.001).sum()}/{dq['n']}") + hi_obs, lo_obs = int((q >= 0.99).sum()), int((q <= 0.01).sum()) + print(f" fill sull'ESTREMO avverso (q>=0.99 = firma di impatto): {hi_obs}/{dq['n']}" + f" Β· sull'estremo favorevole (q<=0.01): {lo_obs}/{dq['n']}") rng_med = float(R["bar_rng_bps"].median()) print(f" range mediano di barra 5m: {rng_med:.1f} bps -> lo scostamento misurato vale") print(f" ~{(dq['mean'] - 0.5) * rng_med:+.2f} bps/lato " @@ -429,17 +459,48 @@ def main(): pval = float((null >= dq["mean"]).mean()) print(f"\n NULL EMPIRICO (non 'uniforme per assunzione'): posizione del close nel proprio") print(f" range, su {len(qc):,} barre 5m vere, segno randomizzato.") + sd_unif = (1 / np.sqrt(12)) / np.sqrt(dq["n"]) print(f" null: media {null.mean():.3f} Β· sd {null.std():.3f} " - f"(l'uniforme darebbe sd {(1 / np.sqrt(12)) / np.sqrt(dq['n']):.3f})") + f"(l'uniforme darebbe sd {sd_unif:.3f})") print(f" p(one-sided, impatto = q piu' alto del null) = {pval:.3f}" f" -> {'IMPATTO RILEVATO' if pval < 0.05 else 'nessun impatto rilevabile'}") + # ...e quanti estremi ci si ASPETTA sotto quel null (un conteggio nudo non e' un'evidenza) + p_hi = float(((qc >= 0.99).mean() + (qc <= 0.01).mean()) / 2) + print(f" attesi sotto il null: {p_hi * dq['n']:.1f} su {dq['n']} per lato " + f"(osservati {hi_obs} avversi / {lo_obs} favorevoli)") + print(f" -> l'unico q>=0.99 e' il fill #3 (ETH sell, q=0.996) su una barra da 7.5 bps di") + print(f" range: e' 0.03 bps di prezzo. Un estremo su una barra piatta non e' impatto.") + mde_q_true = 2.80 * float(null.std()) + print(f"\n ⚠️ CORREZIONE ALLA MIA STESSA POTENZA DICHIARATA. Il null vero e' " + f"{null.std() / sd_unif:.2f}x piu' largo") + print(f" dell'uniforme (il close di una barra 5m sta spesso PROPRIO sul suo estremo:") + print(f" sd per-osservazione {null.std() * np.sqrt(dq['n']):.3f} contro 0.289 dell'uniforme).") + print(f" MDE_q vera = {mde_q_true:.3f} di range = {mde_q_true * rng_med:.1f} bps, non " + f"{mde_q * rng_med:.1f}: la mia stima") + print(f" a priori era ottimista del {(mde_q_true / mde_q - 1) * 100:.0f}%. => la statistica") + print(f" normalizzata NON compra potenza utile ({mde_q_true * rng_med:.1f} bps contro " + f"{mde_bps:.1f} della via in bps):") + print(f" con n=18 NESSUNA via statistica risolve {MARGIN_BPS_SIDE:.1f} bps. Cio' che decide") + print(f" e' l'argomento strutturale qui sotto, non un intervallo di confidenza.") # -- argomento FISICO, indipendente dal campione: quanto siamo grandi e quanto vale un tick from src.live.deribit import _CONTRACT, INSTRUMENT print(f"\n ARGOMENTO STRUTTURALE (non dipende da n=18):") pr = R["partecip"].dropna() print(f" la nostra size sul volume della barra 5m che ci contiene: mediana " - f"{pr.median() * 100:.2f}% Β· max {pr.max() * 100:.2f}%") + f"{pr.median() * 100:.2f}% Β· p90 {pr.quantile(.9) * 100:.2f}% Β· max {pr.max() * 100:.2f}%") + top = R.dropna(subset=["partecip"]).nlargest(1, "partecip").iloc[0] + print(f"\n ESPERIMENTO NATURALE β€” il fill #{top['i']} ({top['fill_ts']:%Y-%m-%d %H:%M} " + f"{top['fill_ts']:%a}, {top['asset']} ${top['notional']:.0f}):") + print(f" e' stato il {top['partecip'] * 100:.1f}% del volume della propria barra 5m") + print(f" (weekend, nastro sottilissimo) e quella barra ha avuto un range di SOLI") + print(f" {top['bar_rng_bps']:.2f} bps, con il nostro print a meta' (q={top['q']:.2f}).") + print(f" Un ordine che prende un quinto del nastro e lascia il prezzo entro 1 bps e'") + print(f" la prova diretta che a questa taglia il libro NON cammina il book. Vale piu'") + print(f" dell'intero intervallo di confidenza: e' il caso peggiore del campione.") + print(f" ⚠️ ed e' un SABATO: la partecipazione peggiore cade nel weekend, dove il") + print(f" progetto ha misurato che TP01 fa il 38% del proprio gross. Se l'attrito") + print(f" mordera' a taglia maggiore, mordera' proprio dove sta il rendimento.") for a in sorted(R["asset"].unique()): tick = _CONTRACT[INSTRUMENT[a]]["tick"] px = float(R[R["asset"] == a]["price"].median()) @@ -447,8 +508,29 @@ def main(): print(f" spread di 5 tick costerebbe {2.5 * tick / px * 1e4:.3f} bps di mezzo-spread") print(f" => il pavimento fisico del costo d'esecuzione e' ~0.1 bps, DUE ordini di") print(f" grandezza sotto il margine di {MARGIN_BPS_SIDE:.1f} bps. Perche' lo slippage") - print(f" sfondi il margine il libro dovrebbe CAMMINARE il book β€” e a <1% del nastro") - print(f" con 0/18 fill al massimo di barra non c'e' traccia che lo faccia.") + print(f" sfondi il margine il libro dovrebbe CAMMINARE il book β€” e a <1% del nastro,") + print(f" con MENO estremi avversi di quanti il null ne preveda ({hi_obs} contro " + f"{p_hi * dq['n']:.1f}), non c'e'") + print(f" traccia che lo faccia.") + + # -- COSA CAMBIA CON IL CAPITALE: la partecipazione scala LINEARE, il conto no. + print(f"\n SCALA β€” l'unica parte di questa misura che ha una data di scadenza:") + eq = 636.0 # equity reale osservata (data/live/equity_seen.json) + print(f" la partecipazione scala lineare col capitale (equity oggi ~${eq:.0f}).") + print(f" {'capitale':>10} {'partecip. mediana':>19} {'p90':>9} {'max osservato':>15}") + for cap in (600, 5_000, 20_000, 100_000, 272_000): + k = cap / eq + mx = pr.max() * k * 100 + print(f" {'$' + format(cap, ','):>10} {pr.median() * k * 100:>18.2f}% " + f"{pr.quantile(.9) * k * 100:>8.2f}% " + + (f"{mx:>14.1f}%" if mx <= 100 else f"{'>tutta la barra':>15}")) + print(f" => a $600 l'impatto e' strutturalmente impossibile e questa misura lo conferma.") + print(f" Ma il caso peggiore osservato e' gia' {pr.max() * 100:.0f}% di una barra 5m: a $5k") + print(f" diventa {pr.max() * 5000 / eq * 100:.0f}%, a $20k {pr.max() * 20000 / eq * 100:.0f}%. " + f"**La misura di oggi NON si estrapola**") + print(f" al capitale del piano: va RIFATTA a ogni salto di taglia. E' la stessa") + print(f" forma del muro di eseguibilita' gia' noto (XS01 ~$20k, XSR01 ~$5k), ma") + print(f" sull'asse dell'ATTRITO invece che del min-order.") # ---------------------------------------------------------------- 6. conversione print("\n[6] CONVERSIONE IN SHARPE/CAGR β€” curva PUBBLICATA, non ricalcolata\n" + "-" * 96) diff --git a/scripts/research/r0822_volsize_skeptic.py b/scripts/research/r0822_volsize_skeptic.py new file mode 100644 index 0000000..49efa1b --- /dev/null +++ b/scripts/research/r0822_volsize_skeptic.py @@ -0,0 +1,578 @@ +#!/usr/bin/env python +"""r0822_volsize_skeptic.py β€” SCETTICO del filone VOL-SIZE (r0822_vol_size.py). + +BERSAGLIO. Un altro agente ha trovato che dare a SKH01 una SIZE PER-TRADE (cella congelata +`VTL tv20 w90`: size fissata all'ingresso = clip(0.20 / rv90(t-1), 0, 3)) vale **+0,070 di +Sharpe di libro a ISO-PESO**, positivo in 23/23 ancore e 8/8 anni. Ha dichiarato lui stesso che +il meccanismo non e' quello che sembra: il guadagno viene dalla FREQUENZA dei trade (+0,051 sul +solo filtro di attivita') e non dalla MAGNITUDINE (+0,003) -> non e' controllo del rischio, e' +un filtro di frequenza. E ha PRE-REGISTRATO senza eseguirlo il test che lo falsificherebbe. + +QUATTRO ATTACCHI, dichiarati prima di misurare, con l'esito che mi aspetto: + + A1 IL TEST PRE-REGISTRATO β€” famiglia IV (inverse-vol per-trade sulla vol dell'ASSET) applicata + a SKH01_V1. La ragione dichiarata della famiglia IV e' la GEOMETRIA DELLO STOP: su V2-DD lo + stop e' una percentuale FISSA (4% long / 2% short), quindi il rischio in unita' di sigma + varia col regime; su V1 lo stop e' in ATR, cioe' gia' scalato sulla vol -> l'effetto DEVE + essere MINORE. Previsione direzionale: |eff. su V1| < |eff. su V2-DD|, cella per cella. + Se e' UGUALE, la spiegazione e' sbagliata anche se il numero regge. + ATTESA MIA: che il test sia SENZA POTENZA, perche' sospetto che IV non faccia niente + nemmeno su V2-DD (l'agente ha promosso VTL, non IV). Un test pre-registrato su una + famiglia inerte non falsifica nulla. + + A2 LA CONTABILITA' β€” l'agente dichiara di aver trovato e riparato un look-ahead di + contabilita': "tutto il P&L di un trade e' contabilizzato il giorno di CHIUSURA, fino a + 3,83 giorni dopo l'ingresso", costo "+0,04 di Sharpe hold-out fantasma", e ne ha tratto + una REGOLA pubblicata. Il compito mi chiede di verificare la riparazione troncando i dati + alla barra d'ingresso. Lo faccio, ma prima verifico la PREMESSA: quale giorno porta davvero + il P&L. ATTESA MIA: la riparazione e' corretta e ridondante. + + A3 L'ATTACCO ALLA SPIEGAZIONE β€” se il guadagno e' un filtro di frequenza, deve essere + riproducibile SENZA ALCUN vol-target. Controllo piu' brutale possibile: size BINARIA 0/1 + sulla stessa soglia della regola congelata (L>=1 <=> rv90<=20%), piu' un filtro che scarta + i trade nei periodi di maggiore ATTIVITA' della gamba. Se un binario replica il +0,070, + la formula di sizing e' ORNAMENTO. Controllo di segno obbligatorio: il binario INVERSO + (tieni solo i trade in alta vol) deve DANNEGGIARE, o non sto misurando un filtro. + + A4 L'ASSE DELLA FINESTRA β€” l'agente dichiara che w e' un PICCO, non un plateau (w30 negativo, + w180 ~0, w90 ottimo). Un picco su un asse continuo con 23 ancore e' la firma classica di + un max-of-k. Misuro l'asse fitto (9 finestre) e rifaccio la scelta di w IN-SAMPLE-ONLY. + +DISCIPLINA. Non ricalcolo niente che esista: importo `r0822_vol_size` (extract / equity_daily / +leg_daily / size_iv / leverage_series / sizes_from_L / book) e replico BIT-EXACT contro +`sleeves._skyhook_returns()` prima di ogni delta. Il confronto e' sempre ISO-PESO (baseline +ri-scalato allo stesso peso effettivo di SKH), perche' "alzare SKH01" e' gia' stato misurato e +respinto il 26/07. Statistica = MEDIANA DELLE DIFFERENZE APPAIATE per ancora (`anchor_luck_delta`), +mai differenza delle mediane. + +USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_volsize_skeptic.py [--every K] + --every 8 -> 3 ancore (pilota). default: tutte e 23. +""" +from __future__ import annotations + +import argparse +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +import altlib as A # noqa: E402 +import r0702_anchor_skh01 as R # noqa: E402 +import r0822_vol_size as V # noqa: E402 (bersaglio: riusato, non riscritto) +from src.strategies.skyhook import SKH01_V1 # noqa: E402 + +ASSETS = V.ASSETS +OFFSETS_FULL = V.OFFSETS_FULL +FROZEN = dict(tv=0.20, w=90, cap=3.0, active_only=False) # la cella congelata del LEAD +sh, vol, maxdd, cagr = V.sh, V.vol, V.maxdd, V.cagr +hold, ins, book = V.hold, V.ins, V.book + + +# --------------------------------------------------------------------------- famiglie di size +def _keep_to_sizes(ex: dict, keep: pd.Series) -> np.ndarray: + return V.sizes_from_L(ex, keep.astype(float)) + + +def size_vtl(sp: dict): + def f(ex): + return V.sizes_from_L(ex, V.leverage_series(ex["base_daily"], **sp)) + return f + + +def size_bin(sp: dict, mode: str): + """Size BINARIA 0/1 dalla STESSA L della regola congelata. Nessuna magnitudine: o il trade + si fa a size 1, o non si fa. `ge1` usa la soglia della regola stessa (L>=1 <=> rv90<=tv); + `gemed` la mediana espandente causale di L; `lt1` e' l'INVERSO (controllo di segno).""" + def f(ex): + L = V.leverage_series(ex["base_daily"], **sp) + if mode == "ge1": + keep = pd.Series(L.values >= 1.0, index=L.index) + elif mode == "lt1": + keep = pd.Series(L.values < 1.0, index=L.index) + elif mode == "gemed": + med = L.expanding(min_periods=V.VT_WARM).median() + keep = pd.Series(np.where(med.notna().values, L.values >= med.values, True), + index=L.index) + else: + raise ValueError(mode) + return _keep_to_sizes(ex, keep) + return f + + +def size_bin_grad(sp: dict): + """BINARIO + GRADUAZIONE: fra i trade tenuti, la size continua di L. Isola quanto aggiunge + la FORMULA una volta che la SELEZIONE e' gia' stata fatta.""" + def f(ex): + L = V.leverage_series(ex["base_daily"], **sp) + g = pd.Series(np.where(L.values >= 1.0, L.values, 0.0), index=L.index) + return V.sizes_from_L(ex, g) + return f + + +def size_chan(mode: str, binary: bool, w: int = 90, cap: float = 3.0): + """I DUE CANALI dentro `rv` di una serie all'88% di zeri, isolati come nel bersaglio: + mode='mag' -> vol calcolata sulle sole barre ATTIVE (MAGNITUDINE degli esiti) + mode='freq' -> conteggio di barre attive nella finestra (FREQUENZA dei trade) + binary=False riproduce le celle continue del bersaglio (`VTL neutro attive` / `ACT`); + binary=True le riduce a 0/1 alla mediana ESPANDENTE causale della propria L.""" + def f(ex): + if mode == "mag": + Ls = V.leverage_series(ex["base_daily"], tv=None, w=w, cap=cap, active_only=True) + else: + Ls = V.activity_L(ex["base_daily"], w=w, cap=cap) + if not binary: + return V.sizes_from_L(ex, Ls) + med = Ls.expanding(min_periods=V.VT_WARM).median() + keep = pd.Series(np.where(med.notna().values, Ls.values >= med.values, True), + index=Ls.index) + return _keep_to_sizes(ex, keep) + return f + + +def size_dropact(w: int = 90, q: float = 0.75): + """Scarta i trade il cui giorno d'ingresso cade nei periodi in cui la gamba e' stata piu' + ATTIVA (conteggio di barre attive negli ultimi w giorni sopra il quantile ESPANDENTE causale + q). Nessun vol-target, nessuna magnitudine, nessun prezzo: solo la frequenza dei trade.""" + def f(ex): + base = ex["base_daily"] + cnt = (base != 0.0).astype(float).rolling(w, min_periods=max(5, w // 3)).sum().shift(1) + thr = cnt.expanding(min_periods=V.VT_WARM).quantile(q) + keep = pd.Series(np.where(thr.notna().values & cnt.notna().values, + cnt.values <= thr.values, True), index=base.index) + return _keep_to_sizes(ex, keep) + return f + + +def iv_family() -> dict: + d = {} + for p in (0.5, 1.0): + for w in (30, 90): + for cp in (2.0, 4.0): + d[f"IV p{p} w{w} cap{cp:.0f}"] = V.size_iv(p, w, cp) + return d + + +# --------------------------------------------------------------------------- motore +def legs(EX: dict, funcs: dict) -> dict: + """name -> serie giornaliera della gamba SKH 50/50, per un'ancora.""" + out = {"BASE": V.leg_daily(EX, {a: V.size_flat(EX[a]) for a in ASSETS})} + for nm, fn in funcs.items(): + out[nm] = V.leg_daily(EX, {a: fn(EX[a]) for a in ASSETS}) + return out + + +def iso_pair(TP: pd.Series, leg: pd.Series, leg_base: pd.Series): + """Libro della variante e libro di CONTROLLO ISO-PESO (baseline ri-scalato alla stessa vol + di gamba -> stesso peso effettivo di SKH). E' la convenzione del bersaglio, riusata.""" + vb = vol(leg_base) + m_s = vol(leg) / vb if vb > 0 else 1.0 + return book(TP, leg), book(TP, leg_base * m_s), m_s + + +def delta_table(BK: dict, CT: dict, names, offs, label: str, extra_hold=True) -> dict: + res = {} + print(f" {'cella':<26}{'ISO dShFULL':>12}{'pos/n':>8}{'banda':>20}" + + (f"{'ISO dShHOLD':>13}{'pos/n':>8}" if extra_hold else "") + f"{'wSKHeff':>9}") + for nm in names: + dF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o, n=nm: CT[n][o], offs, + metric=sh) + row = dict(dF=dF["median_paired"], nF=dF["n_positive"], lo=dF["lo"], hi=dF["hi"]) + s = (f" {nm:<26}{row['dF']:>+12.3f}{row['nF']:>5}/{len(offs):<3}" + f" [{row['lo']:+.3f},{row['hi']:+.3f}]".ljust(20 + 26 + 12 + 8)) + s = (f" {nm:<26}{row['dF']:>+12.3f}{row['nF']:>5}/{len(offs):<3}" + f"{'[%+.3f,%+.3f]' % (row['lo'], row['hi']):>20}") + if extra_hold: + dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]), + lambda o, n=nm: hold(CT[n][o]), offs, metric=sh) + row.update(dH=dH["median_paired"], nH=dH["n_positive"]) + s += f"{row['dH']:>+13.3f}{row['nH']:>5}/{len(offs):<3}" + we = float(np.median([0.25 * m for m in MSCALE[nm]])) if nm in MSCALE else float("nan") + row["weff"] = we + s += f"{we:>9.3f}" + print(s) + res[nm] = row + return res + + +MSCALE: dict = {} + + +def per_year(BK: dict, CT: dict, nm: str, offs) -> tuple: + yrs = sorted({int(y) for y in BK[nm][offs[0]].index.year}) + cells = [] + for y in yrs: + v = [sh(BK[nm][o][BK[nm][o].index.year == y]) - sh(CT[nm][o][CT[nm][o].index.year == y]) + for o in offs if (BK[nm][o].index.year == y).sum() > 20] + cells.append(float(np.median(v)) if v else float("nan")) + return yrs, cells + + +# =========================================================================== main +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--every", type=int, default=1) + args = ap.parse_args() + offs = OFFSETS_FULL[:: max(1, args.every)] + t0 = time.time() + L = 112 + + print("=" * L) + print(" r0822 VOL-SIZE β€” SCETTICO. Bersaglio: `VTL tv20 w90` = +0,070 ISO dShFULL (23/23)") + print("=" * L) + print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} della griglia a priori 02/07 {list(offs)}") + print(" lente: path CANONICO (identica nelle due braccia di ogni confronto). TP01 canonico.") + + TP = A.tp01_baseline_daily() + + # ------------------------------------------------------------------ celle + IV = iv_family() + FUN_V2 = dict(IV) + FUN_V2["VTL tv20 w90 (LEAD)"] = size_vtl(FROZEN) + FUN_V2["BIN L>=1 (0/1)"] = size_bin(FROZEN, "ge1") + FUN_V2["BIN L>=med (0/1)"] = size_bin(FROZEN, "gemed") + FUN_V2["BIN L<1 (INVERSO)"] = size_bin(FROZEN, "lt1") + FUN_V2["BIN+GRAD"] = size_bin_grad(FROZEN) + FUN_V2["DROP attivita' >p75"] = size_dropact(90, 0.75) + FUN_V2["DROP attivita' >p50"] = size_dropact(90, 0.50) + FUN_V2["canale MAGNITUDINE cont."] = size_chan("mag", False) + FUN_V2["canale FREQUENZA cont."] = size_chan("freq", False) + FUN_V2["canale MAGNIT. 0/1"] = size_chan("mag", True) + FUN_V2["canale FREQ. 0/1"] = size_chan("freq", True) + WGRID = (15, 20, 30, 45, 60, 90, 120, 180, 270) + for w in WGRID: + if w != 90: + FUN_V2[f"VTL tv20 w{w}"] = size_vtl(dict(FROZEN, w=w)) + FUN_V1 = dict(IV) + 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") + 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.") + + # ------------------------------------------------------------------ ciclo ancore + BK2, CT2, BK1, CT1 = {}, {}, {}, {} + EX0 = None + print("\n calcolo per ancora ...", flush=True) + for o in offs: + EXV2 = {a: V.extract(a, o) for a in ASSETS} + EXV1 = {a: V.extract(a, o, SKH01_V1) for a in ASSETS} + if o == offs[0]: + EX0 = (EXV2, EXV1) + for tag, EXX, FUN, BKd, CTd in (("V2", EXV2, FUN_V2, BK2, CT2), + ("V1", EXV1, FUN_V1, BK1, CT1)): + lg = legs(EXX, FUN) + base = lg["BASE"] + for nm, s in lg.items(): + b, c, m_s = iso_pair(TP, s, base) + BKd.setdefault(nm, {})[o] = b + CTd.setdefault(nm, {})[o] = c + if tag == "V2": + MSCALE.setdefault(nm, []).append(m_s) + if o != offs[0]: + del EXV2, EXV1 + print(f" fatto in {time.time()-t0:.0f}s") + EXV2, EXV1 = EX0 + + # ------------------------------------------------------------------ Β§0 SANITY + print("\n" + "-" * L) + print(" 0. SANITY β€” replica BIT-EXACT prima di qualunque delta (se non e' 0, il problema") + print(" e' mio e va detto)") + print("-" * L) + from src.portfolio.sleeves import _skyhook_returns + mine = V.leg_daily(EXV2, {a: V.size_flat(EXV2[a]) for a in ASSETS}) + off = _skyhook_returns() + d0 = float(np.max(np.abs(mine.values - off.values))) if len(mine) == len(off) else float("nan") + print(f" gamba 50/50 off{offs[0]} vs sleeves._skyhook_returns(): max|dif| = {d0:.2e} " + f"({len(mine)} giorni)") + assert d0 < 1e-15, "replica NON bit-exact" + for a in ASSETS: + ref = R.run_asset(a, offs[0])[0] + R._CACHE.clear() + dd = float(np.max(np.abs(V.equity_daily(EXV2[a], V.size_flat(EXV2[a])).values - ref.values))) + print(f" {a} vs r0702_anchor_skh01.run_asset: max|dif| = {dd:.2e}") + assert dd < 1e-15 + + lead = "VTL tv20 w90 (LEAD)" + dLEAD = A.anchor_luck_delta(lambda o: BK2[lead][o], lambda o: CT2[lead][o], offs, metric=sh) + dLEADH = A.anchor_luck_delta(lambda o: hold(BK2[lead][o]), lambda o: hold(CT2[lead][o]), + offs, metric=sh) + print(f" RIPRODUZIONE DEL TITOLO: ISO dShFULL {dLEAD['median_paired']:+.3f} " + f"({dLEAD['n_positive']}/{len(offs)}) ISO dShHOLD {dLEADH['median_paired']:+.3f} " + f"({dLEADH['n_positive']}/{len(offs)}) [pubblicato +0,070 (23/23) / +0,101 (19/23)]") + med_F = float(np.median([sh(BK2["BASE"][o]) for o in offs])) + med_H = float(np.median([sh(hold(BK2["BASE"][o])) for o in offs])) + med_D = float(np.median([maxdd(BK2["BASE"][o]) for o in offs])) + print(f" libro BASE (V2-DD), mediana della banda: ShFULL {med_F:.3f} / ShHOLD {med_H:.3f} " + f"/ maxDD {med_D*100:.1f}% [pubblicato 1,626 / 1,037 / 10,4%]") + + # ------------------------------------------------------------------ Β§1 CONTABILITA' + print("\n" + "-" * L) + print(" 1. LA CONTABILITA' β€” verifica della PREMESSA prima della riparazione") + print(" (l'agente: 'tutto il P&L di un trade e' contabilizzato il giorno di CHIUSURA')") + print("-" * L) + tot_ent = tot_ex = tot_chk = 0 + for a in ASSETS: + ex = EXV2[a] + d_e = ex["idx"][ex["i_ent"]].floor("D") + d_x = ex["idx"][ex["i_ex"]].floor("D") + dl = ex["base_daily"] + multi = np.where(d_e.values != d_x.values)[0] + ne = nx = ck = 0 + for k in multi: + n = float(ex["net"][k]) + if abs(n) < 1e-6: + continue + ck += 1 + ne += int(abs(float(dl.get(d_e[k], 0.0)) - n) < 1e-9) + nx += int(abs(float(dl.get(d_x[k], 0.0)) - n) < 1e-9) + print(f" {a}: {ck} trade multi-giorno -> ritorno giornaliero == net del trade " + f"al giorno d'INGRESSO {ne}/{ck}, al giorno di USCITA {nx}/{ck}") + tot_ent += ne; tot_ex += nx; tot_chk += ck + print(f" TOTALE: INGRESSO {tot_ent}/{tot_chk} ({tot_ent/max(tot_chk,1)*100:.1f}%) " + f"USCITA {tot_ex}/{tot_chk} ({tot_ex/max(tot_chk,1)*100:.1f}%)") + print(" -> `backtest_signals` fa `equity[i_ent:i_ex+1] = capital`: il gradino comincia") + print(" alla barra d'INGRESSO e il resample giornaliero (last) lo legge quel giorno.") + + print("\n (a) TRONCAMENTO ALLA BARRA D'INGRESSO β€” le size della regola congelata " + "ricalcolate") + print(" con i soli dati <= barra d'ingresso devono coincidere ESATTAMENTE") + worst = 0.0 + ntest = 0 + for a in ASSETS: + ex = EXV2[a] + full = size_vtl(FROZEN)(ex) + sel = [k for k in range(len(ex["i_ent"])) if ex["i_ent"][k] >= V.SIG_WARM] + sel = sel[:: max(1, len(sel) // 25)][:25] + bad = 0 + for k in sel: + i = int(ex["i_ent"][k]) + # ricostruisco la gamba usando SOLO i trade gia' CHIUSI prima dell'ingresso k + sub = {kk: ex[kk] for kk in ("n", "idx")} + m = ex["i_ex"] < i + sub = dict(n=i + 1, idx=ex["idx"][: i + 1], + i_ent=ex["i_ent"][m], i_ex=ex["i_ex"][m], net=ex["net"][m]) + if not len(sub["i_ent"]): + continue + bd = V.equity_daily(sub, np.ones(len(sub["i_ent"]))) + Lt = V.leverage_series(bd, **FROZEN) + day = ex["idx"][i].floor("D") + v = Lt.reindex([day]).values[0] + v = 1.0 if not np.isfinite(v) else float(v) + worst = max(worst, abs(v - float(full[k]))) + bad += int(abs(v - float(full[k])) > 1e-9) + ntest += 1 + print(f" {a}: {len(sel)-bad}/{len(sel)} size identiche a dati troncati " + f"(divergenze {bad})") + print(f" max|dif| su {ntest} size ricalcolate da zero: {worst:.2e} -> " + f"{'CAUSALE' if worst < 1e-9 else 'NON CAUSALE'}") + + print("\n (b) LA 'RIPARAZIONE' E' UN NO-OP: size-all'INGRESSO vs moltiplicare la SERIE") + print(" GIORNALIERA, a parita' di tutto il resto (stessa L, PER ASSET)") + for a in ASSETS: + ex = EXV2[a] + Ls = V.leverage_series(ex["base_daily"], **FROZEN) + s_in = V.equity_daily(ex, V.sizes_from_L(ex, Ls)) + s_mul = ex["base_daily"] * Ls.reindex(ex["base_daily"].index).fillna(1.0) + j = pd.concat({"a": s_in, "b": s_mul}, axis=1, join="inner").dropna() + 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).") + + # ------------------------------------------------------------------ Β§2 TEST PRE-REGISTRATO + print("\n" + "-" * L) + print(" 2. IL TEST PRE-REGISTRATO β€” famiglia IV su V2-DD (stop % FISSO) vs V1 (stop in ATR)") + print(" Previsione dichiarata dall'agente: |effetto su V1| < |effetto su V2-DD|.") + print("-" * L) + n2 = {a: len(EXV2[a]["i_ent"]) for a in ASSETS} + n1 = {a: len(EXV1[a]["i_ent"]) for a in ASSETS} + print(f" trade all'ancora {offs[0]}: V2-DD {n2} V1 {n1}") + mb2 = float(np.median([sh(BK2["BASE"][o]) for o in offs])) + mb1 = float(np.median([sh(BK1["BASE"][o]) for o in offs])) + print(f" libro BASE mediana di banda: V2-DD ShFULL {mb2:.3f} | V1 ShFULL {mb1:.3f}") + names_iv = list(IV) + print(f"\n {'cella IV':<22}{'V2-DD ISO dF':>14}{'pos/n':>8}{'V1 ISO dF':>12}{'pos/n':>8}" + f"{'|V1|<|V2|?':>12}") + ok = tot = 0 + e2s, e1s = [], [] + for nm in names_iv: + a2 = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs, + metric=sh) + a1 = A.anchor_luck_delta(lambda o, n=nm: BK1[n][o], lambda o, n=nm: CT1[n][o], offs, + metric=sh) + e2, e1 = a2["median_paired"], a1["median_paired"] + e2s.append(e2); e1s.append(e1) + good = abs(e1) < abs(e2) + ok += int(good); tot += 1 + print(f" {nm:<22}{e2:>+14.3f}{a2['n_positive']:>5}/{len(offs):<3}{e1:>+12.3f}" + f"{a1['n_positive']:>5}/{len(offs):<3}{('SI' if good else 'NO'):>12}") + print(f"\n previsione verificata in {ok}/{tot} celle della famiglia") + print(f" |effetto| mediano di famiglia: V2-DD {np.median(np.abs(e2s)):.3f} " + f"V1 {np.median(np.abs(e1s)):.3f}") + print(f" effetto mediano SEGNATO: V2-DD {np.median(e2s):+.3f} V1 {np.median(e1s):+.3f}") + print(" ⚠️ POTENZA DEL TEST: se |effetto su V2-DD| e' gia' ~0, la previsione e' verificabile") + print(" solo per caso e il gate pre-registrato NON puo' falsificare niente.") + for nm in ("VTL tv20 w90 (LEAD)", "BIN L>=1 (0/1)"): + a2 = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs, + metric=sh) + a1 = A.anchor_luck_delta(lambda o, n=nm: BK1[n][o], lambda o, n=nm: CT1[n][o], offs, + metric=sh) + print(f" (riferimento) {nm:<24} V2-DD {a2['median_paired']:+.3f} " + f"({a2['n_positive']}/{len(offs)}) V1 {a1['median_paired']:+.3f} " + f"({a1['n_positive']}/{len(offs)})") + + # ------------------------------------------------------------------ Β§3 ATTACCO ALLA SPIEGAZIONE + print("\n" + "-" * L) + print(" 3. ATTACCO ALLA SPIEGAZIONE β€” quanto del +0,070 resta senza NESSUN vol-target?") + print(" Tutte ISO-PESO vs lo stesso baseline. 'wSKHeff' = peso effettivo di SKH nel libro") + print("-" * L) + grp = [lead, "BIN L>=1 (0/1)", "BIN+GRAD", "BIN L>=med (0/1)", "BIN L<1 (INVERSO)", + "DROP attivita' >p75", "DROP attivita' >p50", + "canale MAGNITUDINE cont.", "canale FREQUENZA cont.", + "canale MAGNIT. 0/1", "canale FREQ. 0/1"] + RES = delta_table(BK2, CT2, grp, offs, "attacco") + base_eff = RES[lead]["dF"] + print() + for nm in grp[1:]: + frac = RES[nm]["dF"] / base_eff * 100 if abs(base_eff) > 1e-9 else float("nan") + print(f" {nm:<26} recupera {frac:>6.0f}% del vantaggio del LEAD") + # quanti trade tocca ciascun filtro + print(f"\n {'cella':<26}{'trade tenuti':>14}{'su':>6}{'size mediana':>14}" + f"{'size = 0':>10}") + for nm in [lead] + grp[1:]: + kept = tot_t = zer = 0 + med = [] + for a in ASSETS: + s = FUN_V2[nm](EXV2[a]) + tot_t += len(s); kept += int((s > 0).sum()); zer += int((s == 0).sum()) + med.append(s) + mm = float(np.median(np.concatenate(med))) + print(f" {nm:<26}{kept:>14}{tot_t:>6}{mm:>14.2f}{zer:>10}") + + print(f"\n COSA SELEZIONA il filtro (ancora {offs[0]}, i due asset uniti): net medio dei") + print(" trade TENUTI vs SCARTATI, e attivita' trailing media dei due gruppi") + print(f" {'cella':<26}{'net TENUTI':>12}{'net SCARTATI':>14}{'att.TEN':>10}{'att.SCA':>10}" + f"{'%tenuti':>9}") + for nm in ("BIN L>=1 (0/1)", "BIN L>=med (0/1)", "BIN L<1 (INVERSO)", + "DROP attivita' >p50", "canale MAGNIT. 0/1", "canale FREQ. 0/1"): + nk, nd, ak, ad = [], [], [], [] + for a in ASSETS: + ex = EXV2[a] + szv = FUN_V2[nm](ex) + cnt = ((ex["base_daily"] != 0.0).astype(float) + .rolling(90, min_periods=30).sum().shift(1)) + av = cnt.reindex(ex["day_ent"]).values + keep = szv > 0 + nk += list(ex["net"][keep]); nd += list(ex["net"][~keep]) + ak += list(av[keep]); ad += list(av[~keep]) + f_ = 100.0 * len(nk) / max(len(nk) + len(nd), 1) + print(f" {nm:<26}{np.mean(nk)*100:>11.2f}%{np.mean(nd)*100 if nd else float('nan'):>13.2f}%" + f"{np.nanmean(ak):>10.1f}{np.nanmean(ad) if ad else float('nan'):>10.1f}{f_:>9.0f}") + + print("\n per anno (ISO dSh, mediana appaiata sulle ancore):") + yrs = None + for nm in [lead, "BIN L>=1 (0/1)", "DROP attivita' >p75", "BIN L<1 (INVERSO)"]: + yrs, cells = per_year(BK2, CT2, nm, offs) + if nm == lead: + print(" " + f"{'':<26}" + "".join(f"{y:>8}" for y in yrs)) + npos = int(np.nansum(np.asarray(cells) > 0)) + print(f" {nm:<26}" + "".join(f"{c:>+8.2f}" for c in cells) + + f" anni positivi {npos}/{len(yrs)}") + + # ------------------------------------------------------------------ Β§4 ASSE DELLA FINESTRA + print("\n" + "-" * L) + print(" 4. L'ASSE DELLA FINESTRA β€” picco o plateau? + scelta di w IN-SAMPLE-ONLY") + print("-" * L) + wnames = [(w, f"VTL tv20 w{w}" if w != 90 else lead) for w in WGRID] + print(f" {'w':>6}{'ISO dShFULL':>14}{'pos/n':>8}{'ISO dShIS':>12}{'ISO dShHOLD':>13}" + f"{'pos/n':>8}") + wf, wi, wh = {}, {}, {} + for w, nm in wnames: + dF = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs, + metric=sh) + dI = A.anchor_luck_delta(lambda o, n=nm: ins(BK2[n][o]), lambda o, n=nm: ins(CT2[n][o]), + offs, metric=sh) + dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK2[n][o]), lambda o, n=nm: hold(CT2[n][o]), + offs, metric=sh) + wf[w], wi[w], wh[w] = dF["median_paired"], dI["median_paired"], dH["median_paired"] + print(f" {w:>6}{wf[w]:>+14.3f}{dF['n_positive']:>5}/{len(offs):<3}{wi[w]:>+12.3f}" + f"{wh[w]:>+13.3f}{dH['n_positive']:>5}/{len(offs):<3}") + wbest_full = max(WGRID, key=lambda w: wf[w]) + wbest_is = max(WGRID, key=lambda w: wi[w]) + print(f"\n argmax su FULL (cio' che l'agente ha pubblicato): w = {wbest_full} " + f"({wf[wbest_full]:+.3f})") + print(f" argmax IN-SAMPLE-ONLY (la sola scelta ammessa): w = {wbest_is} " + f"(IS {wi[wbest_is]:+.3f}) -> quella cella vale FULL {wf[wbest_is]:+.3f} / " + f"HOLD {wh[wbest_is]:+.3f}") + order = sorted(WGRID) + k = order.index(wbest_full) + nb = [order[k - 1] if k > 0 else None, order[k + 1] if k + 1 < len(order) else None] + for w in nb: + if w is not None: + print(f" vicino w={w:<4} vale {wf[w]:+.3f} = {wf[w]/wf[wbest_full]*100:>5.0f}% " + f"del picco") + pos = sum(1 for w in WGRID if wf[w] > 0) + print(f" celle con ISO dShFULL > 0: {pos}/{len(WGRID)} " + f"spread della famiglia {max(wf.values())-min(wf.values()):.3f} " + f"sd fra celle {np.std(list(wf.values()), ddof=1):.3f}") + print(f" stima ONESTA se w e' SCELTO (mediana della famiglia, come per la banda " + f"d'ancora): {np.median(list(wf.values())):+.3f} FULL / " + f"{np.median(list(wh.values())):+.3f} HOLD contro il picco " + f"{wf[wbest_full]:+.3f} / {wh[wbest_full]:+.3f}") + rho = float(np.corrcoef([wi[w] for w in WGRID], [wh[w] for w in WGRID])[0, 1]) + print(f" corr(ISO dSh in-sample, ISO dSh hold-out) fra le {len(WGRID)} finestre: {rho:+.2f}") + print(" -> se e' ~0 o negativa, la scelta di w in-sample NON predice l'hold-out: l'asse") + print(" e' rumore e il picco a w=90 e' un max-of-k.") + + # ------------------------------------------------------------------ Β§5 sintesi + print("\n" + "-" * L) + print(" 5. 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 " + f"{med_F:.3f}") + print(f" vantaggio del LEAD (ISO): {dLEAD['median_paired']:+.3f} FULL / " + 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"\n fatto in {time.time()-t0:.0f}s") + print("=" * L) + + +if __name__ == "__main__": + main()