research(wave-0822): TP01-SINISTRO — l'allarme e' falsificato nel verso; il rischio dell'ottimo funded e' XS01, non TP01

This commit is contained in:
Adriano Dal Pastro
2026-08-22 21:07:19 +00:00
parent 93ce12931a
commit 82ccff2553
5 changed files with 1430 additions and 67 deletions
+96 -40
View File
@@ -62,6 +62,7 @@ USO: nice -n 19 timeout 900 uv run python scripts/research/r0822b_bin_freq.py [
from __future__ import annotations
import argparse
import gc
import sys
import time
from pathlib import Path
@@ -132,6 +133,10 @@ class KeepCache:
self.rv: dict = {}
self.q: dict = {}
def clear(self) -> None:
self.rv.clear()
self.q.clear()
def keep(self, ex: dict, key, thr, w: int, h: float, sg: int) -> pd.Series:
base = ex["base_daily"]
k = (key, w)
@@ -152,6 +157,18 @@ class KeepCache:
KC = KeepCache()
def rss_mb() -> float:
"""RSS in MB, per non far morire il lavoro degli altri agenti (brief §8: 2 core, ~3 GB)."""
try:
with open("/proc/self/status") as f:
for ln in f:
if ln.startswith("VmRSS:"):
return float(ln.split()[1]) / 1024.0
except Exception:
pass
return float("nan")
def size_bin(thr, w, h, sg):
def f(ex, key):
return V.sizes_from_L(ex, KC.keep(ex, key, thr, w, h, sg))
@@ -175,7 +192,7 @@ def iso_pair(TP: pd.Series, leg: pd.Series, leg_base: pd.Series):
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--every", type=int, default=1)
ap.add_argument("--ndraw", type=int, default=200)
ap.add_argument("--ndraw", type=int, default=120)
args = ap.parse_args()
offs = OFFSETS_FULL[:: max(1, args.every)]
t0 = time.time()
@@ -208,8 +225,10 @@ def main() -> None:
print("\n estrazione tabelle trade (V2-DD) ...", flush=True)
EX = {o: {a: V.extract(a, o) for a in ASSETS} for o in offs}
ntr = {a: int(np.mean([len(EX[o][a]["i_ent"]) for o in offs])) for a in ASSETS}
R.get5m.cache_clear() # ~90 MB di 5m non servono durante la §4 (la VPS ha 2 core
gc.collect() # e ~3 GB liberi condivisi); si ricaricano in §9b in ~3 s
print(f" fatto in {time.time()-t0:.0f}s — trade medi/ancora: " +
", ".join(f"{a} {ntr[a]}" for a in ASSETS))
", ".join(f"{a} {ntr[a]}" for a in ASSETS) + f" (RSS {rss_mb():.0f} MB)")
TP = A.tp01_baseline_daily()
can = offs[0]
@@ -299,52 +318,71 @@ def main() -> None:
print(" 4. LA FAMIGLIA INTERA — ISO dShFULL, mediana delle differenze appaiate su "
f"{len(offs)} ancore")
print("-" * L)
# DUE PASSATE, per la RAM (7 GB sulla VPS, ~3 liberi, e siamo in tanti): la 1a calcola le
# metriche per-ancora di tutte le 144 celle SENZA conservare le serie (144 x 23 x 3 serie da
# 2717 punti sarebbero ~500 MB); la 2a ricalcola e CONSERVA le serie delle sole 3-4 celle che
# servono dopo (canonica / scelta al buio / argmax): 4 x 23 gambe, ~2 s.
# RAM (brief §8: 7 GB totali, ~3 liberi, e siamo in tanti). Tre accorgimenti, tutti
# necessari — la prima stesura di questo script e' stata UCCISA a meta' della §4:
# (a) DUE PASSATE: la 1a calcola le metriche per-ancora di tutte le 144 celle SENZA
# conservare le serie (144 x 23 x 3 serie da 2717 punti = ~500 MB); la 2a ricalcola e
# conserva le serie delle sole 3-4 celle che servono dopo (~2 s);
# (b) ciclo ANCORA-ESTERNO con la cache di rv/quantili SVUOTATA a ogni ancora: viva una sola
# ancora alla volta (72 serie invece di 1.656);
# (c) RSS stampata, cosi' se qualcuno la rilancia sa cosa aspettarsi.
BK, CT, LEG, MS = {}, {}, {}, {}
ACC: dict = {}
def eval_cell(thr, w, h, sg, store: bool = False) -> dict:
def _one(o, thr, w, h, sg, store: bool):
"""UNA cella a UNA ancora. Le size si calcolano una volta sola e si riusano (la prima
stesura le ricalcolava per il conteggio dei tenuti); la scomposizione per anno e il maxDD
si fanno SOLO nella seconda passata, perche' servono a 3 celle e costavano il 60% del
tempo su tutte e 144."""
nm = cname(thr, w, h, sg)
fn = size_bin(thr, w, h, sg)
dF, dH, dI, lF, lI, lH, ms, kp, ddv, ddc, dC_ = ([] for _ in range(11))
yr: dict = {}
for o in offs:
lg = leg_of(EX[o], o, fn)
b, c, m = iso_pair(TP, lg, base_leg[o])
dF.append(sh(b) - sh(c)); dH.append(sh(hold(b)) - sh(hold(c)))
dI.append(sh(ins(b)) - sh(ins(c))); dC_.append(cagr(b) - cagr(c))
lF.append(sh(lg)); lI.append(sh(ins(lg))); lH.append(sh(hold(lg)))
ms.append(m); ddv.append(maxdd(b)); ddc.append(maxdd(c))
kp.append(float(np.mean(np.concatenate([fn(EX[o][a], (o, a)) for a in ASSETS]))))
szs = {a: fn(EX[o][a], (o, a)) for a in ASSETS}
lg = V.leg_daily(EX[o], szs)
b, c, m = iso_pair(TP, lg, base_leg[o])
d = ACC.setdefault(nm, dict(thr=thr, w=w, h=h, sg=sg, dF=[], dH=[], dI=[], dC=[],
lF=[], lI=[], lH=[], ms=[], kp=[], ddv=[], ddc=[], yr={}))
d["dF"].append(sh(b) - sh(c)); d["dH"].append(sh(hold(b)) - sh(hold(c)))
d["dI"].append(sh(ins(b)) - sh(ins(c)))
d["lF"].append(sh(lg)); d["lI"].append(sh(ins(lg))); d["lH"].append(sh(hold(lg)))
d["ms"].append(m)
d["kp"].append(float(np.mean(np.concatenate(list(szs.values())))))
if store:
d["dC"].append(cagr(b) - cagr(c))
d["ddv"].append(maxdd(b)); d["ddc"].append(maxdd(c))
for y in {int(t) for t in b.index.year}:
by, cy = b[b.index.year == y], c[c.index.year == y]
if len(by) > 20:
yr.setdefault(y, []).append(sh(by) - sh(cy))
if store:
BK.setdefault(nm, {})[o] = b
CT.setdefault(nm, {})[o] = c
LEG.setdefault(nm, {})[o] = lg
MS.setdefault(nm, {})[o] = m
a_ = np.asarray(dF, float)
return dict(nm=nm, thr=thr, w=w, h=h, sg=sg,
dF=float(np.median(a_)), nF=int((a_ > 0).sum()),
lo=float(a_.min()), hi=float(a_.max()), arrF=a_,
dH=float(np.median(dH)), nH=int((np.asarray(dH) > 0).sum()),
isoI=float(np.median(dI)), dC=float(np.median(dC_)),
legF=float(np.median(lF)), legI=float(np.median(lI)),
legH=float(np.median(lH)), kept=float(np.median(kp)),
weff=float(np.median([W_SKH * x for x in ms])),
ddv=float(np.median(ddv)), ddc=float(np.median(ddc)),
yr={y: float(np.median(v)) for y, v in yr.items()})
d["yr"].setdefault(y, []).append(sh(by) - sh(cy))
BK.setdefault(nm, {})[o] = b
CT.setdefault(nm, {})[o] = c
LEG.setdefault(nm, {})[o] = lg
MS.setdefault(nm, {})[o] = m
RES = {}
for (thr, w, h, sg) in cells:
r = eval_cell(thr, w, h, sg)
RES[r["nm"]] = r
for o in offs:
KC.clear()
for (thr, w, h, sg) in cells:
_one(o, thr, w, h, sg, store=False)
KC.clear()
def _med(v, dflt=float("nan")):
return float(np.median(v)) if len(v) else dflt
def _fin(d: dict) -> dict:
a_ = np.asarray(d["dF"], float)
return dict(nm=cname(d["thr"], d["w"], d["h"], d["sg"]), thr=d["thr"], w=d["w"],
h=d["h"], sg=d["sg"], dF=float(np.median(a_)), nF=int((a_ > 0).sum()),
lo=float(a_.min()), hi=float(a_.max()), arrF=a_,
dH=float(np.median(d["dH"])), nH=int((np.asarray(d["dH"]) > 0).sum()),
isoI=float(np.median(d["dI"])), dC=_med(d["dC"]),
legF=float(np.median(d["lF"])), legI=float(np.median(d["lI"])),
legH=float(np.median(d["lH"])), kept=float(np.median(d["kp"])),
weff=float(np.median([W_SKH * x for x in d["ms"]])),
ddv=_med(d["ddv"]), ddc=_med(d["ddc"]),
yr={y: float(np.median(v)) for y, v in d["yr"].items()})
RES = {nm: _fin(d) for nm, d in ACC.items()}
print(f" ({len(cells)} celle x {len(offs)} ancore valutate in {time.time()-t0:.0f}s "
f"dall'avvio, prima passata senza conservare le serie)")
f"dall'avvio, prima passata senza conservare le serie; RSS {rss_mb():.0f} MB)")
def matrix(sg: int, h: float) -> None:
print(f"\n ISO dShFULL — segno {'+1 (tieni vol BASSA)' if sg>0 else '-1 (CONTROLLO)'}"
@@ -383,9 +421,19 @@ def main() -> None:
f"positive {sum(r['dF']>0 for r in NEG)}/{len(NEG)} <- controllo di segno")
# seconda passata: conserva le serie delle sole celle che servono dopo
sel_iso0 = max(ALLC, key=lambda r: r["isoI"])
ACC2 = ACC
for r in dict((x["nm"], x) for x in (RES[nmcan], argmax, sel_iso0,
max(ALLC, key=lambda z: z["legI"]))).values():
eval_cell(r["thr"], r["w"], r["h"], r["sg"], store=True)
ACC = {}
for o in offs:
_one(o, r["thr"], r["w"], r["h"], r["sg"], store=True)
KC.clear()
RES[r["nm"]] = _fin(ACC[r["nm"]]) # ora con dC / maxDD / per-anno
ACC = ACC2
POS = [RES[cname(t, w, h, +1)] for t in THRS for w in WS for h in HS]
NEG = [RES[cname(t, w, h, -1)] for t in THRS for w in WS for h in HS]
ALLC = POS + NEG
argmax = RES[argmax["nm"]]
dchk = A.anchor_luck_delta(lambda o: BK[nmcan][o], lambda o: CT[nmcan][o], offs, metric=sh)
assert abs(dchk["median_paired"] - RES[nmcan]["dF"]) < 1e-12 and \
dchk["n_positive"] == RES[nmcan]["nF"], "la statistica appaiata di §4 non coincide con A.anchor_luck_delta"
@@ -718,8 +766,10 @@ def main() -> None:
print(f" {tag:<40}{row}")
if fee != keep_fee:
del EXf, basef
gc.collect()
V.FEE_RT = keep_fee
assert V.FEE_RT == 0.001
print(f" (RSS {rss_mb():.0f} MB)")
f_mod = FEEDEC[(f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)", nmcan)]
f_zero = FEEDEC[("fee ZERO (informazione pura)", nmcan)]
print(f" -> a fee ZERO resta {f_zero:+.3f} di {f_mod:+.3f} = "
@@ -735,6 +785,8 @@ def main() -> None:
print(" temporale, ma e' l'unico campione indipendente disponibile a questo costo.")
print("-" * L)
EX1 = {o: {a: V.extract(a, o, SKH01_V1) for a in ASSETS} for o in offs}
R.get5m.cache_clear() # i 5m non servono piu': ~80 MB restituiti
gc.collect()
base1 = {o: V.leg_daily(EX1[o], {a: V.size_flat(EX1[o][a]) for a in ASSETS}) for o in offs}
print(f" {'cella':<26}{'ISO dShFULL':>13}{'pos/n':>8}{'ISO dShHOLD':>14}{'pos/n':>8}"
f"{'tenuti':>9}")
@@ -751,6 +803,10 @@ def main() -> None:
print(f" {cname(r['thr'], r['w'], r['h'], r['sg']):<26}{dF['median_paired']:>+13.3f}"
f"{dF['n_positive']:>5}/{len(offs):<3}{dH['median_paired']:>+14.3f}"
f"{dH['n_positive']:>5}/{len(offs):<3}{float(np.median(kp))*100:>8.0f}%")
del EX1, base1
KC.clear()
gc.collect()
print(f" (RSS {rss_mb():.0f} MB)")
# ---------------------------------------------------------- §11 per anno
print("\n" + "-" * L)
+640
View File
@@ -0,0 +1,640 @@
"""r0822b_depeg.py — IL FALSIFICATORE DI VENUE PER XS01-OOS (2026-08-22, ondata wave-0822).
IL PUNTO FRAGILE, DICHIARATO DALL'AUTORE DEL RISULTATO
-----------------------------------------------------
`r0822b_xs01_oos.py` (RESULTS-0822 §21) ha misurato che il meccanismo congelato di XS01 ha un edge
FUORI dalla sua finestra di scoperta — 2021-2023 su **Binance spot USDT** — e che li' e' PIU' GRANDE
(mediana di fase +1,12 contro +0,37, differenza appaiata +0,668 in 10/10 fasi). Quel risultato
riscrive il gate del canale funded, quindi merita il suo falsificatore.
Lo stesso autore lo ha nominato:
> "La validazione di venue Binance-vs-Hyperliquid gira SOLO sulla finestra comune 2024+, cioe'
> proprio il periodo SENZA depeg USDT. Il fuori campione contiene il depeg di maggio 2022. Se i
> close Binance/USDT 2021-2023 divergessero da un venue USD in modo NON COMUNE fra gli asset, il
> ranking cambierebbe e il risultato con esso. Test che lo deciderebbe: rifare il meccanismo sui
> close Coinbase USD per il sottoinsieme quotato li', 2021-2023, e chiedere corr fra i due sleeve
> >= 0.99 come sul 2024+."
Questo script esegue esattamente quel test.
PERCHE' LA DOMANDA NON E' OZIOSA (il meccanismo del danno, prima dei numeri)
---------------------------------------------------------------------------
La regola del progetto — "Binance non e' la verita', e' USDT, ~10 bps fuori, e sotto depeg fino al
3% off" — e' stata **correttamente** giudicata non-binding da §21: uno z-score cross-sezionale
sottrae la media di sezione, quindi e' invariante a un fattore **COMUNE**. Ma un depeg non e'
garantito comune: nei giorni di stress la liquidita' si sposta in modo diverso per asset (i major
restano scambiati contro USDT, i minori no; i flussi di fuga passano per coppie diverse), e una
divergenza **IDIOSINCRATICA** cambia il RANKING — che e' esattamente cio' che la strategia legge.
Maggio 2022 (UST/LUNA, con USDT sceso a ~0,95) e' il caso peggiore possibile.
Percio' la divergenza va misurata in DUE pezzi, e sono due domande diverse:
(a) COMUNE = mediana per giorno dello scarto fra i venue -> quella che lo z-score annulla;
(b) IDIOSINCRATICA = scarto per asset DOPO aver tolto la mediana del giorno -> quella che uccide.
Riportare solo (a) darebbe la risposta rassicurante e sbagliata.
IL VINCOLO DEL DATO, E COME E' STATO RISOLTO (dichiarato PRIMA di misurare)
--------------------------------------------------------------------------
Su Coinbase USD, dei 13 asset di U13:
BNB -> quotato solo dal 2025-10 = FUORI (nessun venue USD ce l'ha nel 2021-23: e' l'asset
nativo del venue che stiamo mettendo in discussione)
XRP -> SOSPESO su Coinbase dal 2021-01-19 al 2023-07 (causa SEC) = buco proprio sulla finestra
NEAR/INJ -> quotati su Coinbase solo dal 2022-09
AVAX -> dal 2021-09-30 (il vincolo che decide l'inizio della finestra)
XRP e' recuperato da **Bitstamp USD** (l'altra referenza USD del `venue_watch` del progetto, che
non ha mai sospeso la coppia). Restano fuori BNB, NEAR, INJ.
Ne escono DUE test, e il secondo copre cio' che il primo non copre:
TEST 1 "U10-USD" = universo FISSO di 10 gambe (BTC ETH LTC LINK AAVE ADA DOGE SOL AVAX + XRP),
2021-09-30 -> 2023-12-31, meccanismo **CONGELATO** (k=5, min_assets=10:
dieci gambe sono ESATTAMENTE il minimo, nessun parametro toccato).
Contiene il depeg di maggio 2022 e FTX. NON contiene il 2021.
TEST 2 "U13-IBRIDO" = i 13 asset e la finestra INTERA 2021-01 -> 2023-12, con il close USD
sostituito a quello USDT **dove esiste** (9-12 gambe su 13 a seconda della
data) e Binance dove non esiste. Non e' un confronto puro di venue, e'
la domanda operativa: *se sostituisco tutto il prezzo USD che esiste, il
risultato di §21 si muove?*
In entrambi i test il confronto e' **APPAIATO**: stesso sottoinsieme, stessa finestra, stesse 10
fasi, stesso codice del motore. Confrontare "19 gambe su Binance" con "10 su Coinbase" misurerebbe
l'UNIVERSO, non il venue (regola del progetto: un confronto appaiato eredita TUTTI i parametri
della riga in cui compare) — e per questo il TEST 1 riporta anche cosa fa U10 **su Binance**
rispetto a U13 su Binance, cioe' quanto del titolo di §21 questo sottoinsieme riproduce.
MECCANISMO: CONGELATO. Il motore e' importato da `r0822b_xs01_oos.py` (che a sua volta replica
`sleeves._xsec_returns` a max|diff| = 0.0). Nessuna riga di strategia riscritta qui: se il motore
fosse riscritto, una differenza di venue e una differenza di implementazione sarebbero
indistinguibili.
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822b_depeg.py
Rete: sola lettura, <= 2 req/s, MAI nella finestra :25-:29 (rate limit per-IP condiviso col
collettore della catena opzioni; guasto del 29/07). Download in cache nello scratchpad: la
seconda corsa e' offline.
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import altlib as A # noqa: E402
import r0822b_xs01_oos as X # noqa: E402 (motore CONGELATO + cache Binance)
from src.portfolio.sleeves import XS_CFG # noqa: E402
CACHE = X.CACHE.parent / "r0822b_depeg"
CACHE.mkdir(parents=True, exist_ok=True)
# ---------------------------------------------------------------------------
# UNIVERSI E FINESTRE — dichiarati PRIMA di guardare i numeri
# ---------------------------------------------------------------------------
U13 = X.U13 # l'universo di §21
# venue USD per asset. `None` = nessun venue USD con storia sulla finestra.
USD_SRC: dict[str, tuple[str, str] | None] = {
"BTC": ("coinbase", "BTC/USD"), "ETH": ("coinbase", "ETH/USD"),
"LTC": ("coinbase", "LTC/USD"), "LINK": ("coinbase", "LINK/USD"),
"AAVE": ("coinbase", "AAVE/USD"), "ADA": ("coinbase", "ADA/USD"),
"DOGE": ("coinbase", "DOGE/USD"), "SOL": ("coinbase", "SOL/USD"),
"AVAX": ("coinbase", "AVAX/USD"), "NEAR": ("coinbase", "NEAR/USD"),
"INJ": ("coinbase", "INJ/USD"),
"XRP": ("bitstamp", "XRP/USD"), # Coinbase l'ha sospeso 2021-01 -> 2023-07
"BNB": None, # nessun venue USD prima del 2025-10
}
U10 = ["BTC", "ETH", "LTC", "LINK", "AAVE", "ADA", "DOGE", "SOL", "AVAX", "XRP"]
T1_START, T1_END = "2021-09-30", "2024-01-01" # AVAX su Coinbase e' il vincolo
T2_START, T2_END = "2021-01-01", "2024-01-01" # la finestra fuori campione di §21
DEPEG = ("2022-05-07", "2022-05-21") # UST/LUNA + USDT a ~0.95 (estremo incluso)
PHASES = list(range(XS_CFG["H"])) # 10 fasi: la stima onesta e' la MEDIANA
CORR_SOGLIA = 0.99 # soglia DICHIARATA DALL'AUTORE, non da me
# ===========================================================================
# 1. FETCH USD (Coinbase / Bitstamp) + certificazione locale
# ===========================================================================
def _fetch_paged(ex, market: str, tf: str, start_ms: int, end_ms: int,
step_bars: int, bar_ms: int) -> list:
"""Paginazione DETERMINISTICA sulla finestra: avanza anche quando una pagina torna vuota.
⚠️ Non e' un dettaglio: XRP su Coinbase ha un buco di 2,5 anni (sospensione SEC) e un loop che
si ferma alla prima pagina vuota — il modo ovvio di scrivere questo ciclo — troncherebbe la
serie li', producendo un 'asset che non esiste' invece di un 'asset con un buco'. Sono due
fatti diversi e portano a due decisioni diverse.
"""
rows, cur, span = [], start_ms, step_bars * bar_ms
while cur < end_ms:
X._outside_forbidden_window()
batch = []
for attempt in range(3):
try:
batch = ex.fetch_ohlcv(market, tf, since=cur, limit=step_bars)
break
except Exception as e: # noqa: BLE001
if attempt == 2:
print(f" [rete] {market} @ {cur}: {type(e).__name__} {str(e)[:70]}")
time.sleep(2 ** attempt)
time.sleep(0.55) # <= 2 req/s
rows += [b for b in (batch or []) if cur <= b[0] < cur + span]
cur += span
return rows
def fetch_usd_daily(asset: str) -> pd.DataFrame:
"""Close giornalieri USD dal venue dichiarato in USD_SRC. Cache su disco."""
src = USD_SRC.get(asset)
if src is None:
return pd.DataFrame(columns=["timestamp", "close", "volume"])
venue, market = src
f = CACHE / f"{venue[:2]}_{asset.lower()}_1d.parquet"
if f.exists():
return pd.read_parquet(f)
import ccxt
ex = getattr(ccxt, venue)({"enableRateLimit": True})
ex.load_markets()
if market not in ex.markets:
d = pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"])
d.to_parquet(f)
return d
start = ex.parse8601("2019-01-01T00:00:00Z")
end = int(time.time() * 1000)
step = 300 if venue == "coinbase" else 1000
rows = _fetch_paged(ex, market, "1d", start, end, step, 86_400_000)
d = (pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"])
.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True))
d.to_parquet(f)
return d
def series_of(d: pd.DataFrame) -> pd.Series:
if not len(d):
# indice tz-aware anche da vuoto: un asset senza venue USD (BNB) deve poter essere
# confrontato/reindicizzato come gli altri, non far cadere lo script
return pd.Series(dtype=float, index=pd.DatetimeIndex([], tz="UTC"))
return pd.Series(d["close"].values.astype(float),
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
def certify_usd(asset: str, d: pd.DataFrame, a: str, b: str) -> dict:
"""Stessa forma della certificazione di §21, ma valutata SULLA FINESTRA DEL TEST.
Certificare "tutta la serie" direbbe la cosa sbagliata: XRP su Coinbase ha 900 giorni di buco
che sulla finestra 2021-09/2023-12 sono TUTTI dentro. Il numero utile e' la copertura della
finestra in cui il meccanismo gira, non della serie.
"""
s = series_of(d)
s = s[(s.index >= pd.Timestamp(a, tz="UTC")) & (s.index < pd.Timestamp(b, tz="UTC"))]
if not len(s):
return dict(sym=asset, n=0, ok=False, start="-", end="-", gap=-1,
flat=float("nan"), zero_vol=float("nan"))
v = series_of(d.assign(close=d["volume"]))
v = v[(v.index >= pd.Timestamp(a, tz="UTC")) & (v.index < pd.Timestamp(b, tz="UTC"))]
attesi = (pd.Timestamp(b, tz="UTC") - s.index.min()).days
c = s.values
flat = float(np.mean(c[1:] == c[:-1])) if len(c) > 1 else float("nan")
zv = float(np.mean(v.values == 0.0)) if len(v) else float("nan")
gap = int(attesi - len(s))
return dict(sym=asset, n=len(s), start=str(s.index.min().date()),
end=str(s.index.max().date()), gap=gap, flat=flat, zero_vol=zv,
ok=bool(gap == 0 and flat < 0.02 and (not np.isfinite(zv) or zv < 0.01)))
# ===========================================================================
# 2. pannelli
# ===========================================================================
def bin_panel(syms: list[str]) -> pd.DataFrame:
"""Pannello Binance USDT — dalla cache di §21, cosi' il lato USDT del confronto e' BIT-IDENTICO
a quello che ha prodotto il risultato che sto falsificando."""
return pd.concat({s: series_of(X.fetch_binance_daily(s)) for s in syms}, axis=1).sort_index()
def usd_panel(syms: list[str]) -> pd.DataFrame:
return pd.concat({s: series_of(fetch_usd_daily(s)) for s in syms}, axis=1).sort_index()
def splice(usd: pd.Series, usdt: pd.Series) -> pd.Series:
"""Serie IBRIDA: USD dove esiste, USDT prima — RI-SCALANDO il tratto USDT al livello USD nel
giorno del giunto.
⚠️ Senza la ri-scalatura il giunto crea un rendimento fantasma pari allo scarto fra i venue
(pochi bps, ma esattamente il tipo di artefatto che questo script deve misurare, non fabbricare).
Il meccanismo legge RAPPORTI di prezzo a 30 e 90 giorni: dopo la ri-scalatura la serie e'
continua e i rapporti che attraversano il giunto sono ben definiti.
"""
u = usd.dropna()
if not len(u):
return usdt
t0 = u.index[0]
if t0 not in usdt.index or not np.isfinite(usdt.get(t0, np.nan)):
return usd.reindex(usdt.index)
k = float(u.iloc[0]) / float(usdt.loc[t0])
pre = usdt[usdt.index < t0] * k
return pd.concat([pre, u]).sort_index().reindex(usdt.index)
def hybrid_panel(syms: list[str]) -> tuple[pd.DataFrame, dict]:
B, U = bin_panel(syms), usd_panel(syms)
cols, meta = {}, {}
for s in syms:
u = U[s] if s in U.columns else pd.Series(dtype=float)
u = u.reindex(B.index)
cols[s] = splice(u, B[s])
cov = u.dropna()
meta[s] = (str(cov.index[0].date()) if len(cov) else "mai")
return pd.concat(cols, axis=1).sort_index(), meta
# ===========================================================================
# 3. metriche e allineamento appaiato
# ===========================================================================
sh, dd, cagr, win, alive, line = X.sh, X.dd, X.cagr, X.win, X.alive, X.line
def pair(a: pd.Series, b: pd.Series) -> tuple[pd.Series, pd.Series]:
"""Allinea due sleeve sullo stesso indice: un confronto di venue non puo' confrontare finestre."""
j = pd.concat({"a": a, "b": b}, axis=1, join="inner").dropna()
return j["a"], j["b"]
def ranking_scan(PB: pd.DataFrame, PU: pd.DataFrame, phases: list[int],
label: str) -> pd.DataFrame:
"""IL CANALE DEL DANNO, misurato dove passa: il venue puo' far male SOLO cambiando le gambe
scelte. Scansione di TUTTE le fasi (non solo la canonica: un ribaltamento su una fase sola
sarebbe invisibile all'ancora 0) con self-check dello specchio contro il motore."""
from scipy.stats import spearmanr
lb = XS_CFG["lookbacks"]
mlb, H, k = max(lb), XS_CFG["H"], XS_CFG["k"]
rows, sp, checked, tot = [], [], 0, 0
for ph in phases:
det: dict = {}
X.xsec_engine(PB, phase=ph, detail=det)
WB = det["W"]
for i in range(mlb, len(PB)):
if i % H != ph:
continue
iB, sB = score_at(PB, i, lb)
iU, sU = score_at(PU, i, lb)
if len(iB) < 10 or len(iU) < 10:
continue
tot += 1
oB = np.argsort(sB)
loB, hiB = set(iB[oB[:k]]), set(iB[oB[-k:]])
mh, ml = set(np.flatnonzero(WB[i] > 0)), set(np.flatnonzero(WB[i] < 0))
if mh or ml: # self-check solo dove il motore ha una posizione
checked += 1
if (mh, ml) != (hiB, loB):
raise AssertionError(f"specchio != motore ({label}, fase {ph}, i={i}): "
"misurerei la mia riscrittura invece del venue")
oU = np.argsort(sU)
loU, hiU = set(iU[oU[:k]]), set(iU[oU[-k:]])
ov = (len(hiB & hiU) + len(loB & loU)) / (2 * k)
sp.append(float(spearmanr(sB, sU).statistic))
if ov < 1.0:
diff = sorted(set(PB.columns[list(hiB ^ hiU)]) | set(PB.columns[list(loB ^ loU)]))
rows.append(dict(fase=ph, data=PB.index[i], overlap=ov, gambe=",".join(diff)))
D = pd.DataFrame(rows)
print(f" [{label}] {tot} ribilanciamenti su {len(phases)} fasi; specchio verificato "
f"contro il motore in {checked}/{checked} con posizione")
print(f" [{label}] Spearman dello score fra i venue: mediana {np.median(sp):.4f} "
f"min {np.min(sp):.4f}")
print(f" [{label}] ribilanciamenti con gambe DIVERSE: {len(D)}/{tot} "
f"({len(D)/max(tot,1)*100:.2f}%)")
for _, r in D.iterrows():
print(f" fase {int(r['fase'])} {r['data'].date()} "
f"sovrapposizione {r['overlap']*100:3.0f}% gambe scambiate: {r['gambe']}")
return D
def score_at(P: pd.DataFrame, i: int, lookbacks) -> tuple[np.ndarray, np.ndarray]:
"""Specchio della riga di score del motore (z-score medio sui lookback). Serve per confrontare
i RANKING fra venue; la sezione [4] verifica che i top-k/bottom-k che ne derivano coincidano
ESATTAMENTE con quelli scelti dal motore — se lo specchio divergesse, ogni numero di ranking
misurerebbe la mia riscrittura invece del venue."""
px = P.values.astype(float)
elig = np.isfinite(px[i])
for L in lookbacks:
elig = elig & np.isfinite(px[i - L])
idx = np.flatnonzero(elig)
sc, cnt = np.zeros(len(idx)), 0
for L in lookbacks:
r = px[i, idx] / px[i - L, idx] - 1.0
if r.std() > 0:
sc += (r - r.mean()) / r.std()
cnt += 1
return idx, (sc / cnt if cnt else sc)
# ===========================================================================
def main() -> None:
t0 = time.time()
pd.set_option("display.width", 200)
print("=" * 100)
print("DEPEG — il close USDT 2021-2023 puo' aver cambiato il RANKING di XS01-OOS?")
print(f"XS_CFG (CONGELATO) = {XS_CFG} soglia dichiarata dall'autore: corr >= {CORR_SOGLIA}")
print("=" * 100)
# ------------------------------------------------------------- [0] sanity
print("\n[0] SANITY — il motore importato e' quello che ha prodotto §21?")
from src.portfolio.sleeves import _xsec_returns
off = _xsec_returns()
rep = X.xsec_engine(X.hl_panel(), phase=0)
j = pd.concat({"o": off, "r": rep}, axis=1, join="inner").dropna()
md = float(np.max(np.abs(j["o"] - j["r"])))
print(f" motore vs sleeve UFFICIALE su HL: n={len(j)} max|diff| = {md:.2e} "
f"-> {'IDENTICO' if md < 1e-12 else 'DIVERGE — STOP'}")
if md >= 1e-12:
return
# ------------------------------------------------------- [1] dato USD
print("\n[1] DATO USD — quanti dei 13 asset di §21 hanno un prezzo in DOLLARI nel 2021-2023?")
print(f" {'sym':<6}{'venue USD':>11}{'n':>6}{'start':>12}{'end':>12}{'gap':>5}"
f"{'flat%':>8}{'vol0%':>8} (finestra {T1_START} .. {T1_END})")
certs = {}
for s in U13:
d = fetch_usd_daily(s)
c = certify_usd(s, d, T1_START, T1_END)
certs[s] = c
ven = USD_SRC[s][0] if USD_SRC[s] else ""
flag = "" if c["ok"] else " <-- non usabile su questa finestra"
print(f" {s:<6}{ven:>11}{c['n']:>6}{c['start']:>12}{c['end']:>12}{c['gap']:>5}"
f"{c['flat']*100:>8.2f}{c['zero_vol']*100:>8.2f}{flag}")
usabili = [s for s in U13 if certs[s]["ok"]]
print(f" -> usabili sulla finestra del TEST 1: {len(usabili)}/13 {usabili}")
print(f" -> ESCLUSI: BNB (nessun venue USD prima del 2025-10 — e' l'asset nativo del venue")
print(f" sotto esame), NEAR e INJ (Coinbase solo dal 2022-09).")
print(f" ⚠️ LIMITE DEL TEST, da dire subito: 10 gambe su 13 e finestra che parte dal")
print(f" {T1_START} (AVAX su Coinbase) -> il TEST 1 NON copre il 2021, che in §21 e'")
print(f" l'anno piu' forte (+2,14). Copre pero' ESATTAMENTE cio' che l'obiezione nomina:")
print(f" il depeg di maggio 2022 e FTX. Il 2021 lo copre il TEST 2 (ibrido).")
# controllo di ALLINEAMENTO: le barre USD sono sulla stessa griglia di quelle USDT?
print("\n Controllo di ALLINEAMENTO — se le barre USD e USDT fossero su griglie sfasate,")
print(" ogni numero sotto sarebbe uno sfasamento di calendario travestito da divergenza")
print(" di venue. corr dei RITORNI giornalieri, 2024+ (finestra dove esistono entrambi):")
B24 = bin_panel(U13)
U24 = usd_panel(U13)
al, shifted = [], []
for s in U13:
if s not in U24.columns or not U24[s].notna().any():
continue
k = pd.concat({"u": U24[s], "b": B24[s]}, axis=1, join="inner").dropna()
k = k[k.index >= pd.Timestamp("2024-01-01", tz="UTC")].pct_change().dropna()
if len(k) > 200:
al.append((s, float(k["u"].corr(k["b"]))))
shifted.append(float(k["u"].shift(1).corr(k["b"]))) # controllo POSITIVO
print(" " + " ".join(f"{s}:{c:.4f}" for s, c in al))
lo_al = min(c for _, c in al)
# ⚠️ Un controllo che non fallisce mai non e' un controllo: il modo di fallire e' uno
# sfasamento di UNA barra, e va mostrato che il numero se ne accorge. Con la serie USD
# ritardata di un giorno la corr collassa: e' quella la scala del guasto che sto escludendo.
print(f" corr minima {lo_al:.4f} | controllo POSITIVO (USD ritardato di 1 giorno): "
f"corr mediana {np.median(shifted):+.4f}, massima {np.max(shifted):+.4f}")
if lo_al < 0.95:
print(" -> SFASAMENTO: i due venue non sono sulla stessa griglia. STOP.")
return
print(" -> allineati (uno sfasamento darebbe ~0, non 0,99). Gli asset sotto 0,999")
print(f" ({', '.join(s for s, c in al if c < 0.999)}) sono solo piu' RUMOROSI in USD")
print(" (book piu' sottile sul venue USD), non sfasati.")
# --------------------------------------------- [2] la divergenza, in DUE pezzi
print("\n[2] LA DIVERGENZA USD-vs-USDT, scomposta — e il pezzo che conta e' il SECONDO")
Bp = bin_panel(U10).loc[T1_START:T1_END]
Up = usd_panel(U10).reindex(Bp.index)
both = Bp.notna() & Up.notna()
D = 1e4 * np.log(Up / Bp) # scarto di livello, bps
D = D.where(both)
common = D.median(axis=1) # (a) componente COMUNE (il peg + il venue)
idio = D.sub(common, axis=0) # (b) componente IDIOSINCRATICA (cambia il rango)
print(f" {len(Bp)} giorni, {len(U10)} asset, {int(both.values.sum())} osservazioni appaiate")
print(f" (a) COMUNE |mediana per giorno|: med {common.abs().median():7.1f} bps "
f"p95 {common.abs().quantile(.95):7.1f} max {common.abs().max():7.1f}")
print(f" (b) IDIOSINCRATICO |scarto de-medianato|: med {idio.abs().stack().median():7.1f} bps "
f"p95 {idio.abs().stack().quantile(.95):7.1f} max {idio.abs().stack().max():7.1f}")
print(" -> (a) e' cio' che lo z-score cross-sezionale annulla per costruzione; (b) no.")
print(f"\n LA FINESTRA DEL DEPEG {DEPEG[0]} .. {DEPEG[1]} (UST/LUNA; USDT a ~0,95):")
dm = (D.index >= pd.Timestamp(DEPEG[0], tz="UTC")) & (D.index < pd.Timestamp(DEPEG[1], tz="UTC"))
print(f" {'giorno':<12}{'comune':>9}" + "".join(f"{s:>8}" for s in U10))
for t in D.index[dm]:
print(f" {str(t.date()):<12}{common.loc[t]:>9.0f}" +
"".join(f"{idio.loc[t, s]:>8.0f}" if np.isfinite(idio.loc[t, s]) else f"{'':>8}"
for s in U10))
print(" (numeri in bps; colonna 'comune' = mediana del giorno, le altre = scarto DOPO")
print(" averla tolta, cioe' la parte che puo' spostare il ranking)")
idm = idio[dm].abs().stack()
print(f" idiosincratico NELLA finestra: med {idm.median():.1f} bps p95 "
f"{idm.quantile(.95):.1f} max {idm.max():.1f} "
f"(FUORI: med {idio[~dm].abs().stack().median():.1f}, "
f"p95 {idio[~dm].abs().stack().quantile(.95):.1f})")
print("\n Ma il ranking non legge il LIVELLO: legge i rapporti a 30 e 90 giorni. Divergenza")
print(" idiosincratica degli STESSI rendimenti che entrano nello score (bps):")
for L in XS_CFG["lookbacks"]:
rB = (Bp / Bp.shift(L) - 1.0) * 1e4
rU = (Up / Up.shift(L) - 1.0) * 1e4
dR = (rU - rB).where(both)
dRi = dR.sub(dR.median(axis=1), axis=0)
w = dRi[dm].abs().stack()
print(f" r{L:<3d}: idiosincratico med {dRi.abs().stack().median():6.1f} bps "
f"p95 {dRi.abs().stack().quantile(.95):7.1f} max {dRi.abs().stack().max():8.1f}"
f" | nel depeg med {w.median():6.1f} max {w.max():8.1f}")
# ------------------------------------------- [3] IL TEST 1 — U10, k=5 congelato
print(f"\n[3] TEST 1 — meccanismo CONGELATO su U10, {T1_START} .. {T1_END}, USD vs USDT")
print(f" 10 gambe = ESATTAMENTE il minimo del meccanismo (min_assets=10, k=5): nessun")
print(f" parametro toccato, e il confronto e' appaiato su TUTTO tranne il venue.")
PB = Bp[U10].dropna()
PU = Up[U10].dropna()
idxc = PB.index.intersection(PU.index)
PB, PU = PB.loc[idxc], PU.loc[idxc]
print(f" pannello comune: {len(idxc)} giorni, {PB.index[0].date()} .. {PB.index[-1].date()}")
rows = []
for ph in PHASES:
a = alive(X.xsec_engine(PU, phase=ph))
b = alive(X.xsec_engine(PB, phase=ph))
a, b = pair(a, b)
rows.append(dict(ph=ph, corr=float(a.corr(b)), sh_usd=sh(a), sh_usdt=sh(b),
d=sh(a) - sh(b), dd_usd=dd(a), dd_usdt=dd(b), n=len(a)))
R = pd.DataFrame(rows)
print(f" {'fase':>5}{'corr':>9}{'Sh USD':>9}{'Sh USDT':>9}{'dSharpe':>10}"
f"{'DD USD':>9}{'DD USDT':>9}")
for _, r in R.iterrows():
print(f" {int(r['ph']):>5}{r['corr']:>9.4f}{r['sh_usd']:>9.2f}{r['sh_usdt']:>9.2f}"
f"{r['d']:>+10.3f}{r['dd_usd']*100:>8.1f}%{r['dd_usdt']*100:>8.1f}%")
print(f" {'MEDIANA':>5}{R['corr'].median():>9.4f}{R['sh_usd'].median():>9.2f}"
f"{R['sh_usdt'].median():>9.2f}{R['d'].median():>+10.3f}"
f"{R['dd_usd'].median()*100:>8.1f}%{R['dd_usdt'].median()*100:>8.1f}%")
print(f" corr MINIMA fra le 10 fasi: {R['corr'].min():.4f} (fase {int(R.loc[R['corr'].idxmin(),'ph'])})"
f" | soglia dichiarata {CORR_SOGLIA} -> "
f"{'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}")
print(f" dSharpe appaiato (USD - USDT): mediana {R['d'].median():+.3f} "
f"banda [{R['d'].min():+.3f},{R['d'].max():+.3f}] "
f"fasi in cui USD e' MEGLIO {(R['d'] > 0).sum()}/{len(R)}")
print(f" ⚠️ la statistica e' la MEDIANA DELLE DIFFERENZE APPAIATE, non la differenza delle")
print(f" mediane (lezione 26/07: quest'ultima confronta fasi diverse e puo' ribaltare).")
# quanto di §21 riproduce questo sottoinsieme? (o si misura l'universo, non il venue)
p13 = bin_panel(U13)[U13].dropna()
s13_w = win(alive(X.xsec_engine(p13, phase=0)), T1_START, T1_END)
s10_w = win(alive(X.xsec_engine(PB, phase=0)), T1_START, T1_END)
print(f"\n Contesto obbligatorio — quanto di §21 vive in questo sottoinsieme (Binance, fase 0,")
print(f" stessa finestra): U13 Sh {sh(s13_w):+.2f} | U10 Sh {sh(s10_w):+.2f}")
print(f" -> il TEST 1 giudica il VENUE su U10; NON e' una replica del titolo di §21.")
# ------------------------------------------------ [4] i RANKING, non i P&L
print("\n[4] IL CANALE DEL DANNO — cambia il RANKING? (e' cio' che la strategia legge)")
print(" Un P&L simile puo' nascere da due ranking diversi che si compensano: la domanda")
print(" diretta e' se le GAMBE SCELTE cambiano. Scansione su tutte e 10 le fasi.")
D1 = ranking_scan(PB, PU, PHASES, "TEST 1 / U10")
if len(D1):
ind = D1[(D1["data"] >= pd.Timestamp(DEPEG[0], tz="UTC")) &
(D1["data"] < pd.Timestamp(DEPEG[1], tz="UTC"))]
print(f" di cui dentro la finestra del depeg: {len(ind)}")
# ------------------------------- [5] TEST 2 — U13 ibrido, finestra INTERA di §21
print(f"\n[5] TEST 2 — U13 IBRIDO (USD dove esiste, USDT altrove), {T2_START} .. {T2_END}")
print(" Domanda operativa: sostituendo TUTTO il prezzo in dollari che esiste, il titolo")
print(" di §21 (mediana di fase +1,12 fuori campione) si muove?")
HB, meta = hybrid_panel(U13)
print(" prima data USD per asset: " + " ".join(f"{s}:{meta[s]}" for s in U13))
PBh = p13
PUh = HB[U13].dropna()
ic = PBh.index.intersection(PUh.index)
PBh, PUh = PBh.loc[ic], PUh.loc[ic]
rows2 = []
for ph in PHASES:
a = win(alive(X.xsec_engine(PUh, phase=ph)), T2_START, T2_END)
b = win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END)
a, b = pair(a, b)
rows2.append(dict(ph=ph, corr=float(a.corr(b)), sh_usd=sh(a), sh_usdt=sh(b),
d=sh(a) - sh(b), dd_usd=dd(a), dd_usdt=dd(b)))
R2 = pd.DataFrame(rows2)
print(f" {'fase':>5}{'corr':>9}{'Sh IBRIDO':>11}{'Sh USDT':>10}{'dSharpe':>10}"
f"{'DD IBR':>9}{'DD USDT':>9}")
for _, r in R2.iterrows():
print(f" {int(r['ph']):>5}{r['corr']:>9.4f}{r['sh_usd']:>11.2f}{r['sh_usdt']:>10.2f}"
f"{r['d']:>+10.3f}{r['dd_usd']*100:>8.1f}%{r['dd_usdt']*100:>8.1f}%")
print(f" {'MEDIANA':>5}{R2['corr'].median():>9.4f}{R2['sh_usd'].median():>11.2f}"
f"{R2['sh_usdt'].median():>10.2f}{R2['d'].median():>+10.3f}"
f"{R2['dd_usd'].median()*100:>8.1f}%{R2['dd_usdt'].median()*100:>8.1f}%")
print(f" corr minima {R2['corr'].min():.4f} -> "
f"{'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'} (soglia {CORR_SOGLIA})")
print(f" 📌 IL NUMERO DI TESTA DI §21, RICALCOLATO IN DOLLARI: mediana di fase fuori")
print(f" campione {R2['sh_usdt'].median():+.2f} (USDT, replica) -> "
f"{R2['sh_usd'].median():+.2f} (IBRIDO USD).")
# differenza appaiata con la macchineria del progetto (stesso gate di §21)
dlt = A.anchor_luck_delta(
lambda ph: win(alive(X.xsec_engine(PUh, phase=ph)), T2_START, T2_END),
lambda ph: win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END),
PHASES, metric=sh)
print(f" anchor_luck_delta (IBRIDO - USDT): mediana appaiata {dlt['median_paired']:+.3f} "
f"[{dlt['lo']:+.3f},{dlt['hi']:+.3f}], fasi con IBRIDO>USDT "
f"{dlt['n_positive']}/{dlt['n_anchors']}")
print("\n E il ranking, sull'ibrido (la fase con la corr piu' bassa va GUARDATA, non")
print(" riassunta: una corr di 0,97 su una fase o e' un ribaltamento vero o e' un giunto):")
D2 = ranking_scan(PBh, PUh, PHASES, "TEST 2 / U13-IBRIDO")
# ------------------------------- [5b] le gambe che NESSUN venue USD puo' controllare
print("\n[5b] LE TRE GAMBE NON CONTROLLABILI — quanto del risultato di §21 ci passa?")
print(" BNB e' il token nativo del venue sotto esame e NON esiste in USD nel 2021-23;")
print(" NEAR e INJ hanno un prezzo USD solo dal 2022-09. Se il titolo di §21 vivesse su")
print(" di loro, questo falsificatore non avrebbe potuto vedere niente, e andrebbe detto.")
base_b = A.anchor_luck_band(
lambda ph: win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END),
PHASES, canonical=0, metric=sh)
print(f" base U13 (Binance, fuori campione, mediana di fase): {base_b['median']:+.2f}")
for a_ in ("BNB", "NEAR", "INJ"):
sub = p13[[c for c in U13 if c != a_]].dropna()
bb = A.anchor_luck_band(
lambda ph, sub=sub: win(alive(X.xsec_engine(sub, phase=ph, min_assets=10)),
T2_START, T2_END),
PHASES, canonical=0, metric=sh)
print(f" senza {a_:<5s}: mediana di fase {bb['median']:+.2f} "
f"({bb['median']-base_b['median']:+.2f}) banda [{bb['lo']:+.2f},{bb['hi']:+.2f}]")
print(" (leave-one-out a 12 gambe: il meccanismo congelato regge, min_assets=10)")
# ---------------------------------------------------- [6] IL CASO PEGGIORE
print("\n[6] IL CASO PEGGIORE — non la media: dove i due venue divergono di piu'?")
print(f" {'anno':>6}" + "".join(f"{k:>12}" for k in
("corr T1", "dSh T1", "corr T2", "dSh T2")))
for y in (2021, 2022, 2023):
a1 = b1 = a2 = b2 = None
cs1, ds1, cs2, ds2 = [], [], [], []
for ph in PHASES:
a1 = win(alive(X.xsec_engine(PU, phase=ph)), f"{y}-01-01", f"{y+1}-01-01")
b1 = win(alive(X.xsec_engine(PB, phase=ph)), f"{y}-01-01", f"{y+1}-01-01")
if len(a1) > 60 and len(b1) > 60:
a1, b1 = pair(a1, b1)
cs1.append(float(a1.corr(b1))); ds1.append(sh(a1) - sh(b1))
a2 = win(alive(X.xsec_engine(PUh, phase=ph)), f"{y}-01-01", f"{y+1}-01-01")
b2 = win(alive(X.xsec_engine(PBh, phase=ph)), f"{y}-01-01", f"{y+1}-01-01")
if len(a2) > 60 and len(b2) > 60:
a2, b2 = pair(a2, b2)
cs2.append(float(a2.corr(b2))); ds2.append(sh(a2) - sh(b2))
f1 = f"{np.median(cs1):.4f}" if cs1 else ""
g1 = f"{np.median(ds1):+.3f}" if ds1 else ""
f2 = f"{np.median(cs2):.4f}" if cs2 else ""
g2 = f"{np.median(ds2):+.3f}" if ds2 else ""
print(f" {y:>6}{f1:>12}{g1:>12}{f2:>12}{g2:>12}")
worst1 = R.loc[R["corr"].idxmin()]
worst2 = R2.loc[R2["corr"].idxmin()]
wd1 = R.loc[R["d"].abs().idxmax()]
wd2 = R2.loc[R2["d"].abs().idxmax()]
print(f" peggior FASE per corr: T1 fase {int(worst1['ph'])} corr {worst1['corr']:.4f} | "
f"T2 fase {int(worst2['ph'])} corr {worst2['corr']:.4f}")
print(f" peggior FASE per dSharpe: T1 fase {int(wd1['ph'])} {wd1['d']:+.3f} | "
f"T2 fase {int(wd2['ph'])} {wd2['d']:+.3f}")
st = idio.abs().stack().sort_values(ascending=False)
print(" le 8 divergenze IDIOSINCRATICHE piu' grandi della finestra (bps di livello):")
for (t, s), v in st.head(8).items():
indep = "DEPEG" if pd.Timestamp(DEPEG[0], tz="UTC") <= t < pd.Timestamp(DEPEG[1], tz="UTC") else ""
print(f" {str(t.date()):<12}{s:<6}{v:8.0f} bps (comune quel giorno "
f"{common.loc[t]:+.0f} bps) {indep}")
print(" -> se le peggiori NON cadono nel depeg, il rumore di venue e' book sottile,")
print(" non contaminazione da stablecoin: due cause diverse, due implicazioni diverse.")
# il giorno peggiore: dove il P&L dei due sleeve si stacca di piu'
a0 = alive(X.xsec_engine(PU, phase=0)); b0 = alive(X.xsec_engine(PB, phase=0))
a0, b0 = pair(a0, b0)
gap = (a0 - b0)
print(f" scarto GIORNALIERO fra i due sleeve (fase 0): med "
f"{gap.abs().median()*1e4:.1f} bps p99 {gap.abs().quantile(.99)*1e4:.1f} bps "
f"max {gap.abs().max()*1e4:.1f} bps il {gap.abs().idxmax().date()}")
cum = (1 + a0).cumprod().iloc[-1] / (1 + b0).cumprod().iloc[-1] - 1
print(f" divario CUMULATO su tutta la finestra del TEST 1: {cum*100:+.2f}% "
f"(equity USD vs equity USDT)")
# --------------------------------------------------------------- [7] gate
print("\n[7] GATE")
print(f" soglia di venue (dichiarata dall'autore PRIMA, non da me): corr >= {CORR_SOGLIA}")
print(f" TEST 1 U10 congelato : corr mediana {R['corr'].median():.4f}, minima "
f"{R['corr'].min():.4f} -> {'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}")
print(f" TEST 2 U13 ibrido : corr mediana {R2['corr'].median():.4f}, minima "
f"{R2['corr'].min():.4f} -> {'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'}")
print(f" riferimento 2024+ di §21 (Binance vs Hyperliquid): 0,9991 / dSharpe -0,025")
su = alive(X.xsec_engine(PUh, phase=0))
mg = A.marginal_vs_tp01(win(su, T2_START, T2_END))
print(f" marginal_vs_tp01 sullo sleeve IBRIDO fuori campione: {mg.get('marginal_verdict')} "
f"(corr {mg.get('corr_full')}, is_hedge {mg.get('is_hedge')}, "
f"insample_edge {mg.get('has_insample_edge')})")
imp = A.implausible_sharpe(win(su, T2_START, T2_END))
print(f" implausible_sharpe (ibrido OOS): {imp['implausible']} Sh {imp.get('sharpe'):+.2f} "
f"perdite su barre attive {imp.get('loss_frac', float('nan'))*100:.1f}%")
print(" causalita': NON RI-GIRATA — il motore e' importato invariato da r0822b_xs01_oos.py,")
print(" che la verifica (prezzi futuri randomizzati -> max|diff| = 0). Cambiare il venue")
print(" cambia i dati, non l'ordine temporale in cui il motore li legge.")
print(" deflated_sharpe: NON PERTINENTE — qui non si SELEZIONA una cella, si confronta due")
print(" volte lo STESSO meccanismo su due fonti di prezzo. Nessun trial nuovo di strategia.")
print(" null de-levering: NON PERTINENTE (nessun claim di 'meno drawdown').")
print(" anchor: girato per costruzione — ogni numero sopra e' su tutte e 10 le fasi.")
print(f"\n[fine] {time.time()-t0:.1f}s")
if __name__ == "__main__":
main()
+637
View File
@@ -0,0 +1,637 @@
"""r0822b_prop_recal — PROP-ALLOC RIFATTO con XS01 preso dalla STORIA LUNGA (2026-08-22, ondata 2).
IL BUCO CHE CHIUDE
------------------
Il risultato di testa dell'ondata (RESULTS-0822 §1, PROP-ALLOC) e': su un conto FUNDED da $100k
l'allocazione ottima sotto barriera di drawdown mette **XS01 al 50%** e porta
J = P(pass) x P(vivo 12m) da 0,335 (libro live 75/25) a **0,738**, con
**P(>=50 EUR/g) da 600 EUR in 36 mesi = 42%**. Quel filone ha dichiarato da solo il proprio limite:
"tutto il vantaggio poggia sul drift di XS01 misurato sulla sua finestra di scoperta"
e ha proposto un gate che era un'ATTESA: *non aprire un funded prima che XS01 abbia una finestra
fuori dal 2024-2026*. Il filone XS01-OOS (RESULTS-0822 §21) ha trovato quella finestra lo stesso
giorno — stessi 19 ticker su Binance spot USDT 1d, **2021-01 -> 2023-12, con LUNA e FTX dentro** —
e ha misurato che l'edge di RENDIMENTO regge (mediana di fase +1,12 fuori contro +0,37 dentro,
differenza appaiata +0,668 in 10/10 fasi). Ma ha anche misurato TRE correzioni nel verso opposto:
(i) il maxDD standalone di XS01 RADDOPPIA: 10,8% -> 20,9-22,0%;
(ii) il peso 0,50 non sopravvive: fuori campione a w=0,50 il maxDD di libro risale a 8,9%
contro 6,0% a w=0,25 — cioe' restituisce la protezione di DD che e' l'UNICA ragione per
mettere XS01 su un conto a barriera;
(iii) il 32% dell'universo non e' testabile (ARB/OP/SUI/APT/SEI/TIA non quotati nel 2021-23) e
su di esso vive gran parte del numero di scoperta (19 gambe 1,31 vs 13 gambe 0,54).
Questo script rifa' il conto di PROP-ALLOC con la serie LUNGA al posto di quella di scoperta e
risponde a UNA domanda: **J regge?**
LA PREVISIONE, REGISTRATA PRIMA DI MISURARE
-------------------------------------------
Mi aspetto che J SCENDA ma resti sopra il libro live, per tre ragioni che tirano in versi diversi:
(a) il drift di XS01 fuori campione e' PIU' ALTO -> spinge J su;
(b) il suo maxDD raddoppia e la barriera e' STATICA (una soglia assoluta non e' scale-invariante)
-> spinge J giu', e su un conto a barriera questa e' la forza che conta;
(c) la finestra lunga contiene il 2022, quindi TP01 (lo sleeve DIFENSIVO, sotto-pesato da
PROP-ALLOC su un campione senza sinistro) ci arriva con un curriculum diverso -> mi aspetto
che l'argmax RIALZI TP01 e ABBASSI XS01 rispetto al 25/25/50.
Previsione numerica dichiarata prima: argmax con w_XS in [0,25 ; 0,38], J fra 0,50 e 0,70,
P(>=50/g) fra il 10% e il 35%. Se J restasse >= 0,70 con w_XS = 0,50 la previsione e' sbagliata e
lo dico; se J crollasse sotto il livello del libro live, il fronte funded va ripensato ADESSO.
DISCIPLINA DI RIPRODUZIONE (non negoziabile)
--------------------------------------------
Prima di pubblicare un numero nuovo la macchina deve riprodurre quello vecchio. §0 rigira
PROP-ALLOC con la sua stessa macchineria (`r0822_prop_alloc` IMPORTATO, non riscritto), stessi
semi, stessa griglia, stesso de-luck, e confronta con i valori PUBBLICATI, cablati qui sotto in
`PUB`. Se la riproduzione fallisce, quello E' il risultato e viene detto.
CONVENZIONI DICHIARATE
----------------------
* Lente **ACCOPPIATA obbligatoria**. Su una regola di DD a UN GIORNO la lente close-only e'
esattamente CIECA (misurato oggi: 0,00 breach contro 0,40-1,21 veri, rapporto INF). La
macchineria del wick e' quella di `r0725_prop_coupled` (crypto: minimo ESATTO sul path orario
condiviso TP01+SKH01; XS01: 4 checkpoint O/estremo/estremo/C con ordinamento CONDIVISO e ordine
AVVERSO dagli OHLC giornalieri). Per la finestra 2021-23 gli OHLC sono quelli **Binance** della
cache di `r0822b_xs01_oos` — il costruttore di tuple e' lo STESSO codice e viene validato in §1
contro `pc.xsec_daily_tuples()` sui dati HL, dove deve dare max|diff| = 0.
* **Universo dichiarato: U13** (i 13 dei 19 quotati prima del 2020-11) come serie PRIMARIA, perche'
e' l'unica testabile fuori campione e perche' e' la versione **PIU' DEBOLE** di quella che sta nel
libro (13 gambe invece di 19: XS01-OOS misura 0,54 contro 1,31 sulla stessa finestra e venue).
Scegliere la piu' debole e' conservativo per la decisione. U19-EXP (universo che si allarga da 10
a 19 gambe) e' riportato come **altro capo della banda**, mai come numero di testa.
* Meccanismo **CONGELATO** (XS_CFG: blend [30,90], H=10, k=5, gate p30, vol-target 20%, 5 bps/lato).
Nessun parametro toccato, nessuna cella cercata: qui non si seleziona, si RI-MISURA.
* de-luck sul drift **x0,89** (misurato 26/07), identico a PROP-ALLOC.
* **Distorsioni portate avanti DICHIARATE, non rimosse in silenzio:**
(1) la banda d'ancora NON e' girata sotto questa lente (fuori budget il 22/08 e fuori budget
qui): l'ottimo gira sull'ancora canonica e si appoggia a SKH01, che il progetto misura come
lo sleeve con la fortuna d'ancora piu' grande da restituire -> la distorsione e' A FAVORE
del risultato. Lo stress SKH x0,50 / x0,33 e' li' apposta.
(2) l'intero risultato poggia sull'assunto — **MAI VERIFICATO CON UNA FIRM** — che una firm
crypto listi i 19 (qui 13) alt Hyperliquid **con short abilitato**. Il critico dell'ondata
lo chiama "la ripetizione esatta dell'errore GTAA01/PRIIPs". Non e' verificabile da qui
(serve leggere un listino), quindi §6 misura quanto del risultato MUORE se e' falso.
* Monte Carlo: 2500 percorsi per J (come PROP-ALLOC), 1500 per la scala di conti. **Un Monte Carlo
ha una risoluzione e va detta**: SE su un livello di J ~ +-0,010, sulle differenze APPAIATE
(indici di bootstrap condivisi) molto meno, ~+-0,007.
Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822b_prop_recal.py`
Rete: NESSUNA (tutto da disco: parquet certificati + cache Binance di r0822b_xs01_oos).
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import r0725_prop_coupled as pc # noqa: E402 macchineria del wick accoppiato (25/07)
import r0725_prop_ladder as pl # noqa: E402 regole delle firm + scala di conti
import r0822_prop_alloc as PA # noqa: E402 IL filone da riprodurre (22/08)
import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance (22/08)
from src.portfolio.sleeves import XS_UNIVERSE, _HL_DIR # noqa: E402
RAW = ROOT / "data" / "raw"
FEE_SIDE = 0.001 / 2.0
HYRO = "HYRO"
# ---------------------------------------------------------------- numeri PUBBLICATI da riprodurre
# (RESULTS-0822 §1, tabella "1 — PROP-ALLOC"; log della corsa nello scratchpad)
PUB = dict(
argmaxJ=dict(w="25/25/50", lev=0.750, J=0.738, p_pass=0.822, p_alive=0.898, sharpe=1.69),
argmaxS=dict(w="38/25/38", lev=0.750, J=0.721, p_pass=0.773, p_alive=0.933, sharpe=1.73),
live=dict(w="75/25/0", lev=0.500, J=0.335, p_pass=0.386, p_alive=0.868, sharpe=1.09),
dJ_vs_sharpe=0.0164, dJ_vs_live=0.3996,
ladder_med=39.28, ladder_p50=0.417, ladder_zero=0.139,
)
LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50)
STEP = 8
N_PATHS = PA.N_PATHS # 2500
EV_H = FU_H = 365
SEED_EV, SEED_FU = 8220, 8221 # identici a PROP-ALLOC
W3 = PA.CRYPTO_PROP # ("TP01","SKH01","XS01")
TRIALS = 0 # contatore della griglia dichiarata (al RIALZO)
def bump(n: int) -> None:
global TRIALS
TRIALS += n
def hr(t: str = "") -> None:
print("\n" + "-" * 104)
if t:
print(f" {t}")
print("-" * 104)
# =============================================================== 1. XS01 ACCOPPIATO, QUALSIASI DATO
def ohlc_panels(loader, syms: list[str]) -> tuple[pd.DataFrame, ...]:
"""(C, O, H, L) allineati. `loader(sym) -> DataFrame con timestamp/open/high/low/close`."""
o, h, lo, c = {}, {}, {}, {}
for s in syms:
d = loader(s)
i = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
o[s] = pd.Series(d["open"].astype(float).values, index=i)
h[s] = pd.Series(d["high"].astype(float).values, index=i)
lo[s] = pd.Series(d["low"].astype(float).values, index=i)
c[s] = pd.Series(d["close"].astype(float).values, index=i)
C = pd.concat(c, axis=1).sort_index()
return C, pd.concat(o, axis=1).reindex(C.index)[C.columns], \
pd.concat(h, axis=1).reindex(C.index)[C.columns], \
pd.concat(lo, axis=1).reindex(C.index)[C.columns]
def xs_tuples(C: pd.DataFrame, O: pd.DataFrame, H: pd.DataFrame, L: pd.DataFrame,
phase: int = 0, wick_mode: str = "shared", **kw) -> pd.DataFrame:
"""(R, m) giornalieri di XS01 su un pannello QUALSIASI, con la stessa convenzione di wick di
`r0725_prop_coupled.xsec_daily_tuples`. Le posizioni vengono dal motore CONGELATO di
`r0822b_xs01_oos.xsec_engine` (che regge i NaN = asset non ancora quotato).
Validato in §1 contro `pc.xsec_daily_tuples()` sui dati HL: deve dare max|diff| = 0."""
det: dict = {}
r = X.xsec_engine(C, phase=phase, detail=det, **kw)
W, scale, turn = det["W"], det["scale"], det["turn"]
px = C.values.astype(float)
n, A = px.shape
prev_c = np.vstack([np.full((1, A), np.nan), px[:-1]])
held = np.vstack([np.zeros((1, A)), W[:-1]]) # gambe TENUTE nel giorno i
with np.errstate(invalid="ignore"):
rO = O.values / prev_c - 1.0
rH = H.values / prev_c - 1.0
rL = L.values / prev_c - 1.0
rC = px / prev_c - 1.0
vO = np.nansum(held * rO, axis=1)
vH = np.nansum(held * rH, axis=1)
vL = np.nansum(held * rL, axis=1)
vC = np.nansum(held * rC, axis=1)
if wick_mode == "perleg": # bound severo: ogni gamba al suo peggio
worst = np.where(held >= 0, rL, rH)
m_gross = np.minimum(np.nansum(held * worst, axis=1), vC)
else: # 'shared': ordine AVVERSO
m_gross = np.minimum(np.minimum.reduce([vO, vL, vH, vC]),
np.minimum.reduce([vO, vH, vL, vC]))
m_net = (m_gross - turn * FEE_SIDE) * scale
R_net = r.values
out = pd.DataFrame({"R": R_net, "m": np.minimum(m_net, R_net)}, index=C.index)
return out
def bin_loader(sym: str) -> pd.DataFrame:
"""Legge dalla cache di r0822b (nessuna rete) e SCARTA la barra del giorno in corso: una barra
giornaliera parziale in coda e' esattamente il difetto trovato oggi nei monitor forward."""
d = X.fetch_binance_daily(sym)
today = pd.Timestamp.utcnow().normalize().value // 10**6
return d[d["timestamp"] < today].reset_index(drop=True)
def hl_loader(sym: str) -> pd.DataFrame:
return pd.read_parquet(_HL_DIR / f"hl_{sym.lower()}_1d.parquet")
# =============================================================== 2. LIBRO A 3 GAMBE, PANNELLO LIBERO
def rg(w: dict, idx: pd.DatetimeIndex, xs: pd.DataFrame, drop_xs: bool = False,
dm: tuple = ()) -> tuple[np.ndarray, np.ndarray]:
"""(R, gap) del libro TP01/SKH01/XS01 su un pannello arbitrario e con una serie XS01
arbitraria. E' `PA.book_RG` con il pannello e la serie XS01 come argomenti invece che
cablati: crypto col minimo ESATTO sul path orario condiviso (`PA._crypto`, che per uno
stress taglia il drift sulle GAMBE ORARIE prima di comporre — il minimo giornaliero non e'
separabile per gamba, quindi non si puo' tagliare a valle); il minimo di XS01 si SOMMA
(worst simultaneo, convenzione severa del 25/07). gap = min_intraday - chiusura (<= 0).
Validato bit-exact contro `PA.book_RG` in §1.
`dm` = (("XS01", 0.5), ...) taglia il DRIFT di quello sleeve lasciando la forma della coda.
⚠ Il taglio crypto usa la media sull'INTERA storia oraria (2019+), non sulla finestra: e'
la convenzione di PROP-ALLOC, tenuta identica perche' le due tabelle vanno confrontate."""
dmd = dict(dm)
w_tp, w_sk = float(w.get("TP01", 0.0)), float(w.get("SKH01", 0.0))
w_xs = 0.0 if drop_xs else float(w.get("XS01", 0.0))
R = np.zeros(len(idx))
M = np.zeros(len(idx))
if w_tp > 0 or w_sk > 0:
c = PA._crypto(w_tp, w_sk, dmd.get("TP01", 1.0), dmd.get("SKH01", 1.0)).reindex(idx)
R += np.nan_to_num(c["R"].values)
M += np.nan_to_num(c["m"].values)
if w_xs > 0:
f = xs.reindex(idx)
rv = np.nan_to_num(f["R"].values)
mv = np.nan_to_num(f["m"].values)
mult = dmd.get("XS01", 1.0)
if mult != 1.0:
cutv = (1.0 - mult) * rv.mean() # media PRIMA del taglio (come PA)
rv, mv = rv - cutv, mv - cutv
R += w_xs * rv
M += w_xs * mv
return R, np.minimum(M, R) - R
def obj_rg(R: np.ndarray, G: np.ndarray, lev: float, ev_idx, fu_idx,
firm: str = HYRO, factor: float = PA.DELUCK) -> dict:
"""`PA.objective` senza la costruzione del libro (che qui e' su un pannello diverso)."""
Rd = PA.deluck(R, factor)
e = PA.eval_sim(Rd, G, ev_idx, lev, firm)
f = PA.funded_sim(Rd, G, fu_idx, lev, firm, 100_000.0)
ann = float(Rd.mean() * 365.0)
vol = float(Rd.std() * np.sqrt(365.0))
return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"],
e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0),
vol=vol, drift=ann, jvec=(e["passed"] & f["alive"]).astype(float))
def maxdd(R: np.ndarray) -> float:
eq = np.cumprod(1.0 + R)
return float((eq / np.maximum.accumulate(eq) - 1.0).min())
def scan(grid: list[dict], idx, xs, ev_idx, fu_idx, drop_xs: bool = False,
dm: tuple = ()) -> pd.DataFrame:
rows = []
for w in grid:
R, G = rg(w, idx, xs, drop_xs=drop_xs, dm=dm)
dd = maxdd(PA.deluck(R))
for lev in LEVS:
o = obj_rg(R, G, lev, ev_idx, fu_idx)
rows.append(dict(w=PA.wkey(w), lev=lev, maxdd=dd,
**{k: v for k, v in o.items() if k != "jvec"}))
bump(len(grid) * len(LEVS))
return pd.DataFrame(rows)
def best_row(df: pd.DataFrame, key: str = "J") -> pd.Series:
return df.loc[df[key].idxmax()]
def wdict(key: str) -> dict:
return dict(zip(W3, [int(x) / 100 for x in key.split("/")]))
# =============================================================== main
def main() -> None:
try:
sys.stdout.reconfigure(line_buffering=True)
except Exception: # pragma: no cover
pass
t0 = time.time()
def tick(lab: str) -> None:
print(f" [{time.time()-t0:6.0f}s] {lab}")
print("=" * 104)
print(" PROP-RECAL — J regge se XS01 viene dalla STORIA LUNGA invece che dalla sua finestra"
" di scoperta?")
print("=" * 104)
print(" PREVISIONE REGISTRATA PRIMA: argmax con w_XS in [0.25, 0.38], J in [0.50, 0.70],")
print(" P(>=50 EUR/g) in [10%, 35%]. Il verso atteso: drift XS piu' alto (spinge su) contro")
print(" maxDD raddoppiato + barriera STATICA + 2022 dentro (spingono giu'; su un conto a")
print(" barriera la seconda forza pesa di piu').")
# ================================================== §0. RIPRODUZIONE
hr("§0. RIPRODUZIONE — la macchina deve dare i numeri PUBBLICATI prima di darne di nuovi")
idx_pub = PA._panel_index(PA.START_WIN)
ev_pub = PA.boot_idx(len(idx_pub), EV_H, N_PATHS, seed=SEED_EV)
fu_pub = PA.boot_idx(len(idx_pub), FU_H, N_PATHS, seed=SEED_FU)
print(f" pannello 5-sleeve di PROP-ALLOC: {len(idx_pub)} giorni "
f"{idx_pub[0].date()} -> {idx_pub[-1].date()} | de-luck x{PA.DELUCK} | "
f"{N_PATHS} percorsi | semi {SEED_EV}/{SEED_FU}")
grid = PA.simplex_grid(W3, STEP)
t = time.time()
Gpub = PA.grid_scan(grid, LEVS, HYRO, ev_pub, fu_pub)
bump(len(grid) * len(LEVS))
print(f" griglia {len(grid)} pesi x {len(LEVS)} leve = {len(grid)*len(LEVS)} celle "
f"({time.time()-t:.0f}s)")
b_pub = best_row(Gpub)
ws = Gpub.loc[Gpub["sharpe"].idxmax(), "w"]
bs_pub = best_row(Gpub[Gpub["w"] == ws])
bl_pub = best_row(Gpub[Gpub["w"] == "75/25/0"])
print(f"\n {'':>18} {'pesi':>10} {'leva':>6} {'J':>7} {'PUB':>7} {'d':>7} | "
f"{'P(pass)':>8} {'PUB':>7} | {'P(vivo)':>8} {'PUB':>7} | {'Sharpe':>7} {'PUB':>6}")
ok = True
for lab, row, p in (("argmax J", b_pub, PUB["argmaxJ"]), ("argmax Sharpe", bs_pub, PUB["argmaxS"]),
("libro LIVE 75/25", bl_pub, PUB["live"])):
same_w = (row["w"] == p["w"]) and abs(row["lev"] - p["lev"]) < 1e-9
dj = row["J"] - p["J"]
ok &= same_w and abs(dj) < 0.02
print(f" {lab:>18} {row['w']:>10} {row['lev']:>6.3f} {row['J']:>7.3f} {p['J']:>7.3f} "
f"{dj:>+7.3f} | {row['p_pass']:>8.1%} {p['p_pass']:>7.1%} | "
f"{row['p_alive']:>8.1%} {p['p_alive']:>7.1%} | {row['sharpe']:>7.2f} {p['sharpe']:>6.2f}"
f"{'' if same_w else ' <-- CELLA DIVERSA'}")
dA, seA = PA.paired_delta(wdict(b_pub["w"]), float(b_pub["lev"]),
wdict(bs_pub["w"]), float(bs_pub["lev"]), HYRO, ev_pub, fu_pub)
dB, seB = PA.paired_delta(wdict(b_pub["w"]), float(b_pub["lev"]),
{"TP01": 0.75, "SKH01": 0.25}, float(bl_pub["lev"]),
HYRO, ev_pub, fu_pub)
bump(4)
print(f"\n differenze APPAIATE argmaxJ - argmaxSharpe {dA:+.4f} +-{seA:.4f} "
f"(PUB {PUB['dJ_vs_sharpe']:+.4f})")
print(f" argmaxJ - LIVE {dB:+.4f} +-{seB:.4f} "
f"(PUB {PUB['dJ_vs_live']:+.4f})")
ok &= abs(dA - PUB["dJ_vs_sharpe"]) < 0.02 and abs(dB - PUB["dJ_vs_live"]) < 0.03
pc.DELUCK = PA.DELUCK
r_lad = pc.simulate([tuple(wdict(b_pub["w"]).get(n, 0.0) for n in W3)],
[0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75, seed=20260822,
lens="coupled", start=PA.START_WIN)
print(f" scala di conti (I-bis) EUR/g med {r_lad['med']:.2f} (PUB {PUB['ladder_med']:.2f}) | "
f"P(>=50/g) {r_lad['p_target']:.1%} (PUB {PUB['ladder_p50']:.1%}) | "
f"P(zero) {r_lad['p_zero']:.1%} (PUB {PUB['ladder_zero']:.1%})")
ok &= abs(r_lad["p_target"] - PUB["ladder_p50"]) < 0.03
verdetto = "OK — si procede" if ok else "FALLITA — ogni numero sotto va letto con questa riserva"
print(f"\n >>> RIPRODUZIONE: {verdetto}")
tick("§0 riproduzione")
# ================================================== §1. LA SERIE LUNGA, ACCOPPIATA
hr("§1. LA SERIE LUNGA DI XS01, CON IL WICK — e la validazione del costruttore di tuple")
Chl, Ohl, Hhl, Lhl = ohlc_panels(hl_loader, [s for s in XS_UNIVERSE
if (_HL_DIR / f"hl_{s.lower()}_1d.parquet").exists()])
Chl = Chl.dropna()
Ohl, Hhl, Lhl = Ohl.reindex(Chl.index), Hhl.reindex(Chl.index), Lhl.reindex(Chl.index)
mine = xs_tuples(Chl, Ohl, Hhl, Lhl, phase=0)
ref = pc.xsec_daily_tuples("shared")
j = mine.join(ref, lsuffix="_m", rsuffix="_r", how="inner").dropna()
dR = float(np.abs(j["R_m"] - j["R_r"]).max())
dM = float(np.abs(j["m_m"] - j["m_r"]).max())
print(f" CONTROLLO: il mio costruttore di tuple, alimentato con gli OHLC HL, contro")
print(f" `pc.xsec_daily_tuples()` (la macchineria del 25/07): n={len(j)} "
f"max|dR| = {dR:.2e} max|dm| = {dM:.2e} -> "
f"{'IDENTICO' if max(dR, dM) < 1e-12 else 'DIVERGE — STOP'}")
if max(dR, dM) >= 1e-12:
print(" !! il costruttore non replica: i numeri sulla storia lunga sarebbero di un'altra")
print(" macchina. Mi fermo qui.")
return
for wtest in ({"TP01": 0.25, "SKH01": 0.25, "XS01": 0.50}, {"TP01": 0.75, "SKH01": 0.25}):
Ra, Ga = rg(wtest, idx_pub, ref)
Rb_, Gb_ = PA.book_RG(wtest)
dd1 = float(np.abs(Ra - Rb_).max()); dd2 = float(np.abs(Ga - Gb_).max())
print(f" CONTROLLO: il mio costruttore di LIBRO contro `PA.book_RG` "
f"({PA.wkey(wtest)}): max|dR| {dd1:.2e} max|dgap| {dd2:.2e} -> "
f"{'IDENTICO' if max(dd1, dd2) < 1e-12 else 'DIVERGE'}")
Cb, Ob, Hb, Lb = ohlc_panels(bin_loader, XS_UNIVERSE)
p13 = Cb[X.U13].dropna()
o13, h13, l13 = Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], Lb.reindex(p13.index)[X.U13]
xs13 = xs_tuples(p13, o13, h13, l13, phase=0)
xsEXP = xs_tuples(Cb, Ob, Hb, Lb, phase=0)
bump(2)
first13 = xs13.index[np.flatnonzero(xs13["R"].values != 0.0)[0]]
firstEX = xsEXP.index[np.flatnonzero(xsEXP["R"].values != 0.0)[0]]
print(f"\n XS01 U13 (13 gambe fisse, Binance) prima barra ATTIVA {first13.date()} "
f"-> {xs13.index[-1].date()}")
print(f" XS01 U19-EXP (10 -> 19 gambe, Binance) prima barra ATTIVA {firstEX.date()} "
f"-> {xsEXP.index[-1].date()}")
print(" (barra del giorno in corso SCARTATA: una giornaliera parziale in coda e' il difetto")
print(" trovato oggi nei monitor forward)")
# finestre dichiarate
cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index
base_idx = cry_idx.intersection(xs13.index)
S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC"))
S_DISC = pd.Timestamp(PA.START_WIN, tz="UTC")
I_LONG = base_idx[base_idx >= S_LONG]
I_OOS = I_LONG[I_LONG < S_DISC]
I_DISC = base_idx[base_idx >= S_DISC]
WINDOWS = (("W-LONG 2021+ (OOS + scoperta)", I_LONG),
("W-OOS 2021-2023 (puro OOS)", I_OOS),
("W-DISC 2024+ (scoperta)", I_DISC))
print(f"\n {'finestra':>32} {'giorni':>7} {'da':>12} {'a':>12}")
for lab, ii in WINDOWS:
print(f" {lab:>32} {len(ii):>7d} {str(ii[0].date()):>12} {str(ii[-1].date()):>12}")
tick("§1 serie lunga")
# ================================================== §2. PROFILO SOTTO BARRIERA
hr("§2. IL PROFILO CHE CAMBIA — cio' che una barriera STATICA vede: drift, vol, coda, gap")
print(" (de-luckato x0.89; `gap peggiore` = l'escursione intraday che la close-only NON vede)")
print(f" {'sleeve / finestra':>34} {'Sharpe':>7} {'vol a':>7} {'drift a':>8} {'maxDD':>7} "
f"{'g.peggiore':>11} {'gap p90':>9} {'gap peggiore':>13}")
prof_rows = [
("TP01 W-LONG", {"TP01": 1.0}, I_LONG, xs13),
("TP01 W-DISC", {"TP01": 1.0}, I_DISC, xs13),
("SKH01 W-LONG", {"SKH01": 1.0}, I_LONG, xs13),
("SKH01 W-DISC", {"SKH01": 1.0}, I_DISC, xs13),
("XS01 U13 Binance W-OOS", {"XS01": 1.0}, I_OOS, xs13),
("XS01 U13 Binance W-DISC", {"XS01": 1.0}, I_DISC, xs13),
("XS01 U19EXP Binance W-OOS", {"XS01": 1.0}, I_OOS, xsEXP),
("XS01 UFFICIALE HL W-DISC", {"XS01": 1.0}, I_DISC, ref),
]
for lab, w, ii, xf in prof_rows:
R, G = rg(w, ii, xf)
Rd = PA.deluck(R)
vol = Rd.std() * np.sqrt(365.0)
print(f" {lab:>34} {Rd.mean()*365/vol if vol > 0 else 0:>7.2f} {vol:>6.1%} "
f"{Rd.mean()*365:>8.1%} {maxdd(Rd)*100:>6.1f}% {Rd.min():>11.2%} "
f"{-np.percentile(-G, 90)*100:>8.2f}pp {G.min()*100:>12.2f}pp")
print("\n Le righe da leggere insieme sono XS01 W-OOS e XS01 UFFICIALE: il RENDIMENTO regge")
print(" (anzi sale), ma il maxDD e la coda peggiorano — e la barriera guarda la coda.")
tick("§2 profilo")
# ================================================== §3. LA GRIGLIA RIFATTA
hr("§3. LA GRIGLIA RIFATTA SULLA STORIA LUNGA — stesso simplesso, stesse leve, stessa lente")
res = {}
for lab, ii in WINDOWS:
ev = PA.boot_idx(len(ii), EV_H, N_PATHS, seed=SEED_EV)
fu = PA.boot_idx(len(ii), FU_H, N_PATHS, seed=SEED_FU)
t = time.time()
Gx = scan(grid, ii, xs13, ev, fu)
res[lab] = dict(G=Gx, ev=ev, fu=fu, idx=ii)
b, bl = best_row(Gx), best_row(Gx[Gx["w"] == "75/25/0"])
print(f"\n {lab} ({time.time()-t:.0f}s)")
print(f" {'':>18} {'pesi':>10} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} "
f"{'Sharpe':>7} {'maxDD':>7} {'E[pay/a]':>10}")
for l2, r2 in (("argmax J", b), ("libro LIVE 75/25", bl)):
print(f" {l2:>18} {r2['w']:>10} {r2['lev']:>6.3f} {r2['J']:>7.3f} "
f"{r2['p_pass']:>8.1%} {r2['p_alive']:>8.1%} {r2['sharpe']:>7.2f} "
f"{r2['maxdd']*100:>6.1f}% {r2['e_payout']:>9,.0f}$")
d, se = None, None
try:
Rb, Gb = rg(wdict(b["w"]), ii, xs13)
Rl, Gl = rg({"TP01": 0.75, "SKH01": 0.25}, ii, xs13)
a1 = obj_rg(Rb, Gb, float(b["lev"]), ev, fu)["jvec"]
b1 = obj_rg(Rl, Gl, float(bl["lev"]), ev, fu)["jvec"]
dd_ = a1 - b1
d, se = float(dd_.mean()), float(dd_.std(ddof=1) / np.sqrt(len(dd_)))
bump(2)
except Exception as exc: # pragma: no cover
print(f" delta appaiato non calcolato: {exc}")
if d is not None:
print(f" delta APPAIATO argmax - LIVE : {d:+.4f} +-{se:.4f}"
f" (PROP-ALLOC pubblicava {PUB['dJ_vs_live']:+.4f} su W-DISC con XS01 HL)")
tick("§3 griglie")
# ================================================== §4. LA REGIONE, NON IL PUNTO
hr("§4. IL PESO DI XS01 — c'e' una REGIONE robusta su ENTRAMBE le finestre?")
print(" PROP-ALLOC ha gia' misurato che il PUNTO non conta (il massimo di 200 pesi casuali")
print(" eguagliava l'argmax): la domanda giusta e' se esiste una regione comune. Per ogni")
print(" livello di w_XS si riporta il MEGLIO su TP/SKH e su tutte le leve.")
xs_levels = sorted({int(round(100 * (i / STEP))) for i in range(STEP + 1)})
print(f"\n {'w_XS':>6} | " + " | ".join(f"{lab.split()[0]:>26}" for lab, _ in WINDOWS))
print(f" {'':>6} | " + " | ".join(f"{'J':>7} {'maxDD':>7} {'pesi':>10}" for _ in WINDOWS))
prof_by_w = {lab: {} for lab, _ in WINDOWS}
for wx in xs_levels:
cells = []
for lab, _ in WINDOWS:
Gx = res[lab]["G"]
sub = Gx[Gx["w"].str.endswith(f"/{wx}")]
if len(sub) == 0:
cells.append(f"{'':>7} {'':>7} {'':>10}")
continue
r2 = best_row(sub)
prof_by_w[lab][wx] = float(r2["J"])
cells.append(f"{r2['J']:>7.3f} {r2['maxdd']*100:>6.1f}% {r2['w']:>10}")
print(f" {wx:>5}% | " + " | ".join(cells))
print("\n Regione robusta = livelli di w_XS entro il 3% del meglio di finestra, su TUTTE e tre:")
robust = []
for wx in xs_levels:
okall = True
for lab, _ in WINDOWS:
d = prof_by_w[lab]
if wx not in d or d[wx] < 0.97 * max(d.values()):
okall = False
if okall:
robust.append(wx)
print(f" {robust if robust else 'NESSUN livello comune'}"
f" (entro il 5%: "
f"{[wx for wx in xs_levels if all(wx in prof_by_w[l] and prof_by_w[l][wx] >= 0.95*max(prof_by_w[l].values()) for l, _ in WINDOWS)]})")
tick("§4 regione")
# ================================================== §5. LA BANDA ANCORATA AL MISURATO
hr("§5. LA BANDA — con gli stress ANCORATI a cio' che e' misurato, non a cio' che era assunto")
print(" PROP-ALLOC pubblicava 42% / 7,7% / 0,7% dove il caso severo era 'XS x0,00' = il drift")
print(" di XS01 assunto NULLO perche' non misurabile fuori campione. Quel drift ORA E'")
print(" MISURATO, e fuori campione e' PIU' ALTO: lo scenario cambia stato, da assunzione a")
print(" misura. Cio' che resta assunto e' (a) la fortuna d'ancora di SKH01 (misurata altrove,")
print(" non sotto questa lente) e (b) il LISTINO della firm (§6). Gli stress qui sotto sono")
print(" quelli, non un pessimismo arbitrario.")
st_rows = []
STRESS = ((), (("SKH01", 0.50),), (("SKH01", 0.33),))
SLAB = {(): "nessuno (drift pieno)",
(("SKH01", 0.50),): "SKH x0.50 (ancora 26/07)",
(("SKH01", 0.33),): "SKH x0.33 (ancora severa)"}
for lab, ii in (WINDOWS[0], WINDOWS[1]):
Gw, ev, fu = res[lab]["G"], res[lab]["ev"], res[lab]["fu"]
top_keys = list(Gw.sort_values("J", ascending=False).drop_duplicates("w")["w"].head(10))
print(f"\n {lab} (argmax ri-cercato sulle {len(top_keys)} celle-peso migliori: e' un")
print(f" risparmio di calcolo, ed e' CONSERVATIVO per la stabilita' dell'ottimo)")
print(f" {'stress':>28} {'argmax pesi':>12} {'leva':>6} {'J':>7} {'J del LIVE':>11} "
f"{'delta appaiato':>16}")
for dmt in STRESS:
best_j, best_k, best_l, best_v = -1.0, None, None, None
for k in top_keys:
R, G = rg(wdict(k), ii, xs13, dm=dmt)
for lv in LEVS:
o = obj_rg(R, G, lv, ev, fu)
if o["J"] > best_j:
best_j, best_k, best_l, best_v = o["J"], k, lv, o["jvec"]
bump(len(top_keys) * len(LEVS))
RL, GL = rg({"TP01": 0.75, "SKH01": 0.25}, ii, xs13, dm=dmt)
oL, lvL = None, None
for lv in LEVS:
o = obj_rg(RL, GL, lv, ev, fu)
if oL is None or o["J"] > oL["J"]:
oL, lvL = o, lv
bump(len(LEVS))
d_ = best_v - oL["jvec"]
# guardia contro l'errore del 22/08: un delta appaiato DEVE ereditare lo stress
# della riga in cui compare, o confronta due configurazioni non stressate.
assert abs(float(d_.mean()) - (best_j - oL["J"])) < 0.05, "delta appaiato incoerente"
print(f" {SLAB[dmt]:>28} {best_k:>12} {best_l:>6.3f} {best_j:>7.3f} "
f"{oL['J']:>11.3f} {d_.mean():>+11.4f} "
f"+-{d_.std(ddof=1)/np.sqrt(len(d_)):.4f}")
st_rows.append((lab, SLAB[dmt], best_k, best_l, best_j, oL["J"]))
tick("§5 banda")
# ================================================== §6. XS01 ASSENTE
hr("§6. LO SCENARIO CHE UCCIDE TUTTO — la firm NON lista gli alt (assunto mai verificato)")
print(" Non e' uno stress sul drift: e' la RIMOZIONE dello sleeve. Il libro torna a due gambe")
print(" e il conto funded diventa il libro live a leva. E' l'unico scenario che non si misura")
print(" con un backtest — si verifica leggendo un listino — quindi qui se ne misura il DANNO.")
print(f"\n {'finestra':>28} {'con XS01: pesi':>15} {'lev':>5} {'J':>7} | "
f"{'SENZA XS01: pesi':>17} {'lev':>5} {'J':>7} | {'J perso':>8} {'quota':>7}")
absent = {}
for lab, ii in WINDOWS:
Gx, ev, fu = res[lab]["G"], res[lab]["ev"], res[lab]["fu"]
b = best_row(Gx)
sub2 = Gx[Gx["w"].str.endswith("/0")]
b2 = best_row(sub2)
absent[lab] = b2
lost = float(b["J"]) - float(b2["J"])
print(f" {lab:>28} {b['w']:>15} {b['lev']:>5.2f} {b['J']:>7.3f} | "
f"{b2['w']:>17} {b2['lev']:>5.2f} {b2['J']:>7.3f} | {lost:>+8.3f} "
f"{lost/max(b['J'],1e-9):>6.0%}")
print("\n NB: 'SENZA XS01' e' gia' ri-ottimizzato su TP/SKH e su tutte le leve — e' il MEGLIO")
print(" che il libro a due gambe puo' fare, non il 75/25 cablato.")
tick("§6 assenza")
# ================================================== §7. LA SCALA DI CONTI
hr("§7. LA SCALA DI CONTI — P(>=50 EUR/g) da EUR 600 in 36 mesi, lente ACCOPPIATA")
print(f" macchineria del 25/07 (`pc.simulate`), de-luck x{PA.DELUCK}, {pl.MAX_CONCURRENT} conti"
f" max, morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, 1500 percorsi")
print(" (le tuple del libro vengono INIETTATE nella cache di `pc.book_tuples`, cosi' la")
print(" simulazione e' letteralmente la stessa, con un input diverso)")
print(f"\n {'finestra':>28} {'allocazione':>26} {'leva':>6} {'EUR/g med':>10} "
f"{'P(>=10/g)':>10} {'P(>=50/g)':>10} {'P(zero)':>8}")
def ladder(ii, w: dict, lev: float, tag: str, wlab: str, drop_xs: bool = False) -> dict:
R, G = rg(w, ii, xs13, drop_xs=drop_xs)
cfg = (float(w.get("TP01", 0.0)), float(w.get("SKH01", 0.0)),
0.0 if drop_xs else float(w.get("XS01", 0.0)))
key = (cfg[0], cfg[1], cfg[2], "shared")
saved = pc._TUP_CACHE.pop(key, None)
pc._TUP_CACHE[key] = (R, G, ii)
r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=lev,
seed=20260822, lens="coupled", start=str(ii[0].date()))
pc._TUP_CACHE.pop(key, None)
if saved is not None:
pc._TUP_CACHE[key] = saved
print(f" {tag:>28} {wlab:>26} {lev:>6.2f} {r['med']:>10.2f} {r['p_ge10']:>10.1%} "
f"{r['p_target']:>10.1%} {r['p_zero']:>8.1%}")
return r
lad = {}
for lab, ii in WINDOWS:
Gx = res[lab]["G"]
b = best_row(Gx)
b2 = absent[lab]
short = lab.split()[0]
lad[(short, "argmax")] = ladder(ii, wdict(b["w"]), float(b["lev"]), short,
f"argmax {b['w']}")
lad[(short, "2550")] = ladder(ii, wdict("25/25/50"), 0.75, short,
"25/25/50 (PROP-ALLOC)")
lad[(short, "noxs")] = ladder(ii, wdict(b2["w"]), float(b2["lev"]), short,
f"SENZA XS01 {b2['w']}", drop_xs=True)
lad[(short, "live")] = ladder(ii, {"TP01": 0.75, "SKH01": 0.25}, 0.75, short,
"LIVE 75/25 @0.75x")
bump(12)
tick("§7 scala di conti")
# ================================================== §8. SINTESI + GATE
hr("§8. SINTESI — la banda da citare, e il gate da RI-REGISTRARE")
bL = best_row(res[WINDOWS[0][0]]["G"])
bO = best_row(res[WINDOWS[1][0]]["G"])
bD = best_row(res[WINDOWS[2][0]]["G"])
print(f" J e argmax per finestra (XS01 = U13 Binance in tutte e tre, quindi la SOLA cosa che")
print(f" cambia fra le righe e' la FINESTRA):")
print(f" W-DISC 2024+ argmax {bD['w']:>10} @{bD['lev']:.2f}x J {bD['J']:.3f}")
print(f" W-OOS 2021-2023 argmax {bO['w']:>10} @{bO['lev']:.2f}x J {bO['J']:.3f}")
print(f" W-LONG 2021+ argmax {bL['w']:>10} @{bL['lev']:.2f}x J {bL['J']:.3f}")
print(f" (PROP-ALLOC pubblicato, XS01 HL 19 gambe, W-DISC: 25/25/50 @0.75x J 0.738)")
print(f"\n P(>=50 EUR/g) in 36 mesi, banda onesta:")
for short in ("W-LONG", "W-OOS", "W-DISC"):
row = " | ".join(f"{k}: {lad[(short, k)]['p_target']:.1%}"
for k in ("argmax", "2550", "noxs", "live") if (short, k) in lad)
print(f" {short:>8} {row}")
print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}")
print(f"\n fatto in {time.time()-t0:.0f}s")
print("=" * 104)
if __name__ == "__main__":
main()
+26 -17
View File
@@ -635,9 +635,9 @@ def main() -> None:
print(f" ESEGUIBILE (compra ali all'ask, vendi corpo al bid): "
f"{bf['viol_exec'].sum():,} = {bf['viol_exec'].mean():.4%}")
if dte_min == DTE_MIN:
bf_keep = bf
bf_keep, v_keep = bf, v
bf = bf_keep
bf, vert = bf_keep, v_keep
print("\n" + "=" * 100)
print("3. QUANTO E' GRANDE L'INCOERENZA, E QUANTO COSTA ATTRAVERSARLA")
print("=" * 100)
@@ -651,12 +651,17 @@ def main() -> None:
f"p90 {r.quantile(.9):.3f} · p99 {r.quantile(.99):.3f}")
print(f" mezzo spread denaro-lettera mediana {hs.median():.3f} pt-vol · "
f"p25 {hs.quantile(.25):.3f} · p75 {hs.quantile(.75):.3f}")
sub = x.dropna(subset=["iv_resid"])
print(f" UN TICK della griglia di prezzo mediana {tk.median():.3f} pt-vol "
f"-> l'incoerenza e' sotto il tick nel {np.mean(r.reindex(x.index) < tk):.1%} dei casi")
f"-> l'incoerenza e' sotto il tick nel "
f"{np.mean(sub['iv_resid'].abs() < sub['tick_pt']):.1%} dei casi (n={len(sub):,})")
both = x.dropna(subset=["iv_resid"])
rap = np.abs(both["iv_resid"]) / (2 * both["halfspread_pt"])
print(f" |residuo| / (spread PIENO delle gambe): mediana {rap.median():.3f} · "
f"p99 {rap.quantile(.99):.3f} · quota > 1 = {np.mean(rap > 1):.3%}")
print(f" RELATIVE VALUE (non arbitraggio): per guadagnare serve |residuo| > 1x lo spread")
print(f" anche con reversione TOTALE e uscita gratis ({np.mean(rap > 1):.3%} dei casi), "
f"> 2x se si paga anche l'uscita ({np.mean(rap > 2):.3%})")
print(f" spread relativo (ask-bid)/mid del venue: mediana {x['relspread'].median():.1%} "
f"· p25 {x['relspread'].quantile(.25):.1%}")
# valore in dollari dell'incoerenza al MID (il caso migliore possibile: fee 0, spread 0)
@@ -695,17 +700,17 @@ def main() -> None:
print("\n" + "=" * 100)
print("4. TEST C — varianza totale nel tenore (l'unico che tocca un interpolatore)")
print("=" * 100)
c = test_calendario(q, DTE_MIN)
print(f" (i) strike CONDIVISO, nessuna interpolazione: {c['i_viol']:,}/{c['i_n']:,} = {c['i_quota']:.3%}"
f" · ampiezza mediana {c['i_amp_med_pt']:.3f} pt-vol (p95 {c['i_amp_p95_pt']:.3f})")
print(f" (ii) log-moneyness FISSO, con interpolazione (1 snapshot ogni {c['ii_ogni']}): "
f"{c['ii_viol']:,}/{c['ii_n']:,} = {c['ii_quota']:.3%} · ampiezza mediana {c['ii_amp_med_pt']:.3f} pt-vol")
print(f" -> contributo dell'INTERPOLATORE = {c['ii_quota'] - c['i_quota']:+.3%} di quota di violazione")
if "i_val_usd_med" in c:
print(f" valore mediano della violazione (i) al lotto minimo ${c['i_val_usd_med']:.2f} "
f"contro un costo di 2 gambe (spread + fee) di ${c['i_costo_usd_med']:.2f}")
print(f" margine di monotonia sull'intera superficie: la iv lunga sta {-c['room_med_pt']:.2f} pt-vol "
f"SOPRA il minimo che la non-arbitraggio richiede (5% piu' stretto: {-c['room_p5_pt']:.2f}) "
cal = test_calendario(q, DTE_MIN)
print(f" (i) strike CONDIVISO, nessuna interpolazione: {cal['i_viol']:,}/{cal['i_n']:,} = {cal['i_quota']:.3%}"
f" · ampiezza mediana {cal['i_amp_med_pt']:.3f} pt-vol (p95 {cal['i_amp_p95_pt']:.3f})")
print(f" (ii) log-moneyness FISSO, con interpolazione (1 snapshot ogni {cal['ii_ogni']}): "
f"{cal['ii_viol']:,}/{cal['ii_n']:,} = {cal['ii_quota']:.3%} · ampiezza mediana {cal['ii_amp_med_pt']:.3f} pt-vol")
print(f" -> contributo dell'INTERPOLATORE = {cal['ii_quota'] - cal['i_quota']:+.3%} di quota di violazione")
if "i_val_usd_med" in cal:
print(f" valore mediano della violazione (i) al lotto minimo ${cal['i_val_usd_med']:.2f} "
f"contro un costo di 2 gambe (spread + fee) di ${cal['i_costo_usd_med']:.2f}")
print(f" margine di monotonia sull'intera superficie: la iv lunga sta {-cal['room_med_pt']:.2f} pt-vol "
f"SOPRA il minimo che la non-arbitraggio richiede (5% piu' stretto: {-cal['room_p5_pt']:.2f}) "
f"-> la superficie non e' nemmeno VICINA al bordo")
print("\n" + "=" * 100)
@@ -794,11 +799,15 @@ def main() -> None:
print("SINTESI")
print("=" * 100)
nv = int(bf["viol_exec"].sum()); nt = len(bf)
print(f" 1. La superficie e' ARBITRAGE-FREE sui prezzi eseguibili: monotonia 0/{1_111_585:,},")
print(f" 1. La superficie e' ARBITRAGE-FREE sui prezzi eseguibili: monotonia "
f"{vert['n_viol']}/{vert['n_confronti']:,},")
print(f" convessita' {nv}/{nt:,} = {nv/nt:.6%} (e quell'una vale $0,00 e sparisce l'ora dopo),")
print(f" calendario 6/107.177 con margine mediano di 13 punti di vol dal bordo.")
print(f" calendario {cal['i_viol']}/{cal['i_n']:,} con margine mediano di {-cal['room_med_pt']:.0f} "
f"punti di vol dal bordo.")
print(f" 2. L'incoerenza al MID esiste ed e' grande ({bf['viol_mid'].mean():.1%} delle triple)")
print(f" ma vale {np.abs(bf['iv_resid']).median():.3f} punti di vol contro un mezzo spread di 0,86-1,21,")
hs_b = bf[bf.asset == "BTC"]["halfspread_pt"]; hs_e = bf[bf.asset == "ETH"]["halfspread_pt"]
print(f" ma vale {np.abs(bf['iv_resid']).median():.3f} punti di vol contro un mezzo spread di "
f"{hs_b[np.isfinite(hs_b)].median():.2f}-{hs_e[np.isfinite(hs_e)].median():.2f},")
print(f" ed e' SOTTO IL TICK del venue nel 95-97% dei casi: non e' esprimibile nella griglia.")
print(f" 3. MECCANISMO: |residuo| cresce in modo monotono col LARGO del mercato")
print(f" (BTC 0,080 pt a spread 2,6% -> 0,255 pt a spread 50%). L'incoerenza al mid E' la")
+31 -10
View File
@@ -495,6 +495,10 @@ def main() -> None: # noqa: C901
a_sin = max(tab, key=lambda w: tab[w]["sin"][1]["J"])
print(f"\n argmax: 2019+ -> TP01 {a_full:.3f} | 2024+ -> TP01 {a_rec:.3f} | "
f"SOLO-SINISTRO -> TP01 {a_sin:.3f}")
print(f" ⚠️ RISOLUZIONE: un LIVELLO di J ha errore MC ~{np.sqrt(0.25/N_PATHS):.3f} "
f"(N={N_PATHS}) -> celle entro ~0.02 NON sono distinguibili, e infatti 0.375 e 0.500")
print(" si scambiano il posto fra semi diversi. La lettura e' 'la regione 0.25-0.50',")
print(" non 'il punto 0.375' (stessa lezione del null dei tilt casuali di PROP-ALLOC).")
print(" ⚠️ La colonna SOLO-SINISTRO e' una lente ESTREMA e va letta come tale: l'anno intero")
print(" e' fatto di soli blocchi di crash (4 episodi ricampionati). Non e' uno scenario")
print(" plausibile, e' il **limite superiore del danno**: se il verso della risposta e' lo")
@@ -748,8 +752,9 @@ def main() -> None: # noqa: C901
(0.125, 0.375, 0.50), (0.25, 0.25, 0.50), (0.375, 0.125, 0.50)]
com = xs.index.intersection(IDX[i_rec:])
xs_al = xs.reindex(com)
LEV_COM = 0.625 # leva comune per il blocco a XS01=50% (la minima fra le tre righe)
print(f"\n {'TP/SKH/XS':>16} {'ipotesi XS':>12} {'leva':>6} {'J normale':>10} "
f"{'P(viv|replay peggiore)':>24} {'shock di rottura':>18}")
f"{'P(viv|replay pegg.)':>21} {'idem @0.625x':>14} {'shock di rottura':>18}")
for cfg in W3:
wtp, wsk, wxs = cfg
base = wtp + wsk
@@ -767,7 +772,7 @@ def main() -> None: # noqa: C901
bfu = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8302)
lab = f"{int(round(wtp*100))}/{int(round(wsk*100))}/{int(round(wxs*100))}"
def build(hyp: str, shock: float = 0.0):
def build(hyp: str, shock: float = 0.0, lev_: float | None = None):
segsR, segsG = [], []
for t in epis:
rc = base * (crypto(wtp_n)[0][t:t + EPI_LEN] - offc)
@@ -790,16 +795,16 @@ def main() -> None: # noqa: C901
worst = 1.0
for k, g2 in enumerate(sg):
fu = splice_paths(bfu, np.arange(g2[0], g2[1]), seed=8320 + k)
worst = min(worst, J_of(Rp, Gp, evn, fu, lv)["p_alive"])
worst = min(worst, J_of(Rp, Gp, evn, fu, lv if lev_ is None else lev_)["p_alive"])
return worst
if wxs == 0:
print(f" {lab:>16} {'n/a (no XS)':>12} {lv:>6.3f} {on['J']:>10.3f} "
f"{build('A-flat'):>24.1%} {'-':>18}")
f"{build('A-flat'):>21.1%} {build('A-flat', lev_=LEV_COM):>14.1%} {'-':>18}")
continue
for hyp in ("A-flat", "B-peggiore"):
print(f" {lab:>16} {hyp:>12} {lv:>6.3f} {on['J']:>10.3f} "
f"{build(hyp):>24.1%} {'-':>18}")
f"{build(hyp):>21.1%} {build(hyp, lev_=LEV_COM):>14.1%} {'-':>18}")
lo, hi = 0.0, 0.40 # shock di rottura: bisezione su P(vivo) = 50%
for _ in range(9):
mid = 0.5 * (lo + hi)
@@ -807,11 +812,22 @@ def main() -> None: # noqa: C901
lo = mid
else:
hi = mid
print(f" {lab:>16} {'C-shock':>12} {lv:>6.3f} {on['J']:>10.3f} {'(50% per def.)':>24} "
f"{-lo:>17.1%}")
lo2, hi2 = 0.0, 0.40
for _ in range(9):
mid = 0.5 * (lo2 + hi2)
if build("C-shock", mid, lev_=LEV_COM) >= 0.50:
lo2 = mid
else:
hi2 = mid
print(f" {lab:>16} {'C-shock':>12} {lv:>6.3f} {on['J']:>10.3f} {'(50% per def.)':>21} "
f"{'(50% per def.)':>14} {-lo:>17.1%} / {-lo2:.1%} @0.625x")
print("\n Lettura: 'shock di rottura' = il crollo di UN giorno su XS01 che porta P(vivo) al")
print(" 50% quando il crash arriva. Piu' e' piccolo, piu' l'allocazione XS01-pesante e' a un")
print(" giorno di coda dalla morte. Non e' una previsione: e' la taglia della scommessa.")
print(" ⚠️ Le ultime tre righe hanno XS01 FISSO al 50% e differiscono SOLO per come si")
print(" divide il resto fra TP01 e SKH01: e' li' che si legge se TP01 fa da zavorra contro")
print(" l'ignoto di XS01. **La colonna @0.625x e' quella da leggere**, perche' a leva libera")
print(" la riga con meno TP01 sceglie una leva piu' bassa e vince per quello.")
tick("§6 XS01")
# ============================================================ §7. BANDA + VERDETTO
@@ -917,9 +933,14 @@ def main() -> None: # noqa: C901
print(" de-levering**, in una veste nuova: 'protezione dal crash' invece di 'meno DD'.")
print(" 4) Il rischio vero dell'ottimo di PROP-ALLOC NON e' TP01 sotto-pesato: e' XS01 al")
print(" 50% con un comportamento nel crash che il campione non contiene. Un solo giorno")
print(" a -8% su XS01 dimezza la sopravvivenza; con TP01 al 50% ne serve uno a -12.3%.")
print(" **Se TP01 vale qualcosa su un conto funded, vale come zavorra contro l'ignoto di")
print(" XS01, non come assicurazione contro il crash di BTC/ETH — che SKH01 fa meglio.**")
print(" a ~-10% su XS01 dimezza la sopravvivenza quando il crash arriva.")
print(" ⚠️ IPOTESI MIA, NATA E REFUTATA IN SESSIONE: avevo scritto che TP01 fa da")
print(" ZAVORRA contro quell'ignoto (a XS01 25% serviva uno shock -12.3% contro -8.0% a")
print(" XS01 50%). Il controllo a **XS01 FISSO al 50% e leva comune** lo smonta: lo shock")
print(" di rottura e' -10.1 / -10.2 / -10.2% muovendo TP01 da 12% a 38%, cioe' INVARIANTE,")
print(" e la sopravvivenza sotto l'ipotesi severa CALA (74.1 -> 72.6 -> 64.7%). Cio' che")
print(" protegge dall'ignoto di XS01 e' **meno XS01**, non piu' TP01: il primo numero")
print(" confrontava due pesi di XS01 diversi e attribuiva a TP01 il merito dell'altro.")
print("\n ⚠️ COSA MI SMENTIREBBE: un crash in cui SKH01 NON si gira short in tempo (gap")
print(" notturno oltre lo stop, o venue fermo). Il campione ne ha quattro e in tutti e")
print(" quattro SKH01 ha girato. Basterebbe un episodio in cui SKH01 perde quanto il")