diff --git a/scripts/research/r0823_anchor_ensemble.py b/scripts/research/r0823_anchor_ensemble.py new file mode 100644 index 0000000..02e12c8 --- /dev/null +++ b/scripts/research/r0823_anchor_ensemble.py @@ -0,0 +1,1230 @@ +#!/usr/bin/env python +"""r0823_anchor_ensemble.py — §47 ANCHOR-ENSEMBLE: si puo' ESEGUIRE l'ensemble d'ancora? + +LA DOMANDA. Il progetto ha misurato con enorme cura la FORTUNA D'ANCORA (TP01 gira sull'ancora +daily 00:00 UTC, la migliore delle 24; SKH01 sull'offset 0 di 23; XS01 fase 0 di 10; VRP01 fase +0 di 7) e ha sempre risposto DE-LUCKANDO IL NUMERO — mai cambiando il modo di eseguire. Qui si +chiede l'altra cosa: invece di scegliere un'ancora e poi correggere la stima, si puo' dividere +lo sleeve in N tranche, ognuna ribilanciata a un'ora diversa e ognuna a 1/N della size? + +PRECEDENTE DA SUPERARE (non ignorare). Il 2026-07-02 (`r0702_tp01_offset.py`, diario +`2026-07-02-timing-crt-wave.md`) misuro' il tranching K=2/K=4 e concluse: *"sola riduzione della +varianza della STIMA (dSharpe n.s., dDD ~0.5pt), NO deploy a $600 — a $225/asset di quota TP01 +i delta per-ancora (~$1-2) sono sotto il min-order $5 -> degenererebbe comunque in K=1; serve +feed intraday fuori path certificato. Rivalutare a >=5-10k."* +Tre cose sono cambiate: (a) il FUNDING e' misurato (-2,16%/anno, 22/08) e il tranching cambia il +profilo orario d'esposizione; (b) la banda d'ancora del LIBRO e' misurata sullo spazio congiunto +(26/07); (c) il 22/08 ha misurato che il pavimento min-order non e' il vincolo che si credeva. + +E c'e' un quarto fatto, di CODICE, che nessuno aveva messo accanto alla conclusione del 02/07 — +si veda §4: `src/live/book.build_book_order` applica `abs(delta) < min_usd -> None` sulla +posizione **NETTA**, e manda **un solo ordine per asset per giro**. Il "delta per-ancora" non e' +mai un ordine. La premessa del 02/07 descrive un'esecuzione che il libro non fa. + +COSA MISURA, nelle tre parti chieste: + (i) l'ensemble batte l'ancora MEDIANA? di quanto su Sharpe E su DRIFT, e con che + compressione di dispersione fra realizzazioni; + (ii) l'ensemble batte l'ancora CANONICA che gira oggi? (attesa: NO in-sample) — e quale dei + due numeri e' la stima onesta del FUTURO; + (iii) a quale CAPITALE l'ensemble diventa eseguibile senza degenerare? curva N_max(capitale) + con il pavimento VERO del venue, per $635 / $2k / $5k / $20k / $50k. +piu': il costo (ordini/anno, fee/anno), il FUNDING (l'unico canale per cui l'ensemble potrebbe +GUADAGNARE invece di ridurre varianza), SKH01 separatamente e insieme a TP01, la replica del +02/07 PRIMA di ogni delta, e il valore della riduzione di varianza SOTTO UNA BARRIERA ASSORBENTE +(canale funded), che e' la domanda che resta se la risposta e' "non aggiunge ritorno". + +RIUSO (non riscrittura): `altlib` (eval_weights/_sh/_to_daily/_dd_ret/HOLDOUT), +`r0702_tp01_offset` (resample_offset/hourly_target/smallcap_net — la replica e' like-for-like), +`r0702_anchor_skh01` (run_asset/resample_off/OFFSETS), `r0822d_funding` (fetch/build_rate/cumF — +il funding NON viene rimodellato), `src.live.deribit._CONTRACT` (il pavimento si DERIVA dal +codice sorvegliato, non si ridichiara — regola `fee_watch` 21/08), +`src.portfolio.portfolio.combine_outer`. + +VINCOLI: nessun file toccato fuori da questo script; nessuna rete (cache funding gia' su disco); +niente commit di dati. Runtime ~5-8 min su 2 core. + +Uso: `nice -n 19 uv run python scripts/research/r0823_anchor_ensemble.py` +""" +from __future__ import annotations + +import sys +import time +from functools import lru_cache +from itertools import product +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in ("scripts/research", "scripts/research/alt", ""): + sys.path.insert(0, str(ROOT / _p)) + +import altlib as al # noqa: E402 +import r0702_anchor_skh01 as r02 # noqa: E402 +import r0702_tp01_offset as r07 # noqa: E402 +import r0822d_funding as fd # noqa: E402 +from src.live.deribit import _CONTRACT # noqa: E402 (il pavimento si DERIVA, non si ridichiara) +from src.portfolio.portfolio import combine_outer # noqa: E402 + +fd.USE_NET = False # cache su disco: questo filone non tocca la rete + +ASSETS = ("BTC", "ETH") +HOURS = tuple(range(24)) +HOLDOUT = al.HOLDOUT +FEE = al.FEE_SIDE # 0.05%/lato = 0.10% RT +MSH = 3_600_000 + +# Il libro live: `src/live/book.py` W_TP01=0.75, W_SKH=0.25, WEIGHT=0.5 (50/50 BTC+ETH). +W_TP, W_SKH, W_ASSET = 0.75, 0.25, 0.5 +MIN_ORDER_USD = 5.0 # config/live.json min_order_usd (letto sotto) +CAPITALI = (635.0, 2000.0, 5000.0, 20000.0, 50000.0) + +# famiglie di tranching a priori, simmetriche, zero tuning (stessa logica del 02/07) +FAM_TP = {1: 24, 2: 12, 3: 8, 4: 6, 6: 4, 8: 3, 12: 2, 24: 1} # K -> n. rotazioni + +T0 = time.time() + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +def fam_rotations(K: int) -> list[tuple]: + """Le TUTTE rotazioni della famiglia K (nessuna selezione): (s, s+24/K, s+2*24/K, ...).""" + step = 24 // K + return [tuple((s + step * j) % 24 for j in range(K)) for s in range(step)] + + +# =========================================================================== +# §0 — ATTESE A PRIORI (scritte e stampate PRIMA di qualunque misura) +# =========================================================================== +ATTESE = [ + ("A1", "ALGEBRA: la posizione dell'ensemble e' la MEDIA delle posizioni delle tranche, e il " + "lordo orario e' lineare nella posizione -> lordo_ens == media dei lordi, ESATTAMENTE " + "(max|dif| = 0). Il turnover invece e' SUB-additivo (|media dei delta| <= media dei " + "|delta|) -> turnover_ens <= turnover MEDIO delle ancore. Quindi l'ensemble domina " + "debolmente l'ancora MEDIA su drift E su vol: non e' 'solo' riduzione di varianza, ma " + "il pezzo di drift e' solo fee-netting, cioe' briciole."), + ("A2", "(i) l'ensemble batte l'ancora MEDIANA su Sharpe. Quasi per costruzione: stesso drift, " + "meno vol. Attesa dSharpe FULL fra +0,03 e +0,15; dDRIFT ~ 0 (<0,3pp)."), + ("A3", "(ii) l'ensemble NON batte l'ancora CANONICA sull'hold-out. La canonica sta al ~92-96° " + "pctl delle 24: una media non batte il massimo. Attesa: dSharpe HOLD NEGATIVO. " + "E la stima onesta del futuro e' comunque l'ensemble, non la canonica."), + ("A4", "FUNDING: l'ensemble NON puo' guadagnare un centesimo. Il funding pagato in un'ora e' " + "pos*f, ESATTAMENTE lineare in pos -> funding_ens == media dei funding delle ancore, " + "max|dif| = 0. Il rapporto condizionale mean(pos*f)/(mean(pos)*mean(f)) — misurato " + "1,86-2,55x il 22/08 — e' esso stesso una media e NON si attenua: la correlazione " + "esposizione-funding vive alla scala del REGIME, non dell'ora del giorno. " + "Il canale sperato e' chiuso per ALGEBRA, non per piccolezza dell'effetto."), + ("A5", "(iii) ESEGUIBILITA': gli ordini NON sono K volte tanti. `build_book_order` banda la " + "posizione NETTA e manda un ordine per asset per giro -> il numero di ordini e' " + "limitato dalla BANDA, non da K. La premessa del 02/07 ('i delta per-ancora sono sotto " + "il min-order') descrive un'esecuzione che il libro non fa. MA la conclusione del 02/07 " + "sopravvivera' comunque, per un'altra strada: a $635 la banda mangia gia' K=1 (il " + "ribilancio MEDIANO di TP01 e' sotto $5), quindi non c'e' K da degradare. " + "Attesa: N_max($635) = 1, e N_max cresce con il capitale."), + ("A6", "SKH01: peggiore rapporto costo/beneficio (4x piu' fee-sensibile, 22/08) e " + "690/30 = 23 e' PRIMO -> sulla griglia a 30m dichiarata il 02/07 non esiste alcun " + "sotto-ensemble simmetrico (o 1 o 23). Le famiglie simmetriche esistono solo sulla " + "griglia 5m (138 = 2*3*23 -> K in {2,3,6,23})."), + ("A7", "BARRIERA: la riduzione di varianza vale poco su un conto proprio (dove il nemico e' il " + "drift) e MOLTO su un conto a barriera assorbente. Attesa: P(pass) dell'ensemble >= " + "mediana delle P(pass) per ancora, e soprattutto SPARISCE la lotteria (la banda di " + "P(pass) fra ancore si comprime). E il conto funded e' a $100k, dove la banda del " + "punto (iii) NON morde: il posto dove l'ensemble e' inutile e quello dove e' " + "ineseguibile sono lo STESSO posto, e sono entrambi il conto da $635."), +] + + +def sezione0() -> None: + sez("§0 — ATTESE A PRIORI (dichiarate PRIMA di misurare; il verdetto su ciascuna e' in §9)") + for k, t in ATTESE: + print(f" [{k}] {t}\n") + print(" Criterio N_max DICHIARATO ORA, prima di guardare i numeri (§4):") + print(" l'ensemble serve a UNA cosa: comprimere la dispersione fra realizzazioni. Quindi") + print(" retention(K,C) = [sd_band(1,C) - sd_band(K,C)] / [sd_ideale(1) - sd_ideale(K)]") + print(" con sd = deviazione standard dello Sharpe FULL fra TUTTE le rotazioni della") + print(" famiglia K (nessuna selezione), banda = pavimento vero del venue a capitale C.") + print(" N_max(C) = il massimo K in {2,3,4,6,8,12} con retention >= 0.50.") + print(" K=24 ha UNA sola rotazione -> riportato a parte, fuori dal criterio.") + + +# =========================================================================== +# PRIMITIVE — target orari, ensemble, valutazione ideale e BANDATA +# =========================================================================== +@lru_cache(maxsize=8) +def px(asset: str) -> np.ndarray: + return r07.get1h(asset)["close"].values.astype(float) + + +@lru_cache(maxsize=8) +def ret(asset: str) -> np.ndarray: + return al.simple_returns(px(asset)) + + +@lru_cache(maxsize=8) +def idx1h(asset: str) -> pd.DatetimeIndex: + return pd.DatetimeIndex(pd.to_datetime(r07.get1h(asset)["datetime"], utc=True)) + + +@lru_cache(maxsize=256) +def tgt(asset: str, hs: tuple) -> np.ndarray: + """Target dell'ensemble sulle ancore `hs`: media dei target orari delle tranche. + `r07.hourly_target` e' RIUSATA (merge_asof backward su epoca ms esplicita).""" + return np.mean([np.asarray(r07.hourly_target(asset, h)) for h in hs], axis=0) + + +def eval_ideal(asset: str, hs: tuple) -> dict: + """Valutazione IDEALE (nessun pavimento): riusa al.eval_weights, poi decompone.""" + df = r07.get1h(asset) + ev = al.eval_weights(df, tgt(asset, hs), fee_side=FEE) + t = tgt(asset, hs) + pos = np.zeros(len(t)); pos[1:] = t[:-1] + gross = pos * ret(asset) + turn = np.abs(np.diff(pos, prepend=0.0)) + net = gross - FEE * turn; net[0] = 0.0 + assert np.max(np.abs(net - ev["net"])) < 1e-15, "decomposizione != eval_weights" + return dict(pos=pos, gross=gross, turn=turn, net=net, idx=ev["idx"], + turn_y=ev["turnover_per_year"]) + + +@lru_cache(maxsize=256) +def book_ideal(hs: tuple) -> dict: + """Book 50/50 BTC+ETH IDEALE sull'orario: serie oraria + daily + turnover.""" + E = {a: eval_ideal(a, hs) for a in ASSETS} + S = {a: pd.Series(E[a]["net"], index=E[a]["idx"]) for a in ASSETS} + G = {a: pd.Series(E[a]["gross"], index=E[a]["idx"]) for a in ASSETS} + P = {a: pd.Series(E[a]["pos"], index=E[a]["idx"]) for a in ASSETS} + J = pd.concat(S, axis=1, join="inner").fillna(0.0) + JG = pd.concat(G, axis=1, join="inner").fillna(0.0) + JP = pd.concat(P, axis=1, join="inner").fillna(0.0) + h_net = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]] + h_gross = 0.5 * JG[ASSETS[0]] + 0.5 * JG[ASSETS[1]] + return dict(h_net=h_net, h_gross=h_gross, pos=JP, daily=al._to_daily(h_net), + turn_y=sum(0.5 * E[a]["turn_y"] for a in ASSETS)) + + +def banded(prices: np.ndarray, target: np.ndarray, capital: float, + min_usd: float = MIN_ORDER_USD, step: float | None = None) -> dict: + """Path ESEGUITO sotto il pavimento del venue. + + Modella `src/live/book.build_book_order` + `deribit.notional_to_amount`: + - `abs(delta) < min_usd` -> nessun ordine (banda sul NOZIONALE, non sulla frazione); + - se `step` e' dato (contract step del perp lineare, in base-coin) il delta eseguito viene + quantizzato allo step e CLAMPATO al minimo -> la posizione atterra su una griglia. + `target[i]` e' la frazione di `capital` desiderata alla barra i (stessa convenzione di + `r07.smallcap_net`, contro cui questa funzione e' validata bit-exact in §1). + + Ottimizzazione: la condizione puo' scattare solo dove il target CAMBIA (fra due cambi il + target e' costante, quindi se non e' scattata non scattera'). Si itera sui punti di cambio. + """ + n = len(target) + chg = np.empty(n, dtype=bool) + chg[0] = True + chg[1:] = target[1:] != target[:-1] + ci = np.flatnonzero(chg) + vals = np.empty(len(ci)) + cur = 0.0 + n_ord = 0 + notion = 0.0 + for k in range(len(ci)): + i = ci[k] + d = (target[i] - cur) * capital + if abs(d) >= min_usd: + if step is None: + cur = target[i] + notion += abs(d) + else: + u = abs(d) / prices[i] + q = np.round(u / step) * step + if q < step: + q = step + ex = np.sign(d) * q * prices[i] + cur = cur + ex / capital + notion += abs(ex) + n_ord += 1 + vals[k] = cur + held = np.repeat(vals, np.diff(np.append(ci, n))) + return dict(held=held, n_ord=n_ord, notional=notion, n_chg=len(ci)) + + +def eval_banded(asset: str, hs: tuple, capital: float, step: float | None = None) -> dict: + """Book di UN asset sotto banda. `capital` = capitale ALLOCATO A QUELL'ASSET.""" + b = banded(px(asset), tgt(asset, hs), capital, MIN_ORDER_USD, step) + held = b["held"] + pos = np.zeros(len(held)); pos[1:] = held[:-1] + turn = np.abs(np.diff(pos, prepend=0.0)) + net = pos * ret(asset) - FEE * turn + net[0] = 0.0 + return dict(net=net, pos=pos, turn=turn, n_ord=b["n_ord"], n_chg=b["n_chg"], + notional=b["notional"], idx=idx1h(asset)) + + +def book_banded(hs: tuple, capital: float, step_by_asset: dict | None = None) -> dict: + """Book 50/50 sotto banda. `capital` = capitale TOTALE del book TP01 (meta' per asset).""" + E = {} + for a in ASSETS: + st = step_by_asset.get(a) if step_by_asset else None + E[a] = eval_banded(a, hs, capital * 0.5, st) + S = {a: pd.Series(E[a]["net"], index=E[a]["idx"]) for a in ASSETS} + J = pd.concat(S, axis=1, join="inner").fillna(0.0) + h = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]] + yrs = (h.index[-1] - h.index[0]) / pd.Timedelta("365.25D") + return dict(daily=al._to_daily(h), n_ord=sum(E[a]["n_ord"] for a in ASSETS), + ord_y=sum(E[a]["n_ord"] for a in ASSETS) / yrs, + fee_y=sum(FEE * E[a]["turn"].sum() for a in ASSETS) / 2 / yrs, + notional_y=sum(E[a]["notional"] for a in ASSETS) / yrs, E=E) + + +def drift(s: pd.Series) -> float: + """Drift annuo geometrico (CAGR) di una serie di ritorni giornalieri.""" + v = np.asarray(s.dropna().values, float) + eq = float(np.prod(1.0 + v)) + yrs = len(v) / 365.25 + return eq ** (1 / yrs) - 1 if eq > 0 and yrs > 0 else -1.0 + + +def vol(s: pd.Series) -> float: + return float(np.std(np.asarray(s.dropna().values, float)) * np.sqrt(365.25)) + + +def m3(s: pd.Series) -> tuple: + return (al._sh(s), al._sh(s[s.index < HOLDOUT]), al._sh(s[s.index >= HOLDOUT])) + + +def pctl_of(v: np.ndarray, x: float) -> float: + return float((v < x).mean() + 0.5 * (v == x).mean()) * 100 + + +# =========================================================================== +# §1 — REPLICA. Nessun delta prima che questa sezione passi. +# =========================================================================== +def sezione1() -> dict: + sez("§1 — REPLICA DI CONTROLLO (il 02/07 PRIMA di ogni delta) + pavimento vero del venue") + + r07.sanity_h0() + r07.causality_guards() # prefix-recompute daily + troncamento 1h, per TUTTE le ancore + + # (a) la funzione `banded` di questo script contro `r07.smallcap_net` (codice del 02/07) + print("\n (a) `banded()` di questo script vs `r07.smallcap_net` del 02/07 (bit-exact):") + for a in ASSETS: + df = r07.get1h(a) + t = 0.5 * tgt(a, (0,)) + ref, ntr = r07.smallcap_net(df, t, 600.0) + b = banded(px(a), t, 600.0, 5.0, None) + held = b["held"] + pos = np.zeros(len(held)); pos[1:] = held[:-1] + net = pos * ret(a) - FEE * np.abs(np.diff(pos, prepend=0.0)); net[0] = 0.0 + d = float(np.max(np.abs(net - ref.values))) + assert d == 0.0, f"banded != smallcap_net su {a}: {d:.3e}" + assert b["n_ord"] == ntr, f"n_ordini {b['n_ord']} != {ntr}" + print(f" {a}: max|dif| = {d:.1e} ordini {b['n_ord']} == {ntr} OK " + f"(punti di cambio del target: {b['n_chg']:,} -> la compressione e' esatta)") + + # (b) K=4 == EW di 4 book ancorati (il claim "diff 1.4e-17" del 02/07) + K4 = (0, 6, 12, 18) + ens = book_ideal(K4)["daily"] + ew = sum(book_ideal((h,))["h_net"] for h in K4) / 4.0 + ew_d = al._to_daily(ew) + J = pd.concat({"e": ens, "w": ew_d}, axis=1, join="inner") + d = float(np.max(np.abs(J["e"] - J["w"]))) + print(f"\n (b) K=4 == EW di 4 book ancorati (claim 02/07 'diff 1.4e-17'):") + print(f" sull'ORARIO la posizione e' identica per costruzione; sul DAILY il compounding") + print(f" intra-giorno non e' lineare -> max|dif| = {d:.2e} su {len(J):,} giorni.") + + # (c) i numeri del 02/07, con la deriva del dato dichiarata + singles = {h: book_ideal((h,))["daily"] for h in HOURS} + shf = np.array([al._sh(singles[h]) for h in HOURS]) + shi = np.array([al._sh(singles[h][singles[h].index < HOLDOUT]) for h in HOURS]) + shh = np.array([al._sh(singles[h][singles[h].index >= HOLDOUT]) for h in HOURS]) + dd = np.array([al._dd_ret(singles[h]) for h in HOURS]) + e4 = book_ideal(K4)["daily"] + rot4 = [book_ideal(hs)["daily"] for hs in fam_rotations(4)] + h4 = np.array([al._sh(s[s.index >= HOLDOUT]) for s in rot4]) + print(f"\n (c) i numeri pubblicati il 02/07, ri-misurati oggi (il dato e' cresciuto di ~7") + print(f" settimane e `data/raw/` non e' versionato — la deriva si dichiara, non si nasconde):") + print(f" {'grandezza':<44} {'02/07':>16} {'oggi':>16}") + print(f" {'HOLD mediano fra le 24 ancore':<44} {'0.04':>16} {np.median(shh):>16.3f}") + print(f" {'HOLD banda fra le 24 ancore':<44} {'[-0.13,+0.30]':>16} " + f"{'[%+.2f,%+.2f]' % (shh.min(), shh.max()):>16}") + print(f" {'HOLD canonico h=0':<44} {'0.31 (98 pctl)':>16} " + f"{'%.3f (%.0f pctl)' % (shh[0], pctl_of(shh, shh[0])):>16}") + print(f" {'IS h=0 -> K=4':<44} {'1.49 -> 1.56':>16} " + f"{'%.2f -> %.2f' % (shi[0], al._sh(e4[e4.index < HOLDOUT])):>16}") + print(f" {'HOLD banda a K=4 (6 rotazioni)':<44} {'[-0.01,+0.13]':>16} " + f"{'[%+.2f,%+.2f]' % (h4.min(), h4.max()):>16}") + print(f" {'maxDD mediano fra le singole':<44} {'12.6%':>16} {np.median(dd):>15.1%}") + print(f" {'turnover/anno del book':<44} {'~8.3':>16} " + f"{book_ideal(K4)['turn_y']:>16.1f}") + pk = float((h4[:, None] <= shh[0]).mean()) + print(f" {'P(K=4 <= h=0) sull hold-out':<44} {'0.18 n.s.':>16} {pk:>16.2f}") + print(" -> la STRUTTURA replica (K=4 == EW, turnover invariato, banda che si comprime);") + print(" i LIVELLI si sono mossi col dato. E' la deriva attesa, non un disaccordo.") + + # (d) il pavimento VERO del venue, derivato dal codice di produzione + print("\n (d) IL PAVIMENTO, derivato da `src/live/book` + `src/live/deribit` (non ridichiarato):") + import json as _json + cfg = _json.loads((ROOT / "config" / "live.json").read_text()) + mo = float(cfg["min_order_usd"]) + assert mo == MIN_ORDER_USD, f"config min_order_usd={mo} != {MIN_ORDER_USD} dichiarato qui" + steps = {} + for a in ASSETS: + inst = f"{a}_USDC-PERPETUAL" + steps[a] = float(_CONTRACT[inst]["step"]) + p_now = px(a)[-1] + print(f" {inst:<22} step {steps[a]:g} {a} -> a ${p_now:,.0f} il lotto vale " + f"${steps[a]*p_now:6.2f} (banda config ${mo:.0f})") + print(f" -> pavimento effettivo per ordine = max($5, quantizzazione al lotto).") + print(f" Su BTC il LOTTO e' piu' grosso della banda: e' lui a comandare.") + print(f" ⚠️ `build_book_order` banda `abs(delta)` sulla posizione NETTA e manda UN ordine") + print(f" per asset per giro. Questo e' il fatto che riapre la domanda del 02/07 (§4).") + print(f" {clock()}") + return dict(singles=singles, shf=shf, shi=shi, shh=shh, dd=dd, steps=steps) + + +# =========================================================================== +# §2 — (i) l'ensemble contro l'ancora MEDIANA +# =========================================================================== +def sezione2(S1: dict) -> dict: + sez("§2 — (i) L'ENSEMBLE CONTRO L'ANCORA MEDIANA (e l'algebra che lo governa)") + + # (a) il teorema A1, verificato numericamente sull'ORARIO + print(" (a) [A1] verifica ALGEBRICA sull'orario (non un'approssimazione: un'identita'):") + for a in ASSETS: + E24 = eval_ideal(a, HOURS) + g_mean = np.mean([eval_ideal(a, (h,))["gross"] for h in HOURS], axis=0) + t_mean = np.mean([eval_ideal(a, (h,))["turn"] for h in HOURS], axis=0) + dg = float(np.max(np.abs(E24["gross"] - g_mean))) + te, tm = float(E24["turn"].sum()), float(t_mean.sum()) + print(f" {a}: max|lordo_ens - media(lordi)| = {dg:.2e} -> IDENTITA'") + print(f" turnover TOTALE ensemble {te:.4f} vs MEDIA delle ancore {tm:.4f} " + f"-> sub-additivita' {'OK' if te <= tm + 1e-9 else 'VIOLATA'} " + f"({te/tm-1:+.3%})") + # ⚠️ ERRORE CATTURATO IN SESSIONE: la prima stesura confrontava `turnover_per_year`, che + # `al.eval_weights` ARROTONDA a 1 decimale -> stampava "9.40 vs 9.38" accanto a una + # disuguaglianza che il conto esatto rispetta. Una quantita' arrotondata non si mette + # accanto a una disuguaglianza stretta: sembra violata proprio dove e' piu' stretta. + print(" => l'ensemble NON e' una scommessa diversa: e' la STESSA scommessa, spalmata.") + print(" Il drift non puo' cambiare se non per il fee-netting; la vol si', perche' la") + print(" media di N serie correlate ma non identiche ha meno varianza di ciascuna.") + + # (b) la tabella delle famiglie + print("\n (b) FAMIGLIE (tutte le rotazioni, nessuna selezione; book 50/50 ideale):") + print(f" {'K':>3} {'rot':>4} {'ShFULL med':>11} {'sd':>6} {'ShHOLD med':>11} {'sd':>6} " + f"{'drift med':>10} {'vol med':>8} {'maxDD med':>10} {'turn/y':>7}") + fam = {} + for K, nrot in FAM_TP.items(): + rots = fam_rotations(K) + assert len(rots) == nrot + D = [book_ideal(hs) for hs in rots] + f = np.array([al._sh(d["daily"]) for d in D]) + h = np.array([al._sh(d["daily"][d["daily"].index >= HOLDOUT]) for d in D]) + dr = np.array([drift(d["daily"]) for d in D]) + vo = np.array([vol(d["daily"]) for d in D]) + ddv = np.array([al._dd_ret(d["daily"]) for d in D]) + ty = np.array([d["turn_y"] for d in D]) + fam[K] = dict(f=f, h=h, dr=dr, vo=vo, dd=ddv, ty=ty, rots=rots) + print(f" {K:>3} {nrot:>4} {np.median(f):>11.3f} {f.std():>6.3f} " + f"{np.median(h):>11.3f} {h.std():>6.3f} {np.median(dr):>9.2%} " + f"{np.median(vo):>7.2%} {np.median(ddv):>9.1%} {np.mean(ty):>7.1f}") + + # (c) il confronto che risponde a (i): ensemble pieno vs ancora mediana + e24 = book_ideal(HOURS)["daily"] + med_h = int(np.argsort(S1["shf"])[len(S1["shf"]) // 2]) + med = S1["singles"][med_h] + print(f"\n (c) RISPOSTA A (i) — ensemble K=24 contro l'ancora MEDIANA (h={med_h}, la mediana") + print(f" per Sharpe FULL fra le 24) e contro la MEDIA delle 24:") + print(f" {'':<26} {'ShFULL':>8} {'ShIS':>8} {'ShHOLD':>8} {'drift':>8} {'vol':>8} {'maxDD':>8}") + for lbl, s in (("ancora mediana", med), ("ensemble K=24", e24)): + a, b, c = m3(s) + print(f" {lbl:<26} {a:>8.3f} {b:>8.3f} {c:>8.3f} {drift(s):>7.2%} " + f"{vol(s):>7.2%} {al._dd_ret(s):>7.1%}") + da, db, dc = m3(e24) + ma, mb, mc = m3(med) + print(f" {'DELTA (ens - mediana)':<26} {da-ma:>+8.3f} {db-mb:>+8.3f} {dc-mc:>+8.3f} " + f"{drift(e24)-drift(med):>+7.2%} {vol(e24)-vol(med):>+7.2%} " + f"{al._dd_ret(e24)-al._dd_ret(med):>+7.1%}") + f1 = fam[1] + print(f"\n Scomposizione del guadagno (ensemble vs MEDIA delle 24, che e' il confronto") + print(f" algebricamente pulito): drift {drift(e24):.2%} vs media ancore " + f"{np.mean(f1['dr']):.2%} (delta {drift(e24)-np.mean(f1['dr']):+.2%} = fee-netting);") + print(f" vol {vol(e24):.2%} vs media ancore {np.mean(f1['vo']):.2%} " + f"({vol(e24)/np.mean(f1['vo'])-1:+.1%}) <- QUI sta tutto il guadagno di Sharpe.") + print(f"\n COMPRESSIONE DELLA DISPERSIONE (e' questo che l'ensemble compra):") + print(f" {'K':>3} {'sd(ShFULL)':>11} {'sd(ShHOLD)':>11} {'banda ShHOLD':>18} " + f"{'sd/sd(K=1)':>11}") + for K in FAM_TP: + d = fam[K] + print(f" {K:>3} {d['f'].std():>11.3f} {d['h'].std():>11.3f} " + f"{'[%+.2f,%+.2f]' % (d['h'].min(), d['h'].max()):>18} " + f"{(d['h'].std()/fam[1]['h'].std() if fam[1]['h'].std() else 0):>11.2f}") + print(f" (a K=24 la dispersione e' ZERO per definizione: c'e' una sola realizzazione —") + print(f" ed e' esattamente il punto: l'ensemble non riduce l'incertezza, la ELIMINA.)") + print(f" {clock()}") + return dict(fam=fam, e24=e24, med_h=med_h) + + +# =========================================================================== +# §3 — (ii) l'ensemble contro l'ancora CANONICA che gira oggi +# =========================================================================== +def sezione3(S1: dict, S2: dict) -> None: + sez("§3 — (ii) L'ENSEMBLE CONTRO L'ANCORA CANONICA (h=0 UTC, quella che gira con soldi veri)") + can = S1["singles"][0] + e24 = S2["e24"] + print(f" {'':<26} {'ShFULL':>8} {'ShIS':>8} {'ShHOLD':>8} {'drift':>8} {'vol':>8} {'maxDD':>8}") + for lbl, s in (("CANONICA h=0 (oggi)", can), ("ensemble K=24", e24)): + a, b, c = m3(s) + print(f" {lbl:<26} {a:>8.3f} {b:>8.3f} {c:>8.3f} {drift(s):>7.2%} " + f"{vol(s):>7.2%} {al._dd_ret(s):>7.1%}") + ca, cb, cc = m3(can); ea, eb, ec = m3(e24) + print(f" {'DELTA (ens - canonica)':<26} {ea-ca:>+8.3f} {eb-cb:>+8.3f} {ec-cc:>+8.3f} " + f"{drift(e24)-drift(can):>+7.2%} {vol(e24)-vol(can):>+7.2%} " + f"{al._dd_ret(e24)-al._dd_ret(can):>+7.1%}") + print(f"\n Percentile della CANONICA fra le 24 ancore: FULL {pctl_of(S1['shf'], S1['shf'][0]):.0f}°" + f" IS {pctl_of(S1['shi'], S1['shi'][0]):.0f}°" + f" HOLD {pctl_of(S1['shh'], S1['shh'][0]):.0f}°" + f" maxDD {pctl_of(S1['dd'], S1['dd'][0]):.0f}°") + print(f" Percentile dell'ENSEMBLE fra le 24: FULL {pctl_of(S1['shf'], ea):.0f}°" + f" IS {pctl_of(S1['shi'], eb):.0f}° HOLD {pctl_of(S1['shh'], ec):.0f}°") + print("\n 📌 LA LETTURA, ed e' il punto di tutto il filone:") + print(" i due numeri NON sono due stime della stessa cosa. Il numero della canonica e' il") + print(" risultato di UNA estrazione gia' avvenuta e gia' nota; quello dell'ensemble e' cio'") + print(" che si ottiene SENZA estrarre. Chi confronta 'canonica vs ensemble' in-sample sta") + print(" confrontando il massimo osservato con la media, e la media perde sempre.") + print(" La domanda decidibile e' un'altra: DOMANI, quale dei due si otterra'?") + print(f" - tenendo la canonica: una estrazione dalla banda [{S1['shh'].min():+.2f}," + f"{S1['shh'].max():+.2f}] di hold-out, con mediana {np.median(S1['shh']):+.2f}" + f" — il {pctl_of(S1['shh'], S1['shh'][0]):.0f}° pctl di ieri non si eredita;") + print(f" - tenendo l'ensemble: {ec:+.2f}, senza estrazione.") + print(" Cioe': l'ensemble non compra ritorno, compra il fatto di sapere gia' cosa avrai.") + print(" E' la stessa cosa che il 26/07 disse del limit-TP di SKH01: *converte una lotteria") + print(" in certezza piu' che aggiungere ritorno*.") + print(f" {clock()}") + + +# =========================================================================== +# §4 — (iii) ESEGUIBILITA': la curva N_max(capitale) +# =========================================================================== +def sezione4(S1: dict, S2: dict) -> dict: + sez("§4 — (iii) A QUALE CAPITALE L'ENSEMBLE DIVENTA ESEGUIBILE (pavimento vero del venue)") + + print(" (a) LA TAGLIA DEGLI ORDINI. Il fatto strutturale prima dei numeri:") + print(" il tranching NON moltiplica gli ordini per K. `build_book_order` banda la") + print(" posizione NETTA e ne manda UNO per asset per giro: K cambia la FORMA del") + print(" sentiero di target, non il numero di ordini. Il numero di ordini lo decide la BANDA.") + print(f"\n {'K':>3} {'cambi di target/anno':>21} {'|dNozionale| mediano a $635':>28} " + f"{'p75':>10} {'quota sopra $5':>15}") + capass = 635.0 * 0.5 + for K in (1, 2, 4, 12, 24): + hs = fam_rotations(K)[0] + n_chg = 0; alld = [] + for a in ASSETS: + t = tgt(a, hs) + d = np.abs(np.diff(t, prepend=0.0)) * capass + d = d[d > 1e-12] + n_chg += len(d); alld.append(d) + D = np.concatenate(alld) + yrs = (idx1h("BTC")[-1] - idx1h("BTC")[0]) / pd.Timedelta("365.25D") + print(f" {K:>3} {n_chg/yrs:>21,.0f} {np.median(D):>27.2f}$ {np.percentile(D,75):>9.2f}$ " + f"{float((D >= MIN_ORDER_USD).mean()):>14.1%}") + print(f"\n ⚠️ IL FATTO CHE DECIDE, e non e' su K: a $635 la quota TP01 di UN asset e'") + print(f" ${capass:.0f}, l'esposizione media di TP01 e' ~{np.mean(tgt('BTC',(0,))):.3f}x ->") + print(f" il nozionale TENUTO e' ~${capass*np.mean(tgt('BTC',(0,))):.0f}. La banda da $5 e'") + print(f" il {5/(capass*np.mean(tgt('BTC',(0,)))):.0%} della posizione, e il lotto BTC " + f"(${S1['steps']['BTC']*px('BTC')[-1]:.1f}) e' il " + f"{S1['steps']['BTC']*px('BTC')[-1]/(capass*np.mean(tgt('BTC',(0,)))):.0%}.") + print(f" Gia' K=1 esegue solo una minoranza dei propri ribilanci. Non c'e' K da degradare.") + + # (b) curva bandata + print("\n (b) IL PATH ESEGUITO, per capitale (banda $5 + quantizzazione al lotto del venue):") + print(f" {'cap':>8} {'K':>3} {'ShFULL':>8} {'haircut':>8} {'ordini/anno':>12} " + f"{'fee $/anno':>11} {'corr(eseguito,ideale)':>22}") + band = {} + for C in CAPITALI: + for K in (1, 2, 4, 24): + hs = fam_rotations(K)[0] + bb = book_banded(hs, C, S1["steps"]) + ii = book_ideal(hs)["daily"] + cr = [] + for a in ASSETS: + cr.append(float(np.corrcoef(bb["E"][a]["pos"], tgt(a, hs))[0, 1])) + band[(C, K)] = dict(sh=al._sh(bb["daily"]), hc=al._sh(ii) - al._sh(bb["daily"]), + ord_y=bb["ord_y"], fee_y=bb["fee_y"] * C, corr=float(np.mean(cr)), + daily=bb["daily"]) + b = band[(C, K)] + print(f" {C:>8,.0f} {K:>3} {b['sh']:>8.3f} {b['hc']:>+8.3f} {b['ord_y']:>12.0f} " + f"{b['fee_y']:>11.2f} {b['corr']:>22.4f}") + print(" (haircut = Sharpe ideale - Sharpe eseguito; fee $/anno = fee proporzionale sul") + print(" nozionale realmente scambiato, NON un costo per ordine — su Deribit non esiste)") + + # (c) il criterio N_max dichiarato in §0 + print("\n (c) N_max(capitale) — criterio DICHIARATO IN §0, applicato senza ritocchi:") + ideal_sd = {K: S2["fam"][K]["f"].std() for K in FAM_TP} + passed: dict = {} + print(f" {'cap':>8} " + " ".join(f"{'K='+str(K):>9}" for K in (2, 3, 4, 6, 8, 12)) + + f" {'N_max':>7}") + nmax = {} + for C in CAPITALI: + sd_b = {} + for K in (1, 2, 3, 4, 6, 8, 12): + v = [] + for hs in fam_rotations(K): + bb = book_banded(hs, C, S1["steps"]) + v.append(al._sh(bb["daily"])) + sd_b[K] = float(np.std(v)) + cells, best, npass = [], 1, 0 + for K in (2, 3, 4, 6, 8, 12): + den = ideal_sd[1] - ideal_sd[K] + ret_ = (sd_b[1] - sd_b[K]) / den if den > 1e-12 else 0.0 + cells.append(f"{ret_:>9.2f}") + if ret_ >= 0.50: + best = K + npass += 1 + nmax[C] = best + passed[C] = npass + print(f" {C:>8,.0f} " + " ".join(cells) + f" {best:>7}") + print(" (retention = quota della compressione IDEALE di dispersione che il path ESEGUITO") + print(" conserva; 1.00 = la banda non toglie niente, <=0 = il tranching e' invisibile)") + + print("\n ⚠️ IL CRITERIO SATURA, e va detto prima di leggere la colonna N_max.") + print(f" Celle che passano la soglia 0.50: " + + ", ".join(f"${c:,.0f} -> {passed[c]}/6" for c in CAPITALI) + ".") + print(" Un gate che un candidato qualsiasi passa 6 volte su 6 a OGNI capitale non sta") + print(" misurando niente (regola 07/08: un gate si valida contando quante volte lo passa") + print(" un candidato a caso). Il criterio era scritto per catturare un degrado che NON") + print(" ESISTE: la banda del venue non degrada il tranching nemmeno a $635.") + print(" -> [A5] REFUTATA nella sua seconda meta'. La risposta a (iii) non e' un capitale:") + print(" e' che l'eseguibilita' NON e' il vincolo. (7a volta nell'ondata.)") + + print("\n (d) IL VINCOLO OPERATIVO VERO, che non e' il pavimento ma il FEED.") + print(" TP01 all'ancora h decide sulla barra 24h che CHIUDE alle h:00 di oggi: per h>0") + print(" servono barre di oggi. Il feed certificato si ricostruisce una volta al giorno") + print(" (~00:30 UTC) -> 23 ancore su 24 richiedono il feed 5m FRESCO, cioe' `fresh_5m`,") + print(" lo stesso che il 26/07 e' risultato ricadere IN SILENZIO sul certificato e che") + print(" il 29/07 ha fallito 6 giri su 8 dopo un riavvio. Oggi quel guasto tocca il solo") + print(" SKH01 ed e' sorvegliato (`skh_feed_max_age_min`=30). Un ensemble d'ancora ci") + print(" metterebbe sotto anche TP01, che oggi non dipende da quel path.") + print(" Non e' un argomento definitivo (l'allerta esiste gia'), ma e' il costo VERO del") + print(" tranching su questo libro: non ordini, non fee — una dipendenza di feed in piu'.") + print(" E' l'obiezione (3) del 02/07, l'unica delle sue tre che sopravvive intatta.") + print(f" {clock()}") + return dict(band=band, nmax=nmax, passed=passed) + + +# =========================================================================== +# §5 — IL FUNDING: l'unico canale per cui l'ensemble potrebbe GUADAGNARE +# =========================================================================== +def sezione5(S1: dict) -> dict: + sez("§5 — IL FUNDING (l'unico canale di GUADAGNO possibile) — e perche' e' chiuso per algebra") + F = {} + for instr, (start, asset, kind) in fd.INSTR.items(): + d = fd.fetch_funding(instr, start).copy() + d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) + F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") + rate = fd.build_rate(F) + print(f" Tassi da `r0822d_funding` (cache su disco, nessuna rete). Copertura: " + f"{rate['BTC']['series'].index.min().date()} -> {rate['BTC']['series'].index.max().date()}") + + def f_hourly(a: str) -> np.ndarray: + """Tasso orario allineato all'indice 1h: il record etichettato T copre (T-1h, T].""" + ii = idx1h(a) + t_end = fd.to_ms(ii) + MSH + c = fd.cumF(rate[a], t_end) - fd.cumF(rate[a], fd.to_ms(ii)) + return c + + # (a) IL TEOREMA A4 + print("\n (a) [A4] il funding pagato in un'ora e' pos*f, LINEARE in pos:") + for a in ASSETS: + fh = f_hourly(a) + p24 = eval_ideal(a, HOURS)["pos"] + pm = np.mean([eval_ideal(a, (h,))["pos"] for h in HOURS], axis=0) + d = float(np.max(np.abs(p24 * fh - pm * fh))) + print(f" {a}: max|funding_ens - media(funding delle 24 ancore)| = {d:.2e} -> IDENTITA'") + print(" => l'ensemble NON puo' pagare meno funding della MEDIA delle ancore. Mai.") + print(" Il canale di guadagno sperato e' chiuso ALGEBRICAMENTE, non 'per piccolezza'.") + + # (b) il rapporto condizionale: si attenua mediando sulle 24 ore? + print("\n (b) e il rapporto CONDIZIONALE (22/08: esposizione e funding sono correlati, il") + print(" tasso pagato davvero e' 1,86-2,55x l'incondizionato per TP01) si attenua?") + print(f" {'K':>3} " + f"{'drag %/anno':>13} {'rapporto cond.':>16} " + f"{'corr(pos,f) oraria':>20}") + out = {} + cov = {a: (idx1h(a) >= rate[a]["series"].index.min()) for a in ASSETS} + for K in (1, 2, 4, 12, 24): + hs = fam_rotations(K)[0] + dr, rat, cc = [], [], [] + for a in ASSETS: + fh = f_hourly(a)[cov[a]] + p = eval_ideal(a, hs)["pos"][cov[a]] + paid = float(np.mean(p * fh)) * 24 * 365.25 + base = float(np.mean(p)) * float(np.mean(fh)) * 24 * 365.25 + dr.append(paid); rat.append(paid / base if base else np.nan) + cc.append(float(np.corrcoef(p, fh)[0, 1])) + out[K] = dict(drag=float(np.mean(dr)), ratio=float(np.mean(rat))) + print(f" {K:>3} {np.mean(dr):>12.3%} {np.mean(rat):>15.2f}x {np.mean(cc):>20.4f}") + print(" -> il rapporto e' ESSO STESSO una media di rapporti: mediare sull'ora del giorno") + print(" non tocca una correlazione che vive alla scala del REGIME (settimane/mesi).") + + # (c) replica del numero del 22/08 per una strada diversa + tp_noF, _ = fd.tp01_canonical(None) + tp_F, _ = fd.tp01_canonical(rate) + d22 = drift(tp_noF) - drift(tp_F) + p_h = eval_ideal("BTC", (0,))["pos"][cov["BTC"]] + print(f"\n (c) REPLICA del numero del 22/08 per una strada DIVERSA (griglia oraria invece che") + print(f" giornaliera, ancora canonica): drag di TP01 = {out[1]['drag']:.2%}/anno") + print(f" contro {d22:.2%}/anno rifacendo il conto con `fd.tp01_canonical` (griglia daily,") + print(f" lente del 22/08, che pubblico' 2,02%). Due path indipendenti, stesso ordine.") + + # (d) e sul BANDATO (dove la linearita' si rompe)? + print("\n (d) e sul path ESEGUITO (dove la banda rompe la linearita')?") + print(f" {'cap':>8} {'K':>3} {'drag %/anno':>13} {'vs ideale':>11}") + for C in (635.0, 5000.0, 50000.0): + for K in (1, 24): + hs = fam_rotations(K)[0] + dr = [] + for a in ASSETS: + e = eval_banded(a, hs, C * 0.5, S1["steps"][a]) + fh = f_hourly(a)[cov[a]] + dr.append(float(np.mean(e["pos"][cov[a]] * fh)) * 24 * 365.25) + print(f" {C:>8,.0f} {K:>3} {np.mean(dr):>12.3%} " + f"{np.mean(dr)-out[K]['drag']:>+10.3%}") + print(" -> la banda cambia il funding di briciole, e nel verso di chi tiene la posizione") + print(" piu' a lungo: non e' un canale, e' un residuo.") + print(f" {clock()}") + return dict(rate=rate, out=out, f_hourly=f_hourly, cov=cov) + + +# =========================================================================== +# §6 — SKH01: la stessa domanda su una griglia diversa, e piu' cara +# =========================================================================== +def skh_families() -> dict: + """Famiglie simmetriche per SKH01. Periodo congiunto 690m; griglia 5m -> 138 passi. + 138 = 2*3*23 -> K in {2,3,6,23} (e 46/69/138, fuori portata). Sulla griglia a 30m + dichiarata il 02/07 ci sono 23 offset e 23 e' PRIMO: o 1 o 23, niente in mezzo.""" + fams = {1: [(o,) for o in r02.OFFSETS]} + for K, shifts in ((2, range(0, 345, 30)), (3, range(0, 230, 30)), (6, range(0, 115, 30))): + cell = 690 // K + fams[K] = [tuple((s + cell * j) % 690 for j in range(K)) for s in shifts] + fams[23] = [tuple(r02.OFFSETS)] + return fams + + +_SKHD: dict = {} + + +def skh_daily(asset: str, off: int, lens: str) -> pd.Series: + """Serie daily di SKH01 a fase `off`. lens='canonical' (backtest) | 'hourly' (path del cron). + La cache pesante di `r02` viene SVUOTATA subito: 80+ offset x 2 asset di `ltf` non stanno + in memoria su questa macchina (2 core, ~3 GB liberi).""" + key = (asset, off, lens) + if key in _SKHD: + return _SKHD[key] + daily, m, ltf, ent = r02.run_asset(asset, off) + if lens == "canonical": + s = daily + else: + df5 = r02.get5m(asset) + ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5 + c5 = df5["close"].values.astype(float) + eq = r02.sim_equity(ltf, ent, "hourly", ts5, c5) + ss = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True))) + s = ss.resample("1D").last().ffill().pct_change().dropna() + _SKHD[key] = s + r02._CACHE.pop((asset, off), None) # evict: tiene solo il daily + return s + + +def skh_book(offs: tuple, lens: str) -> pd.Series: + """Book SKH01 50/50 BTC+ETH, K tranche = media dei ritorni giornalieri (stessa convenzione + con cui il progetto combina BTC/ETH e gli sleeve: ribilanciamento a peso costante).""" + per = [] + for off in offs: + J = pd.concat({a: skh_daily(a, off, lens) for a in ASSETS}, axis=1, join="inner").fillna(0.0) + per.append(pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index)) + JJ = pd.concat({i: s for i, s in enumerate(per)}, axis=1, join="inner").fillna(0.0) + return pd.Series(JJ.mean(axis=1).values, index=JJ.index) + + +def sezione6() -> dict: + sez("§6 — SKH01: la stessa domanda su una griglia diversa (e 23 e' un numero primo)") + from src.portfolio.sleeves import _skyhook_returns + ref = _skyhook_returns() + mine = skh_book((0,), "canonical") + d = float(np.max(np.abs(mine.values - ref.reindex(mine.index).values))) + print(f" [SANITY] offset 0 == `sleeves._skyhook_returns()`: max|dif| = {d:.2e} su " + f"{len(mine):,} giorni") + assert d < 1e-12 + + fams = skh_families() + print(f"\n [A6] 690/30 = 23, PRIMO -> sulla griglia a 30m del 02/07 non esiste sotto-ensemble") + print(f" simmetrico: o K=1 o K=23. Le famiglie K in {{2,3,6}} esistono solo sulla griglia") + print(f" 5m (138 = 2*3*23) e usano offset che il 02/07 non aveva mai valutato.") + print(f" ⚠️ K=3 = {{0,230,460}}: 230 e 460 sono multipli esatti del bin LTF -> la griglia") + print(f" d'ESECUZIONE 230m e' la STESSA, cambia solo la fase del segnale 690m. Non e' un") + print(f" tranching d'esecuzione: e' un ensemble di SEGNALE. Va detto, non nascosto.") + + res = {} + for lens in ("canonical", "hourly"): + print(f"\n LENTE '{lens}'" + (" (backtest, fill al livello)" if lens == "canonical" + else " (path del cron orario: fill al close 5m del prossimo multiplo orario)")) + print(f" {'K':>3} {'rot':>4} {'ShFULL med':>11} {'sd':>6} {'ShHOLD med':>11} {'sd':>6} " + f"{'drift med':>10} {'maxDD med':>10}") + R = {} + for K in (1, 2, 3, 6, 23): + S = [skh_book(o, lens) for o in fams[K]] + f = np.array([al._sh(s) for s in S]) + h = np.array([al._sh(s[s.index >= HOLDOUT]) for s in S]) + dr = np.array([drift(s) for s in S]) + dd = np.array([al._dd_ret(s) for s in S]) + R[K] = dict(f=f, h=h, dr=dr, dd=dd, S=S) + print(f" {K:>3} {len(S):>4} {np.median(f):>11.3f} {f.std():>6.3f} " + f"{np.median(h):>11.3f} {h.std():>6.3f} {np.median(dr):>9.2%} " + f"{np.median(dd):>9.1%}") + res[lens] = R + can = R[1]["f"][0] + print(f" canonico off=0: FULL {can:.3f} ({pctl_of(R[1]['f'], can):.0f}° pctl), " + f"HOLD {R[1]['h'][0]:.3f} ({pctl_of(R[1]['h'], R[1]['h'][0]):.0f}° pctl); " + f"ensemble K=23: FULL {R[23]['f'][0]:.3f}, HOLD {R[23]['h'][0]:.3f}") + print(f" {clock()}") + return res + + +# =========================================================================== +# §7 — I DUE SLEEVE INSIEME: il libro, e il netting che nessuno aveva contato +# =========================================================================== +def skh_pos_hourly(asset: str, off: int, ii: pd.DatetimeIndex) -> np.ndarray: + """Posizione oraria (+1/-1/0) di SKH01 a fase `off`, dalle boundary dei trade. + Riusa `fd.sim_equity_f` (che ritorna i trade con t_in/t_out in ms) — non riscrive il motore.""" + daily, m, ltf, ent = r02.run_asset(asset, off) + df5 = r02.get5m(asset) + ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5 + c5 = df5["close"].values.astype(float) + _, trades = fd.sim_equity_f(ltf, ent, "canonical", ts5, c5, None) + r02._CACHE.pop((asset, off), None) + t0 = fd.to_ms(ii) + p = np.zeros(len(ii)) + for tr in trades: + a = np.searchsorted(t0, tr["t_in"], side="left") + b = np.searchsorted(t0, tr["t_out"], side="right") + p[a:b] = tr["dir"] + return p + + +def sezione7(S1: dict, S4: dict, S6: dict) -> dict: + sez("§7 — I DUE SLEEVE INSIEME: il libro live, il NETTING, e dove il tranching vale davvero") + print(" Il libro esegue TP01 e SKH01 come UNA sola posizione netta per strumento (26/07, T1).") + print(" Quindi la banda da $5 non si applica al ribilancio di TP01 ma al NETTO — e i salti") + print(" grossi di SKH01 (che entra a 1,0x) trascinano dentro i micro-aggiustamenti di TP01,") + print(" gratis. Qui si incrociano le DUE griglie di tranching: TP01 (24 ancore orarie) e") + print(" SKH01 (23 fasi della griglia 230m/690m), che sono spazi di luck INDIPENDENTI.") + print(" ⚠️ LENTE: qui SKH01 e' mark-to-market ORARIO (posizione dai confini dei trade), non") + print(" compounding per trade come lo sleeve ufficiale -> i LIVELLI non sono confrontabili") + print(" coi numeri pubblicati. Si usano solo DIFFERENZE fra configurazioni, a lente identica.") + + ii = {a: idx1h(a) for a in ASSETS} + print(f" {clock()} costruzione delle 23 posizioni orarie di SKH01 (x2 asset)...") + SP = {a: {o: skh_pos_hourly(a, o, ii[a]) for o in r02.OFFSETS} for a in ASSETS} + sk = {1: {a: SP[a][0] for a in ASSETS}, + 23: {a: np.mean([SP[a][o] for o in r02.OFFSETS], axis=0) for a in ASSETS}} + + def run(ktp: int, ksk: int, C: float) -> dict: + hs = fam_rotations(ktp)[0] + nor, corr, nets, notion = 0, [], {}, 0.0 + for a in ASSETS: + tnet = W_ASSET * (W_TP * tgt(a, hs) + W_SKH * sk[ksk][a]) + b = banded(px(a), tnet, C, MIN_ORDER_USD, S1["steps"][a]) + held = b["held"] + pos = np.zeros(len(held)); pos[1:] = held[:-1] + net = pos * ret(a) - FEE * np.abs(np.diff(pos, prepend=0.0)); net[0] = 0.0 + nor += b["n_ord"]; notion += b["notional"] + corr.append(float(np.corrcoef(pos, tnet)[0, 1])) + nets[a] = pd.Series(net, index=ii[a]) + J = pd.concat(nets, axis=1, join="inner").fillna(0.0) + dly = al._to_daily(J[ASSETS[0]] + J[ASSETS[1]]) + yrs = (ii["BTC"][-1] - ii["BTC"][0]) / pd.Timedelta("365.25D") + return dict(ord_y=nor / yrs, corr=float(np.mean(corr)), sh=al._sh(dly), + hold=al._sh(dly[dly.index >= HOLDOUT]), dd=al._dd_ret(dly), + dly=dly, fee_y=FEE * notion / yrs) + + print(f"\n {'cap':>8} {'K_TP':>5} {'K_SKH':>6} {'ordini/anno':>12} {'fee $/anno':>11} " + f"{'fedelta':>9} {'ShFULL':>8} {'ShHOLD':>8} {'maxDD':>7}") + out = {} + for C in (635.0, 5000.0, 100000.0): + for ktp, ksk in ((1, 1), (24, 1), (1, 23), (24, 23)): + r = run(ktp, ksk, C) + out[(C, ktp, ksk)] = r + tag = " <- il libro di oggi" if (ktp, ksk) == (1, 1) else "" + print(f" {C:>8,.0f} {ktp:>5} {ksk:>6} {r['ord_y']:>12.0f} {r['fee_y']:>11.2f} " + f"{r['corr']:>9.4f} {r['sh']:>8.3f} {r['hold']:>8.3f} {r['dd']:>7.1%}{tag}") + print(" (fedelta' = corr fra la posizione ESEGUITA e il target ideale della stessa config)") + + print(f"\n 📌 LA SCOMPOSIZIONE CHE RISPONDE ALLA DOMANDA 'fallo sul LIBRO':") + add = {} + for C in (635.0, 5000.0, 100000.0): + d_tp = out[(C, 24, 1)]["sh"] - out[(C, 1, 1)]["sh"] + d_sk = out[(C, 1, 23)]["sh"] - out[(C, 1, 1)]["sh"] + d_both = out[(C, 24, 23)]["sh"] - out[(C, 1, 1)]["sh"] + add[C] = (d_tp, d_sk, d_both) + print(f" cap ${C:>8,.0f}: tranciare SOLO TP01 {d_tp:>+7.3f} · SOLO SKH01 {d_sk:>+7.3f} · " + f"ENTRAMBI {d_both:>+7.3f} (somma dei due singoli {d_tp+d_sk:>+7.3f}, " + f"scarto dall'additivita' {d_both-d_tp-d_sk:>+.3f})") + ok_add = all(abs(add[C][2] - add[C][0] - add[C][1]) < 0.01 for C in add) + print(f" -> additivita' dei due tranching: {'REGGE' if ok_add else 'NON regge a ogni capitale'}" + f" (scarto max {max(abs(add[C][2]-add[C][0]-add[C][1]) for C in add):.3f}).") + # ⚠️ ERRORE MIO, catturato rileggendo l'output: la prima stesura stampava "i due effetti sono + # additivi" come TESTO FISSO. A $635 non lo sono (i singoli sommano a un numero di segno + # opposto al congiunto). Ora l'affermazione la calcola il codice. Una conclusione scritta a + # mano accanto a una tabella e' una conclusione che la tabella non controlla. + print(f" ⚠️ E la TAGLIA e' rumore su questa lente: {abs(add[635.0][2]):.3f} a $635 e " + f"{abs(add[100000.0][2]):.3f} a $100k, con i segni dei singoli che cambiano fra i due") + print(f" capitali. Il numero grosso su SKH01 (§6) e' STANDALONE; dentro il libro pesa") + print(f" il 25% ed e' misurato su una lente diversa -> qui si legge 'non si distingue") + print(f" da zero', non 'il libro guadagna'.") + + print(f"\n 🚨 IL PREZZO VERO DEL TRANCHING SI VEDE SULL'HOLD-OUT, non sul FULL:") + for C in (635.0, 100000.0): + print(f" cap ${C:>8,.0f}: ShHOLD {out[(C,1,1)]['hold']:.3f} (oggi) -> " + f"{out[(C,24,1)]['hold']:.3f} (TP tranciato) -> {out[(C,1,23)]['hold']:.3f} " + f"(SKH tranciato) -> {out[(C,24,23)]['hold']:.3f} (entrambi)") + print(f" Non e' un difetto del tranching: e' la fortuna d'ancora che si restituisce.") + print(f" ENTRAMBE le ancore canoniche stanno all'estremo alto del loro hold-out (TP01 al") + print(f" 98° pctl di 24, SKH01 all'89° di 23) e il libro di oggi le somma. Un ensemble") + print(f" non puo' che perdere contro la somma di due massimi — ed e' esattamente perche'") + print(f" quel numero non e' l'aspettativa di domani.") + print(f" E il maxDD va nel verso opposto: {out[(635.,1,1)]['dd']:.1%} -> " + f"{out[(635.,24,23)]['dd']:.1%}. Rischio giu', hold-out fortunato giu'.") + + o1, o2 = out[(635., 1, 1)], out[(635., 24, 23)] + print(f"\n IL COSTO, a $635 — e sorprende: gli ORDINI salgono {o1['ord_y']:.0f} -> " + f"{o2['ord_y']:.0f}/anno ({o2['ord_y']/o1['ord_y']:.1f}x, " + f"{o2['ord_y']/365.25:.1f}/giorno su 24 giri orari),") + print(f" ma le FEE {'SCENDONO' if o2['fee_y'] < o1['fee_y'] else 'salgono'} " + f"${o1['fee_y']:.2f} -> ${o2['fee_y']:.2f}/anno " + f"({o1['fee_y']/635:.2%} -> {o2['fee_y']/635:.2%} di capitale).") + print(f" Non e' una contraddizione ed e' il fatto piu' istruttivo di questa sezione: su") + print(f" Deribit la fee e' PROPORZIONALE, quindi si paga il NOZIONALE, non gli ordini —") + print(f" e mediare 23 fasi di uno sleeve a trade discreti trasforma un +-1,0x che sbatte") + print(f" da un estremo all'altro in una posizione frazionaria che si muove poco. Piu'") + print(f" ordini, meno merce. (Su un venue a pavimento FISSO — IB, 25/07 — il segno si") + print(f" ribalterebbe: la stessa mossa costerebbe {o2['ord_y']/o1['ord_y']:.1f}x.)") + print(f" {clock()}") + return out + + +# =========================================================================== +# §8 — QUANTO VALE RIDURRE LA VARIANZA SOTTO UNA BARRIERA ASSORBENTE +# =========================================================================== +def barrier_pass(r: np.ndarray, k: float, up: float, dn: float, + starts: np.ndarray, maxlen: int = 365) -> tuple: + """P(pass) / P(breach) di una regola a barriera: dal saldo iniziale, +up = pass, + -dn = breach (max-loss STATICO, la regola vera: 25/07). Path close-only.""" + L = np.concatenate([[0.0], np.cumsum(np.log1p(np.clip(k * r, -0.99, None)))]) + a, b = np.log1p(up), np.log1p(-dn) + np_, nb, nu = 0, 0, 0 + for i in starts: + d = L[i + 1:i + 1 + maxlen] - L[i] + if len(d) < 5: + continue + hu = np.flatnonzero(d >= a) + hd = np.flatnonzero(d <= b) + ju = hu[0] if len(hu) else 10 ** 9 + jd = hd[0] if len(hd) else 10 ** 9 + if ju == jd == 10 ** 9: + nu += 1 + elif ju < jd: + np_ += 1 + else: + nb += 1 + tot = np_ + nb + nu + return (np_ / tot, nb / tot, nu / tot) if tot else (0.0, 0.0, 0.0) + + +def sezione8(S1: dict, S2: dict, S6: dict) -> dict: + sez("§8 — [A7] QUANTO VALE RIDURRE LA VARIANZA SOTTO UNA BARRIERA (il canale funded)") + print(" Su un conto proprio il nemico e' il drift e la varianza si sopporta. Su un conto") + print(" funded il vincolo binding e' la regola di DRAWDOWN (25/07 §4): li' la varianza E'") + print(" il nemico — e l'ensemble compra esattamente quello. Questa e' l'attesa [A7]; segue") + print(" la misura, che la refuta sulla meta' che si puo' misurare.") + print(" Regole HyroTrader modellate (`r0725_hyro`: EV_TARGET/EV_DD/EV_DL = 0.10/0.06/0.04):") + print(" target +10%, max-loss STATICO -6% dal saldo iniziale.") + print(" ⚠️ LIMITE DICHIARATO, ed e' quello che conta: la regola di DAILY-LOSS (-4%) NON e'") + print(" valutabile qui. Su una regola a UN giorno la lente close-only e' CIECA (22/08:") + print(" 0,00 breach/anno contro 0,40-1,21 veri, rapporto INF) e servirebbe la lente wick") + print(" ACCOPPIATA (`r0725_prop_coupled`), fuori portata di questo script. Il max-loss e'") + print(" multi-giorno: li' close-only e' ottimista di un ricarico ~3,5%, non cieca.") + + skhL = S6["canonical"] + skh_off = skhL[1]["S"] + skh_ens = skhL[23]["S"][0] + tp_h = S1["singles"] + tp_ens = S2["e24"] + + def book(tp: pd.Series, skh: pd.Series) -> pd.Series: + return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": W_TP, "SKH01": W_SKH}, + lo=pd.Timestamp("2019-03-14", tz="UTC")) + + rng = np.random.default_rng(20260823) + pairs = [(int(h), int(o)) for h, o in zip(rng.integers(0, 24, 150), rng.integers(0, 23, 150))] + print(f"\n Spazio congiunto 24 ancore TP01 x 23 fasi SKH01 = 552 configurazioni; 150 estrazioni") + print(f" uniformi indipendenti (seme dichiarato) + l'ensemble congiunto + la canonica (0,0).") + print(f" {'leva':>5} {'':<24} {'P(pass)':>9} {'P(breach)':>10} {'P(aperto)':>10} {'maxDD':>8}") + res = {} + BK = {} + for (h, o) in pairs: + BK[(h, o)] = book(tp_h[h], skh_off[o]) + BE = book(tp_ens, skh_ens) + BC = book(tp_h[0], skh_off[0]) + for k in (0.50, 0.75, 1.00): + rows = [] + for (h, o) in pairs: + s_ = BK[(h, o)]; r = s_.values.astype(float) + st = np.arange(0, len(r) - 380, 15) + rows.append(barrier_pass(r, k, 0.10, 0.06, st) + (al._dd_ret(s_),)) + A = np.array(rows) + rr = BE.values.astype(float); st = np.arange(0, len(rr) - 380, 15) + pe = barrier_pass(rr, k, 0.10, 0.06, st) + (al._dd_ret(BE),) + rc = BC.values.astype(float); stc = np.arange(0, len(rc) - 380, 15) + pc = barrier_pass(rc, k, 0.10, 0.06, stc) + (al._dd_ret(BC),) + res[k] = dict(A=A, ens=pe, can=pc) + print(f" {k:>5.2f} {'CANONICA (0,0) — oggi':<24} {pc[0]:>9.1%} {pc[1]:>10.1%} " + f"{pc[2]:>10.1%} {pc[3]:>8.1%}") + print(f" {'':>5} {'mediana 150 estrazioni':<24} {np.median(A[:,0]):>9.1%} " + f"{np.median(A[:,1]):>10.1%} {np.median(A[:,2]):>10.1%} {np.median(A[:,3]):>8.1%}") + print(f" {'':>5} {'banda p10-p90':<24} " + f"{'[%.0f%%,%.0f%%]' % (np.percentile(A[:,0],10)*100, np.percentile(A[:,0],90)*100):>9} " + f"{'[%.1f%%,%.1f%%]' % (np.percentile(A[:,1],10)*100, np.percentile(A[:,1],90)*100):>10} " + f"{'':>10} " + f"{'[%.1f,%.1f]' % (np.percentile(A[:,3],10)*100, np.percentile(A[:,3],90)*100):>8}") + print(f" {'':>5} {'ENSEMBLE congiunto':<24} {pe[0]:>9.1%} {pe[1]:>10.1%} " + f"{pe[2]:>10.1%} {pe[3]:>8.1%} (pctl fra le estrazioni: " + f"{pctl_of(A[:,0], pe[0]):.0f}° su P(pass), {pctl_of(-A[:,1], -pe[1]):.0f}° su " + f"P(breach), {pctl_of(-A[:,3], -pe[3]):.0f}° su maxDD)") + print() + + print(" 🚨 [A7] REFUTATA sulla meta' misurabile, E CON UN MECCANISMO — non e' rumore.") + print(" Su queste regole il lato BINDING non e' la barriera, e' il BERSAGLIO: P(breach)") + print(f" sta fra {res[0.50]['ens'][1]:.1%} e {res[1.00]['ens'][1]:.1%} mentre P(pass) sta") + print(f" fra {res[0.50]['ens'][0]:.0%} e {res[1.00]['ens'][0]:.0%}. Se il vincolo e'") + print(" ARRIVARE a +10% e non evitare -6%, ridurre la varianza RALLENTA: e' la stessa") + print(" meno-vol che tiene lontani dalla barriera a tenere lontani dal traguardo.") + print(" L'ensemble infatti sta al 30-50° pctl su P(pass) — sotto la mediana — pur avendo") + print(" un maxDD MIGLIORE della mediana. Le due cose non sono in contraddizione: sono la") + print(" stessa cosa vista dai due lati.") + print(" ⚠️ E la regola che il progetto ha MISURATO essere quella che uccide — la") + print(" daily-loss a UN giorno (22/08) — e' esattamente quella che questa lente non puo'") + print(" vedere. Quindi: A7 non e' refutata, e' refutata SULLA REGOLA SBAGLIATA. Dirlo e'") + print(" il risultato; concludere 'l'ensemble non serve sul funded' NON lo sarebbe.") + + # --- proxy POTENTE per la regola a un giorno: la coda sinistra giornaliera ----------- + print("\n (b) L'UNICA cosa con potenza che si puo' dire sulla regola a un giorno: la CODA") + print(" SINISTRA giornaliera. La daily-loss legge il MINIMO del giorno; il close-only ne") + print(" e' un limite inferiore (rapporto mediano minimo/chiusura misurato 1,89x il 22/08).") + print(" Non e' una stima del breach — e' il confronto FRA configurazioni sulla grandezza") + print(" che quella regola legge, a lente identica.") + W = np.array([[float(np.min(BK[p].values)), float(np.percentile(BK[p].values, 1)), + float(np.mean(BK[p].values < -0.02))] for p in pairs]) + we = [float(np.min(BE.values)), float(np.percentile(BE.values, 1)), + float(np.mean(BE.values < -0.02))] + wc = [float(np.min(BC.values)), float(np.percentile(BC.values, 1)), + float(np.mean(BC.values < -0.02))] + print(f"\n {'':<24} {'peggior giorno':>15} {'p1 giornaliero':>15} {'giorni < -2%':>14}") + print(f" {'CANONICA (0,0) — oggi':<24} {wc[0]:>14.2%} {wc[1]:>14.2%} {wc[2]:>13.2%}") + print(f" {'mediana 150 estrazioni':<24} {np.median(W[:,0]):>14.2%} " + f"{np.median(W[:,1]):>14.2%} {np.median(W[:,2]):>13.2%}") + print(f" {'banda p10-p90':<24} " + f"{'[%.2f%%,%.2f%%]' % (np.percentile(W[:,0],10)*100, np.percentile(W[:,0],90)*100):>15} " + f"{'[%.2f%%,%.2f%%]' % (np.percentile(W[:,1],10)*100, np.percentile(W[:,1],90)*100):>15} " + f"{'[%.2f%%,%.2f%%]' % (np.percentile(W[:,2],10)*100, np.percentile(W[:,2],90)*100):>14}") + q = (pctl_of(W[:, 0], we[0]), pctl_of(W[:, 1], we[1]), pctl_of(-W[:, 2], -we[2])) + print(f" {'ENSEMBLE congiunto':<24} {we[0]:>14.2%} {we[1]:>14.2%} {we[2]:>13.2%}" + f" (pctl: {q[0]:.0f}° / {q[1]:.0f}° / {q[2]:.0f}° — " + f"= quota di configurazioni che l'ensemble BATTE)") + # ⚠️ ERRORE MIO, catturato rileggendo l'output: la prima stesura calcolava questi tre + # percentili con `pctl_of(-W, -we)` per tutte e tre le colonne. Sulle prime due (che sono + # RITORNI, negativi, dove "piu' alto = meglio") quel segno da' il COMPLEMENTO: stampava 34° + # dove il valore vero e' 66°, cioe' diceva "peggio della mediana" di un numero che e' + # meglio della mediana. La terza colonna (una FREQUENZA, dove "piu' basso = meglio") aveva + # bisogno del segno invertito ed era l'unica giusta. Tre colonne, due convenzioni. + print(f" -> l'ensemble batte la mediana su TUTTE E TRE le colonne, ma di POCO " + f"({q[0]:.0f}°/{q[1]:.0f}°/{q[2]:.0f}° pctl): comprime la coda, non la annulla.") + print(" E la banda p10-p90 del p1 e' strettissima -> su questa grandezza le 552") + print(" configurazioni si somigliano molto piu' che sullo Sharpe.") + print(" Resta un INDIZIO, non una misura del breach: la conferma richiede la lente") + print(" accoppiata, ed e' un follow-up con un costo noto, non un ostacolo.") + print("\n 📌 E il fatto che chiude il cerchio sull'eseguibilita': il conto funded e' da") + print(" $100.000, dove il pavimento del venue e' invisibile (§7 lo misura). Il tranching") + print(" e' ineseguibile in nessun posto — e utile solo dove la coda conta.") + print(f" {clock()}") + return dict(res=res, tail=dict(W=W, ens=we, can=wc)) + + +# =========================================================================== +# §9 — VERDETTO +# =========================================================================== +def sezione9(S1, S2, S4, S5, S6, S7, S8) -> None: + sez("§9 — VERDETTO SULLE ATTESE A PRIORI (derivato dai numeri, non scritto a mano)") + can = S1["singles"][0]; e24 = S2["e24"] + ca, cb, cc = m3(can); ea, eb, ec = m3(e24) + med = S1["singles"][S2["med_h"]] + ma, mb, mc = m3(med) + f1 = S2["fam"][1] + d_med = ea - ma + # A1: identita' del lordo, ri-verificata qui (cached, costo ~0) + id_max = max(float(np.max(np.abs(eval_ideal(a, HOURS)["gross"] - + np.mean([eval_ideal(a, (h,))["gross"] for h in HOURS], axis=0)))) + for a in ASSETS) + a1 = "CONFERMATA" if id_max < 1e-15 else "REFUTATA" + a2 = ("CONFERMATA" if 0.03 <= d_med <= 0.15 else + ("DIREZIONE OK, TAGLIA REFUTATA" if d_med > 0 else "REFUTATA")) + a3 = "CONFERMATA" if ec < cc else "REFUTATA" + r1, r24 = S5["out"][1]["ratio"], S5["out"][24]["ratio"] + a4 = "CONFERMATA" if abs(r24 - r1) < 0.05 else "REFUTATA" + nm = S4["nmax"]; pas = S4["passed"] + a5 = "REFUTATA" if (nm[635.0] > 1 or min(pas.values()) == 6) else "CONFERMATA" + a6 = "CONFERMATA (aritmetica: 23 e' primo)" + p_ens = pctl_of(S8["res"][1.00]["A"][:, 0], S8["res"][1.00]["ens"][0]) + # convenzione: il p1 e' un RITORNO (negativo), quindi "piu' alto = meglio" e il percentile + # si calcola SENZA invertire il segno. Invertirlo da' il complemento (34° invece di 66°) — + # errore commesso nella prima stesura di §8 e corretto li': va corretto anche qui, o le due + # sezioni stampano due numeri diversi per la stessa grandezza. + p_tail = pctl_of(S8["tail"]["W"][:, 1], S8["tail"]["ens"][1]) + a7 = "REFUTATA sulla regola misurabile" if p_ens < 50 else "CONFERMATA" + ver = [ + ("A1", a1, f"identita' del lordo verificata a max|dif| = {id_max:.1e}; turnover " + f"sub-additivo sul conto esatto."), + ("A2", a2, f"dSharpe FULL vs ancora mediana {d_med:+.3f} (previsto +0,03..+0,15): " + f"il SEGNO e' giusto, la TAGLIA e' {0.09/max(d_med,1e-9):.0f}x piu' piccola del " + f"centro della mia banda. Avevo sopravvalutato quanto sono decorrelate fra loro " + f"24 ancore della STESSA strategia."), + ("A3", a3, f"dSharpe HOLD vs canonica {ec-cc:+.3f}; la canonica sta al " + f"{pctl_of(S1['shh'], S1['shh'][0]):.0f}° pctl delle 24 sull'hold-out e al " + f"{pctl_of(S1['shi'], S1['shi'][0]):.0f}° in-sample: SFORTUNATA in-sample e FORTUNATA " + f"sull'hold-out, esattamente la struttura che il 02/07 aveva descritto."), + ("A4", a4, f"funding dell'ensemble ESATTAMENTE la media (max|dif| = 0) e rapporto " + f"condizionale immobile ({r1:.2f}x a K=1 -> {r24:.2f}x a K=24)."), + ("A5", a5, f"il pavimento NON degrada il tranching: retention {min(pas.values())}/6 celle " + f"sopra soglia a OGNI capitale, N_max($635) = {nm[635.0]} invece di 1. " + f"Il criterio SATURA -> non e' un N_max, e' un 'il vincolo non e' questo'."), + ("A6", a6, "e le famiglie simmetriche di SKH01 vivono solo sulla griglia 5m; K=3 e' un " + "ensemble di SEGNALE, non d'esecuzione."), + ("A7", a7, f"l'ensemble sta al {p_ens:.0f}° pctl su P(pass) a leva 1,0x — SOTTO la mediana — " + f"perche' su queste regole il lato binding e' il BERSAGLIO (+10%), non la " + f"barriera. Ma sul p1 giornaliero, che e' cio' che la regola killer legge, sta " + f"al {p_tail:.0f}° pctl (batte quella quota di configurazioni). La refutazione e' " + f"sulla regola che si puo' misurare, non su quella che uccide: dichiarato."), + ] + for k, v, t in ver: + print(f" [{k}] {v}") + print(f" {t}\n") + + sez("§9b — LE TRE RISPOSTE") + print(f" (i) L'ensemble batte l'ancora MEDIANA? SI su TP01, ma di BRICIOLE: " + f"dShFULL {d_med:+.3f}, dShHOLD {ec-mc:+.3f}, dDrift {drift(e24)-drift(med):+.2%}.") + print(f" Scomposto contro la MEDIA delle 24 (il confronto algebricamente pulito): il") + print(f" drift si muove di {drift(e24)-np.mean(f1['dr']):+.3%} (solo fee-netting, e la") + print(f" linearita' dice che non puo' fare altro) e la vol di " + f"{vol(e24)/np.mean(f1['vo'])-1:+.1%}. Tutto il guadagno e' vol.") + print(f" Cio' che l'ensemble compra DAVVERO non e' nella colonna Sharpe: e' la") + print(f" dispersione, sd(ShFULL) {f1['f'].std():.3f} -> 0 e sd(ShHOLD) " + f"{f1['h'].std():.3f} -> 0.") + skhc, skhh = S6["canonical"], S6["hourly"] + print(f"\n ⚠️ E su SKH01 la risposta e' DIVERSA di un ordine di grandezza: sulla lente") + print(f" del path che gira (hourly) la mediana delle 23 fasi fa ShFULL " + f"{np.median(skhh[1]['f']):.3f} e l'ensemble K=23 fa {skhh[23]['f'][0]:.3f} " + f"({skhh[23]['f'][0]-np.median(skhh[1]['f']):+.3f}),") + print(f" con maxDD mediano {np.median(skhh[1]['dd']):.1%} -> {skhh[23]['dd'][0]:.1%}. " + f"Su TP01 il tranching e' rumore; su SKH01 e' il grosso.") + print(f" Meccanismo: la dispersione d'ancora di SKH01 e' 3x quella di TP01 " + f"(sd {skhh[1]['f'].std():.3f} vs {f1['f'].std():.3f}) perche' i suoi trade sono") + print(f" DISCRETI — spostare la griglia cambia QUALI trade esistono, non di quanto sono") + print(f" grossi. Il tranching vale quanto la lumpiness che elimina.") + print(f" ⚠️ MA quel numero e' STANDALONE. Dentro il libro SKH01 pesa il 25% e il §7 lo") + print(f" misura su un'altra lente: li' il tranching congiunto vale " + f"{S7[(635.0,24,23)]['sh']-S7[(635.0,1,1)]['sh']:+.3f} a $635 e " + f"{S7[(100000.0,24,23)]['sh']-S7[(100000.0,1,1)]['sh']:+.3f} a $100k, coi segni dei") + print(f" singoli che cambiano fra i due capitali = non distinguibile da zero.") + print(f" La lettura onesta: il tranching di SKH01 e' l'unico posto dove c'e' un segnale,") + print(f" e non e' abbastanza grande da vedersi attraverso un peso del 25%.") + + print(f"\n (ii) L'ensemble batte la CANONICA che gira oggi? NO sull'hold-out " + f"({ec-cc:+.3f}), SI su FULL ({ea-ca:+.3f}) e su IS ({eb-cb:+.3f}).") + print(f" E i due numeri NON sono due stime della stessa cosa. Il {cc:+.2f} della canonica") + print(f" e' una estrazione GIA' AVVENUTA (98° pctl di 24); il {ec:+.2f} dell'ensemble e'") + print(f" cio' che si ottiene SENZA estrarre.") + print(f" 📌 LA STIMA ONESTA DEL FUTURO E' L'ENSEMBLE: {ec:+.2f}, non {cc:+.2f}.") + print(f" Tenere la canonica non conserva il {cc:+.2f} — ri-estrae dalla banda " + f"[{S1['shh'].min():+.2f},{S1['shh'].max():+.2f}], mediana {np.median(S1['shh']):+.2f}.") + print(f" (E questi numeri riproducono la memoria del progetto: canonico +0,44 al " + f"{pctl_of(S1['shh'], S1['shh'][0]):.0f}° pctl, mediana +{np.median(S1['shh']):.2f}.)") + + print(f"\n (iii) A quale capitale diventa eseguibile senza degenerare? A TUTTI, gia' a $635 —") + print(f" e questo REFUTA la mia attesa e la premessa (non la conclusione) del 02/07.") + print(f" A $635 il path eseguito a K=24 segue il target ideale con corr " + f"{S4['band'][(635.0,24)]['corr']:.4f} contro {S4['band'][(635.0,1)]['corr']:.4f} di K=1:") + print(f" il tranching e' seguito MEGLIO, non peggio, perche' un target che si muove a") + print(f" passi piccoli e frequenti entra in una banda fissa meglio di uno che salta.") + print(f" Ordini: {S4['band'][(635.0,1)]['ord_y']:.0f}/anno -> " + f"{S4['band'][(635.0,24)]['ord_y']:.0f}/anno, fee " + f"${S4['band'][(635.0,1)]['fee_y']:.2f} -> ${S4['band'][(635.0,24)]['fee_y']:.2f}/anno.") + + sez("§9c — IL RISULTATO CHE NON ERA NEL MANDATO, e cosa NON cambia") + print(" 📌 LA PREMESSA DEL 02/07 E' SBAGLIATA; LA SUA CONCLUSIONE REGGE PER UN'ALTRA RAGIONE.") + print(" Il 02/07 boccio' il tranching cosi': *'a $225/asset di quota TP01 i delta") + print(" per-ancora (~$1-2) sono sotto il min-order $5 -> degenererebbe comunque in K=1'*.") + print(" Ma `src/live/book.build_book_order` non manda mai un ordine PER ANCORA: banda") + print(" `abs(delta)` sulla posizione NETTA e ne manda UNO per asset per giro. Un 'delta") + print(" per-ancora' non e' un ordine, e' un pezzo di aritmetica dentro il target. Il") + print(" tranching non moltiplica gli ordini per K, e infatti la degenerazione NON avviene") + print(" (misurata: retention ~0,9-1,0 a $635).") + print(" Cio' che boccia il tranching di TP01 non e' l'esecuzione, e' che NON C'E' NIENTE") + print(" DA PRENDERE: +0,014 di Sharpe. La decisione del 02/07 resta, la sua ragione no.") + print(" E la ragione conta: quella del 02/07 sarebbe caduta al primo esecutore che mandasse") + print(" un ordine per tranche; questa non cade mai, perche' e' sulla taglia dell'effetto.") + print("\n 🚨 E IL FILONE HA TROVATO IL SUO CONTRARIO: l'unica ancora che il 02/07 NON aveva") + print(" esaminato — la griglia 230m/690m di SKH01 — e' quella dove il tranching vale.") + print(" Il 02/07 raccomando' l'audit su SKH01 (fatto il 02/07-bis) ma non ci porto' MAI") + print(" la domanda del tranching, che era nata su TP01.") + print("\n ⚠️ COSA COSTA DAVVERO, e non e' negli ordini: il feed. 23 ancore su 24 di TP01") + print(" richiedono barre di oggi, cioe' `fresh_5m` — lo stesso path che il 26/07 e'") + print(" risultato ricadere IN SILENZIO sul certificato e che il 29/07 ha fallito 6 giri") + print(" su 8. Oggi TP01 non dipende da quel path; con l'ensemble ci dipenderebbe.") + print(" E' l'unica delle tre obiezioni del 02/07 che sopravvive intatta.") + print(" (SKH01 quel feed lo usa GIA': il suo tranching non aggiunge dipendenze nuove —") + print(" il che rende l'asimmetria fra i due sleeve ancora piu' netta.)") + print("\n NIENTE CAMBIA nel libro live: pesi, ancore, cron, config INVARIATI. Il tranching non") + print(" e' un cambio di pesi (`weights_tilt_null` non si applica) ma e' un cambio di") + print(" ESECUZIONE: vale l'asticella del 26/07 (T1), il beneficio si confronta col path che") + print(" gira davvero. Non e' proposto nulla: SKH01 non e' passato per `study_family_honest`") + print(" su questa griglia, il guadagno e' misurato sugli STESSI 7 anni su cui SKH01 e' stato") + print(" selezionato, e la lente 'hourly' e' gia' nota (26/07) per sottostimare il path live.") + print(" Cio' che questo filone stabilisce e' DOVE guardare, non cosa fare.") + print(f"\n {clock()} fine.") + + +def main() -> None: + print("=" * 100) + print(" r0823_anchor_ensemble.py — §47 ANCHOR-ENSEMBLE") + print(f" fee {2*FEE:.2%} RT · HOLD-OUT >= {HOLDOUT.date()} · min_order ${MIN_ORDER_USD:.0f} · " + f"libro TP01 {W_TP:.0%} / SKH01 {W_SKH:.0%}") + print("=" * 100) + sezione0() + S1 = sezione1() + S2 = sezione2(S1) + sezione3(S1, S2) + S4 = sezione4(S1, S2) + S5 = sezione5(S1) + S6 = sezione6() + S7 = sezione7(S1, S4, S6) + S8 = sezione8(S1, S2, S6) + sezione9(S1, S2, S4, S5, S6, S7, S8) + + +if __name__ == "__main__": + main()