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 from __future__ import annotations
import argparse import argparse
import gc
import sys import sys
import time import time
from pathlib import Path from pathlib import Path
@@ -132,6 +133,10 @@ class KeepCache:
self.rv: dict = {} self.rv: dict = {}
self.q: 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: def keep(self, ex: dict, key, thr, w: int, h: float, sg: int) -> pd.Series:
base = ex["base_daily"] base = ex["base_daily"]
k = (key, w) k = (key, w)
@@ -152,6 +157,18 @@ class KeepCache:
KC = 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 size_bin(thr, w, h, sg):
def f(ex, key): def f(ex, key):
return V.sizes_from_L(ex, KC.keep(ex, key, thr, w, h, sg)) 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: def main() -> None:
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("--every", type=int, default=1) 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() args = ap.parse_args()
offs = OFFSETS_FULL[:: max(1, args.every)] offs = OFFSETS_FULL[:: max(1, args.every)]
t0 = time.time() t0 = time.time()
@@ -208,8 +225,10 @@ def main() -> None:
print("\n estrazione tabelle trade (V2-DD) ...", flush=True) print("\n estrazione tabelle trade (V2-DD) ...", flush=True)
EX = {o: {a: V.extract(a, o) for a in ASSETS} for o in offs} 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} 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: " + 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() TP = A.tp01_baseline_daily()
can = offs[0] can = offs[0]
@@ -299,52 +318,71 @@ def main() -> None:
print(" 4. LA FAMIGLIA INTERA — ISO dShFULL, mediana delle differenze appaiate su " print(" 4. LA FAMIGLIA INTERA — ISO dShFULL, mediana delle differenze appaiate su "
f"{len(offs)} ancore") f"{len(offs)} ancore")
print("-" * L) print("-" * L)
# DUE PASSATE, per la RAM (7 GB sulla VPS, ~3 liberi, e siamo in tanti): la 1a calcola le # RAM (brief §8: 7 GB totali, ~3 liberi, e siamo in tanti). Tre accorgimenti, tutti
# metriche per-ancora di tutte le 144 celle SENZA conservare le serie (144 x 23 x 3 serie da # necessari — la prima stesura di questo script e' stata UCCISA a meta' della §4:
# 2717 punti sarebbero ~500 MB); la 2a ricalcola e CONSERVA le serie delle sole 3-4 celle che # (a) DUE PASSATE: la 1a calcola le metriche per-ancora di tutte le 144 celle SENZA
# servono dopo (canonica / scelta al buio / argmax): 4 x 23 gambe, ~2 s. # 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 = {}, {}, {}, {} 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) nm = cname(thr, w, h, sg)
fn = size_bin(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)) szs = {a: fn(EX[o][a], (o, a)) for a in ASSETS}
yr: dict = {} lg = V.leg_daily(EX[o], szs)
for o in offs: b, c, m = iso_pair(TP, lg, base_leg[o])
lg = leg_of(EX[o], o, fn) d = ACC.setdefault(nm, dict(thr=thr, w=w, h=h, sg=sg, dF=[], dH=[], dI=[], dC=[],
b, c, m = iso_pair(TP, lg, base_leg[o]) lF=[], lI=[], lH=[], ms=[], kp=[], ddv=[], ddc=[], yr={}))
dF.append(sh(b) - sh(c)); dH.append(sh(hold(b)) - sh(hold(c))) d["dF"].append(sh(b) - sh(c)); d["dH"].append(sh(hold(b)) - sh(hold(c)))
dI.append(sh(ins(b)) - sh(ins(c))); dC_.append(cagr(b) - cagr(c)) d["dI"].append(sh(ins(b)) - sh(ins(c)))
lF.append(sh(lg)); lI.append(sh(ins(lg))); lH.append(sh(hold(lg))) d["lF"].append(sh(lg)); d["lI"].append(sh(ins(lg))); d["lH"].append(sh(hold(lg)))
ms.append(m); ddv.append(maxdd(b)); ddc.append(maxdd(c)) d["ms"].append(m)
kp.append(float(np.mean(np.concatenate([fn(EX[o][a], (o, a)) for a in ASSETS])))) 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}: for y in {int(t) for t in b.index.year}:
by, cy = b[b.index.year == y], c[c.index.year == y] by, cy = b[b.index.year == y], c[c.index.year == y]
if len(by) > 20: if len(by) > 20:
yr.setdefault(y, []).append(sh(by) - sh(cy)) d["yr"].setdefault(y, []).append(sh(by) - sh(cy))
if store: BK.setdefault(nm, {})[o] = b
BK.setdefault(nm, {})[o] = b CT.setdefault(nm, {})[o] = c
CT.setdefault(nm, {})[o] = c LEG.setdefault(nm, {})[o] = lg
LEG.setdefault(nm, {})[o] = lg MS.setdefault(nm, {})[o] = m
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()})
RES = {} for o in offs:
for (thr, w, h, sg) in cells: KC.clear()
r = eval_cell(thr, w, h, sg) for (thr, w, h, sg) in cells:
RES[r["nm"]] = r _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 " 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: def matrix(sg: int, h: float) -> None:
print(f"\n ISO dShFULL — segno {'+1 (tieni vol BASSA)' if sg>0 else '-1 (CONTROLLO)'}" 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") 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 # seconda passata: conserva le serie delle sole celle che servono dopo
sel_iso0 = max(ALLC, key=lambda r: r["isoI"]) 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, for r in dict((x["nm"], x) for x in (RES[nmcan], argmax, sel_iso0,
max(ALLC, key=lambda z: z["legI"]))).values(): 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) 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 \ 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" 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}") print(f" {tag:<40}{row}")
if fee != keep_fee: if fee != keep_fee:
del EXf, basef del EXf, basef
gc.collect()
V.FEE_RT = keep_fee V.FEE_RT = keep_fee
assert V.FEE_RT == 0.001 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_mod = FEEDEC[(f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)", nmcan)]
f_zero = FEEDEC[("fee ZERO (informazione pura)", nmcan)] f_zero = FEEDEC[("fee ZERO (informazione pura)", nmcan)]
print(f" -> a fee ZERO resta {f_zero:+.3f} di {f_mod:+.3f} = " 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(" temporale, ma e' l'unico campione indipendente disponibile a questo costo.")
print("-" * L) print("-" * L)
EX1 = {o: {a: V.extract(a, o, SKH01_V1) for a in ASSETS} for o in offs} 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} 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}" print(f" {'cella':<26}{'ISO dShFULL':>13}{'pos/n':>8}{'ISO dShHOLD':>14}{'pos/n':>8}"
f"{'tenuti':>9}") 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}" 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"{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}%") 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 # ---------------------------------------------------------- §11 per anno
print("\n" + "-" * L) 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): " print(f" ESEGUIBILE (compra ali all'ask, vendi corpo al bid): "
f"{bf['viol_exec'].sum():,} = {bf['viol_exec'].mean():.4%}") f"{bf['viol_exec'].sum():,} = {bf['viol_exec'].mean():.4%}")
if dte_min == DTE_MIN: 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("\n" + "=" * 100)
print("3. QUANTO E' GRANDE L'INCOERENZA, E QUANTO COSTA ATTRAVERSARLA") print("3. QUANTO E' GRANDE L'INCOERENZA, E QUANTO COSTA ATTRAVERSARLA")
print("=" * 100) print("=" * 100)
@@ -651,12 +651,17 @@ def main() -> None:
f"p90 {r.quantile(.9):.3f} · p99 {r.quantile(.99):.3f}") f"p90 {r.quantile(.9):.3f} · p99 {r.quantile(.99):.3f}")
print(f" mezzo spread denaro-lettera mediana {hs.median():.3f} pt-vol · " print(f" mezzo spread denaro-lettera mediana {hs.median():.3f} pt-vol · "
f"p25 {hs.quantile(.25):.3f} · p75 {hs.quantile(.75):.3f}") 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 " 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"]) both = x.dropna(subset=["iv_resid"])
rap = np.abs(both["iv_resid"]) / (2 * both["halfspread_pt"]) rap = np.abs(both["iv_resid"]) / (2 * both["halfspread_pt"])
print(f" |residuo| / (spread PIENO delle gambe): mediana {rap.median():.3f} · " print(f" |residuo| / (spread PIENO delle gambe): mediana {rap.median():.3f} · "
f"p99 {rap.quantile(.99):.3f} · quota > 1 = {np.mean(rap > 1):.3%}") 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%} " print(f" spread relativo (ask-bid)/mid del venue: mediana {x['relspread'].median():.1%} "
f"· p25 {x['relspread'].quantile(.25):.1%}") f"· p25 {x['relspread'].quantile(.25):.1%}")
# valore in dollari dell'incoerenza al MID (il caso migliore possibile: fee 0, spread 0) # 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("\n" + "=" * 100)
print("4. TEST C — varianza totale nel tenore (l'unico che tocca un interpolatore)") print("4. TEST C — varianza totale nel tenore (l'unico che tocca un interpolatore)")
print("=" * 100) print("=" * 100)
c = test_calendario(q, DTE_MIN) cal = test_calendario(q, DTE_MIN)
print(f" (i) strike CONDIVISO, nessuna interpolazione: {c['i_viol']:,}/{c['i_n']:,} = {c['i_quota']:.3%}" print(f" (i) strike CONDIVISO, nessuna interpolazione: {cal['i_viol']:,}/{cal['i_n']:,} = {cal['i_quota']:.3%}"
f" · ampiezza mediana {c['i_amp_med_pt']:.3f} pt-vol (p95 {c['i_amp_p95_pt']:.3f})") 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 {c['ii_ogni']}): " print(f" (ii) log-moneyness FISSO, con interpolazione (1 snapshot ogni {cal['ii_ogni']}): "
f"{c['ii_viol']:,}/{c['ii_n']:,} = {c['ii_quota']:.3%} · ampiezza mediana {c['ii_amp_med_pt']:.3f} pt-vol") 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 = {c['ii_quota'] - c['i_quota']:+.3%} di quota di violazione") print(f" -> contributo dell'INTERPOLATORE = {cal['ii_quota'] - cal['i_quota']:+.3%} di quota di violazione")
if "i_val_usd_med" in c: if "i_val_usd_med" in cal:
print(f" valore mediano della violazione (i) al lotto minimo ${c['i_val_usd_med']:.2f} " 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 ${c['i_costo_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 {-c['room_med_pt']:.2f} pt-vol " 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: {-c['room_p5_pt']:.2f}) " 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") f"-> la superficie non e' nemmeno VICINA al bordo")
print("\n" + "=" * 100) print("\n" + "=" * 100)
@@ -794,11 +799,15 @@ def main() -> None:
print("SINTESI") print("SINTESI")
print("=" * 100) print("=" * 100)
nv = int(bf["viol_exec"].sum()); nt = len(bf) 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" 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" 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" 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" 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") 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"]) 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} | " print(f"\n argmax: 2019+ -> TP01 {a_full:.3f} | 2024+ -> TP01 {a_rec:.3f} | "
f"SOLO-SINISTRO -> TP01 {a_sin:.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(" ⚠️ 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(" 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") 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)] (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:]) com = xs.index.intersection(IDX[i_rec:])
xs_al = xs.reindex(com) 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} " 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: for cfg in W3:
wtp, wsk, wxs = cfg wtp, wsk, wxs = cfg
base = wtp + wsk base = wtp + wsk
@@ -767,7 +772,7 @@ def main() -> None: # noqa: C901
bfu = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8302) bfu = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8302)
lab = f"{int(round(wtp*100))}/{int(round(wsk*100))}/{int(round(wxs*100))}" 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 = [], [] segsR, segsG = [], []
for t in epis: for t in epis:
rc = base * (crypto(wtp_n)[0][t:t + EPI_LEN] - offc) rc = base * (crypto(wtp_n)[0][t:t + EPI_LEN] - offc)
@@ -790,16 +795,16 @@ def main() -> None: # noqa: C901
worst = 1.0 worst = 1.0
for k, g2 in enumerate(sg): for k, g2 in enumerate(sg):
fu = splice_paths(bfu, np.arange(g2[0], g2[1]), seed=8320 + k) 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 return worst
if wxs == 0: if wxs == 0:
print(f" {lab:>16} {'n/a (no XS)':>12} {lv:>6.3f} {on['J']:>10.3f} " 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 continue
for hyp in ("A-flat", "B-peggiore"): for hyp in ("A-flat", "B-peggiore"):
print(f" {lab:>16} {hyp:>12} {lv:>6.3f} {on['J']:>10.3f} " 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% lo, hi = 0.0, 0.40 # shock di rottura: bisezione su P(vivo) = 50%
for _ in range(9): for _ in range(9):
mid = 0.5 * (lo + hi) mid = 0.5 * (lo + hi)
@@ -807,11 +812,22 @@ def main() -> None: # noqa: C901
lo = mid lo = mid
else: else:
hi = mid hi = mid
print(f" {lab:>16} {'C-shock':>12} {lv:>6.3f} {on['J']:>10.3f} {'(50% per def.)':>24} " lo2, hi2 = 0.0, 0.40
f"{-lo:>17.1%}") 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("\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(" 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(" 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") tick("§6 XS01")
# ============================================================ §7. BANDA + VERDETTO # ============================================================ §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(" 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(" 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(" 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(" a ~-10% su XS01 dimezza la sopravvivenza quando il crash arriva.")
print(" **Se TP01 vale qualcosa su un conto funded, vale come zavorra contro l'ignoto di") print(" ⚠️ IPOTESI MIA, NATA E REFUTATA IN SESSIONE: avevo scritto che TP01 fa da")
print(" XS01, non come assicurazione contro il crash di BTC/ETH — che SKH01 fa meglio.**") 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("\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(" 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") print(" quattro SKH01 ha girato. Basterebbe un episodio in cui SKH01 perde quanto il")