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:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user