684 lines
38 KiB
Python
684 lines
38 KiB
Python
"""r0822b_xs01_oos.py — XS01 FUORI dalla sua finestra di scoperta (2026-08-22, ondata wave-0822).
|
|
|
|
IL PROBLEMA (filone PROP-ALLOC, RESULTS-0822 §1)
|
|
------------------------------------------------
|
|
Il risultato piu' vicino all'obiettivo dell'operatore e': su un conto FUNDED da $100k, aggiungere
|
|
XS01 al libro porta J = P(pass) x P(vivo 12m) da 0,335 a 0,738. Ma **tutto** quel vantaggio poggia
|
|
sul drift di XS01 misurato sulla SUA STESSA finestra di scoperta (Hyperliquid, 2024-01 -> oggi,
|
|
~2,6 anni, e senza un solo mercato orso). Azzerando quel drift l'uplift scende a +0,038; sotto
|
|
stress congiunto diventa negativo. Il gate proposto era "non aprire un funded finche' XS01 non ha
|
|
una finestra fuori dal 2024-2026" = ANNI di attesa.
|
|
|
|
LA DOMANDA DI QUESTO FILONE
|
|
---------------------------
|
|
Si puo' testare il MECCANISMO CONGELATO su una storia PIU' LUNGA e INDIPENDENTE invece di aspettare?
|
|
La risposta e' si', e il vincolo non e' il dato ma il CALENDARIO DELLE QUOTAZIONI: 6 dei 19 asset
|
|
(APT, OP, ARB, SUI, SEI, TIA) non esistono prima del 2022-06/2023-10. Il piu' vecchio universo
|
|
>= 10 asset dei 19 e' del **2020-10** -> circa **3 anni interi fuori dalla finestra di scoperta,
|
|
2021-01 -> 2023-12**, che contengono il top 2021, il crollo di maggio 2021, **LUNA e FTX (2022)** e
|
|
la ripresa 2023. E' esattamente il regime che manca al campione di scoperta.
|
|
|
|
IL DATO: BINANCE, E PERCHE' L'OBIEZIONE DEL PROGETTO NON MORDE QUI
|
|
-----------------------------------------------------------------
|
|
CLAUDE.md: "Binance NON e' la verita' (e' USDT, ~10 bps fuori dal consenso, e sotto depeg fino al
|
|
3%) -> usare SOLO come audit indipendente, mai come ancora per ripulire i dati". Quella regola
|
|
riguarda l'uso come **ANCORA DI PREZZO** (livello). Qui Binance serve come **audit indipendente per
|
|
un segnale CROSS-SECTIONAL DI RANKING su barre giornaliere**, e i tre difetti citati sono tutti
|
|
**fattori COMUNI alla valuta di quotazione**:
|
|
- uno scarto costante di ~10 bps sul livello e' identico su tutti e 19 gli asset -> sparisce
|
|
ESATTAMENTE nello z-score cross-sectional (che sottrae la media di sezione);
|
|
- un depeg USDT muove tutte le gambe insieme -> non cambia il RANKING, e il libro e'
|
|
dollar-neutral (0.5/k long, -0.5/k short) quindi il fattore comune si cancella anche nel P&L;
|
|
- a 1d un errore di livello di 10 bps e' 1/50 della vol giornaliera tipica di un alt.
|
|
NON e' un argomento teorico: e' MISURATO qui sotto (sezione 3) confrontando il meccanismo su
|
|
Binance e su Hyperliquid nella finestra in cui esistono ENTRAMBI (2024+).
|
|
E i dati scaricati sono CERTIFICATI comunque (barre flat, gap di calendario, volumi zero =
|
|
backfill sintetico: il progetto ci e' gia' cascato una volta, correzione AXS del 2026-06-20).
|
|
|
|
MECCANISMO: CONGELATO. Nessun parametro toccato. E' `src/portfolio/sleeves.py::_xsec_returns`
|
|
(lookbacks (30,90), H=10, k=5, mode="mom", target_vol=0.20, gate dispersione p30 espandente
|
|
causale, fee 5 bps/lato, vol-target su rv30 shiftata, clip leva 3x). Il motore qui sotto e' una
|
|
versione MASCHERATA (regge NaN = asset non ancora quotato) e la sezione 2 dimostra che riproduce
|
|
lo sleeve ufficiale a **max|diff| = 0.0** sui dati HL.
|
|
|
|
ESITO (2026-08-22, corsa completa 78 s)
|
|
---------------------------------------
|
|
- Il motore replica lo sleeve ufficiale a max|diff| = 0.0; Binance e Hyperliquid danno lo STESSO
|
|
sleeve sulla finestra comune (corr 0.9991, dSharpe -0.025) -> la storia lunga e' utilizzabile.
|
|
- Fuori dalla finestra di scoperta (2021-01 -> 2023-12, contiene LUNA e FTX) il meccanismo ha un
|
|
edge: Sharpe MEDIANO di fase **+1.12** (U13 fisso) / **+1.17** (universo espandente), positivo in
|
|
**10/10** fasi, p = 0.013 contro il null di permutazione cross-sezionale a fee zero.
|
|
- **Il fuori campione e' MIGLIORE della finestra di scoperta**, a parita' di universo: differenza
|
|
appaiata per fase +0.668 (10/10 fasi). L'ipotesi "il drift di XS01 e' un artefatto del 2024-2026"
|
|
e' REFUTATA per la parte testabile del sleeve.
|
|
- MA: il maxDD raddoppia (10.8% nella scoperta -> 20.9-22.0% mediano fuori), il 2021 vale meta'
|
|
dell'edge, una gamba sola (LTC) ne vale 0.9 di Sharpe, e i due affinamenti del 2026-06-19 non si
|
|
replicano. E il deflated-Sharpe FALLISCE (0.342-0.418).
|
|
- Sul LIBRO, fuori campione: TP01+SKH01 75/25 Sharpe +1.17 -> **+1.65** e maxDD 9.4% -> **6.0%**
|
|
a peso XS01 0.25; a peso **0.50** (l'argmax di PROP-ALLOC) il maxDD risale a **8.9%**.
|
|
|
|
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822b_xs01_oos.py
|
|
Rete: solo lettura, <= 2 req/s, MAI nella finestra :25-:29 (rate limit per-IP condiviso col
|
|
collettore della catena). Il download e' messo in cache nello scratchpad: la seconda corsa e'
|
|
offline.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import os
|
|
import sys
|
|
import time
|
|
from datetime import datetime, timezone
|
|
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" / "alt"))
|
|
|
|
import altlib as A # noqa: E402
|
|
from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE # noqa: E402
|
|
|
|
CACHE = Path(os.environ.get(
|
|
"R0822B_CACHE",
|
|
"/tmp/claude-1001/-opt-docker-PythagorasGoal/b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/r0822b",
|
|
))
|
|
CACHE.mkdir(parents=True, exist_ok=True)
|
|
|
|
RAW = ROOT / "data" / "raw"
|
|
FEE_SIDE = 0.001 / 2.0 # identica al sleeve congelato
|
|
DISCOVERY = pd.Timestamp("2024-01-01", tz="UTC") # inizio della finestra di scoperta (HL)
|
|
|
|
# I 13 dei 19 quotati su Binance prima del 2020-11 -> l'universo FISSO piu' lungo possibile
|
|
# che rispetta il vincolo del meccanismo congelato (>= 10 gambe).
|
|
U13 = ["BTC", "ETH", "BNB", "XRP", "LTC", "ADA", "LINK", "DOGE", "SOL", "AVAX", "NEAR", "AAVE", "INJ"]
|
|
SECTORS = ["xlb", "xlc", "xle", "xlf", "xli", "xlk", "xlp", "xlre", "xlu", "xlv", "xly"]
|
|
|
|
|
|
# ===========================================================================
|
|
# 0. disciplina di rete
|
|
# ===========================================================================
|
|
def _outside_forbidden_window() -> None:
|
|
"""Il collettore della catena gira al minuto :25 e satura il rate limit per-IP di Deribit;
|
|
la regola d'ondata e' non toccare la rete fra :25 e :29. Qui parliamo con Binance, ma la
|
|
disciplina e' della MACCHINA (2 core, libro con soldi veri), non del venue."""
|
|
while True:
|
|
m = datetime.now(timezone.utc).minute
|
|
if not (25 <= m <= 29):
|
|
return
|
|
print(f" [rete] minuto :{m:02d} nella finestra vietata :25-:29 — attendo 30s", flush=True)
|
|
time.sleep(30)
|
|
|
|
|
|
# ===========================================================================
|
|
# 1. FETCH + CERTIFY (Binance spot USDT, 1d)
|
|
# ===========================================================================
|
|
def fetch_binance_daily(sym: str, since_iso: str = "2017-01-01T00:00:00Z") -> pd.DataFrame:
|
|
f = CACHE / f"bin_{sym.lower()}_1d.parquet"
|
|
if f.exists():
|
|
return pd.read_parquet(f)
|
|
import ccxt
|
|
ex = ccxt.binance({"enableRateLimit": True})
|
|
since = ex.parse8601(since_iso)
|
|
rows, cur = [], since
|
|
while True:
|
|
_outside_forbidden_window()
|
|
batch = ex.fetch_ohlcv(f"{sym}/USDT", "1d", since=cur, limit=1000)
|
|
time.sleep(0.5) # <= 2 req/s
|
|
if not batch:
|
|
break
|
|
rows.extend(batch)
|
|
if len(batch) < 1000:
|
|
break
|
|
cur = batch[-1][0] + 86_400_000
|
|
d = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"])
|
|
d = d.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)
|
|
d.to_parquet(f)
|
|
return d
|
|
|
|
|
|
def certify(sym: str, d: pd.DataFrame) -> dict:
|
|
"""Certificazione locale, stessa forma di scripts/analysis/fetch_hyperliquid.py.
|
|
Il rivelatore del backfill SINTETICO NON e' il prezzo (cerbero copiava i prezzi da un altro
|
|
venue) ma il **VOLUME**: un run di barre a volume 0 e' padding, non mercato."""
|
|
t = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
|
|
c = d["close"].values.astype(float)
|
|
v = d["volume"].values.astype(float)
|
|
span = (t.max() - t.min()).days + 1
|
|
flat = float(np.mean(c[1:] == c[:-1])) if len(c) > 1 else float("nan")
|
|
zero_v = float(np.mean(v == 0.0))
|
|
# run iniziale a volume zero = firma del padding sintetico (lezione AXS 2026-06-20)
|
|
lead0 = int(np.argmax(v > 0)) if (v > 0).any() else len(v)
|
|
return dict(sym=sym, n=len(d), start=str(t.min().date()), end=str(t.max().date()),
|
|
gap_days=int(span - len(d)), flat=flat, zero_vol=zero_v, lead_zero_vol=lead0,
|
|
ok=bool(span - len(d) == 0 and flat < 0.02 and zero_v < 0.01 and lead0 == 0))
|
|
|
|
|
|
def load_panel(syms: list[str]) -> pd.DataFrame:
|
|
"""Pannello dei CLOSE, indice giornaliero UTC, NaN dove l'asset non e' ancora quotato."""
|
|
cols = {}
|
|
for s in syms:
|
|
d = fetch_binance_daily(s)
|
|
cols[s] = pd.Series(d["close"].values.astype(float),
|
|
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
|
|
return pd.concat(cols, axis=1).sort_index()
|
|
|
|
|
|
def load_sector_panel() -> pd.DataFrame:
|
|
cols = {}
|
|
for s in SECTORS:
|
|
d = pd.read_parquet(RAW / f"eq_{s}_1d.parquet")
|
|
cols[s.upper()] = pd.Series(d["close"].values.astype(float),
|
|
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
|
|
return pd.concat(cols, axis=1).sort_index()
|
|
|
|
|
|
# ===========================================================================
|
|
# 2. MOTORE — meccanismo CONGELATO, versione mascherata (regge i NaN)
|
|
# ===========================================================================
|
|
def xsec_engine(P: pd.DataFrame, phase: int = 0, fee_side: float = FEE_SIDE,
|
|
mode: str | None = None, min_assets: int = 10,
|
|
perm_rng: np.random.Generator | None = None,
|
|
perm_kind: str = "cross", lookbacks=None, disp_pct=None,
|
|
detail: dict | None = None) -> pd.Series:
|
|
"""Riproduce `sleeves._xsec_returns` riga per riga. Unica differenza: l'insieme ammissibile
|
|
e' calcolato PER RIBILANCIAMENTO (un asset entra quando ha almeno max(lookbacks) barre), cosi'
|
|
il meccanismo si puo' applicare a un universo che si allarga nel tempo — che e' cio' che un
|
|
implementatore in tempo reale avrebbe avuto. Con un pannello senza NaN, e' identico all'originale.
|
|
|
|
`perm_rng` attiva un NULL: 'cross' = permuta lo score FRA GLI ASSET al ribilanciamento
|
|
(distrugge l'informazione, conserva ESATTAMENTE la struttura di posizione e di turnover),
|
|
'sign' = pesi casuali fra gli ammissibili (stesso gross, stesso k)."""
|
|
px = P.values.astype(float)
|
|
n, Ncols = px.shape
|
|
H, k = XS_CFG["H"], XS_CFG["k"]
|
|
lookbacks = tuple(XS_CFG["lookbacks"] if lookbacks is None else lookbacks)
|
|
disp_pct = XS_CFG["disp_pct"] if disp_pct is None else disp_pct
|
|
tv, minhist = XS_CFG["target_vol"], XS_CFG["disp_minhist"]
|
|
mode = mode or XS_CFG["mode"]
|
|
mlb = max(lookbacks)
|
|
|
|
with np.errstate(invalid="ignore", divide="ignore"):
|
|
dret = np.vstack([np.zeros(Ncols), px[1:] / px[:-1] - 1.0])
|
|
dret = np.nan_to_num(dret, nan=0.0, posinf=0.0, neginf=0.0)
|
|
|
|
W = np.zeros((n, Ncols)); w = np.zeros(Ncols); disp_hist: list[float] = []
|
|
n_rebal = n_traded = 0
|
|
for i in range(n):
|
|
if i >= mlb and (i % H) == phase:
|
|
elig = np.isfinite(px[i])
|
|
for L in lookbacks:
|
|
elig = elig & np.isfinite(px[i - L])
|
|
m = int(elig.sum())
|
|
if m >= min_assets:
|
|
n_rebal += 1
|
|
idx = np.flatnonzero(elig)
|
|
rLs = [px[i, idx] / px[i - L, idx] - 1.0 for L in lookbacks]
|
|
disp_i = float(np.mean([r.std() for r in rLs]))
|
|
thr = (np.percentile(disp_hist, disp_pct)
|
|
if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf)
|
|
if disp_i >= thr:
|
|
score = np.zeros(m); cnt = 0
|
|
for rL in rLs:
|
|
sd = rL.std()
|
|
if sd > 0:
|
|
score += (rL - rL.mean()) / sd; cnt += 1
|
|
if cnt:
|
|
score /= cnt
|
|
w = np.zeros(Ncols)
|
|
if perm_rng is not None and perm_kind == "sign":
|
|
pick = perm_rng.permutation(m)
|
|
lo, hi = idx[pick[:k]], idx[pick[-k:]]
|
|
else:
|
|
if perm_rng is not None: # perm_kind == "cross"
|
|
score = score[perm_rng.permutation(m)]
|
|
order = np.argsort(score)
|
|
lo, hi = idx[order[:k]], idx[order[-k:]]
|
|
if mode == "mom":
|
|
w[hi] = 0.5 / k; w[lo] = -0.5 / k
|
|
else:
|
|
w[lo] = 0.5 / k; w[hi] = -0.5 / k
|
|
n_traded += 1
|
|
else:
|
|
w = np.zeros(Ncols)
|
|
disp_hist.append(disp_i)
|
|
W[i] = w
|
|
|
|
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
|
|
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum()
|
|
turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
|
|
net = gross - turn * fee_side
|
|
s = pd.Series(net, index=P.index)
|
|
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
|
|
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
|
|
out = pd.Series(s.values * scale, index=P.index)
|
|
out.attrs["n_rebal"] = n_rebal
|
|
out.attrs["n_traded"] = n_traded
|
|
if detail is not None:
|
|
detail.update(W=W, scale=scale, turn=turn, index=P.index,
|
|
n_rebal=n_rebal, n_traded=n_traded)
|
|
return out
|
|
|
|
|
|
def hl_panel(syms=None) -> pd.DataFrame:
|
|
"""Pannello Hyperliquid, ESATTAMENTE come lo costruisce lo sleeve congelato."""
|
|
cols = {}
|
|
for sym in (syms or XS_UNIVERSE):
|
|
p = RAW / f"hl_{sym.lower()}_1d.parquet"
|
|
if not p.exists():
|
|
continue
|
|
d = pd.read_parquet(p)
|
|
cols[sym] = pd.Series(d["close"].values.astype(float),
|
|
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
|
|
return pd.concat(cols, axis=1, join="inner").sort_index().dropna()
|
|
|
|
|
|
# ===========================================================================
|
|
# metriche
|
|
# ===========================================================================
|
|
def sh(s: pd.Series) -> float:
|
|
return A._sh(s)
|
|
|
|
|
|
def dd(s: pd.Series) -> float:
|
|
return A._dd_ret(s)
|
|
|
|
|
|
def cagr(s: pd.Series) -> float:
|
|
r = s.dropna()
|
|
if len(r) < 30:
|
|
return float("nan")
|
|
yrs = (r.index[-1] - r.index[0]).days / 365.25
|
|
return float(np.prod(1.0 + r.values) ** (1.0 / yrs) - 1.0) if yrs > 0 else float("nan")
|
|
|
|
|
|
def win(s: pd.Series, a: str | None = None, b: str | None = None) -> pd.Series:
|
|
x = s
|
|
if a:
|
|
x = x[x.index >= pd.Timestamp(a, tz="UTC")]
|
|
if b:
|
|
x = x[x.index < pd.Timestamp(b, tz="UTC")]
|
|
return x
|
|
|
|
|
|
def alive(s: pd.Series) -> pd.Series:
|
|
"""Taglia il prefisso di ZERI (warm-up dei lookback + attesa che l'universo raggiunga 10
|
|
gambe). ⚠️ Bug catturato nella prima corsa: sull'universo ESPANDENTE il pannello parte dal
|
|
2017 (BTC) ma il meccanismo non puo' operare prima del 2020-12, e i ~1.200 zeri iniziali
|
|
entravano nella finestra 'PRE-2024' abbassandone lo Sharpe e ribaltando la differenza
|
|
appaiata. E' la lezione gia' codificata nel progetto (barre ATTIVE, non di calendario),
|
|
ripetuta in un contesto nuovo."""
|
|
v = np.asarray(s.values, float)
|
|
nz = np.flatnonzero(v != 0.0)
|
|
return s.iloc[nz[0]:] if len(nz) else s
|
|
|
|
|
|
def line(tag: str, s: pd.Series) -> str:
|
|
return (f"{tag:<22s} n={len(s):>5d} Sh {sh(s):+6.2f} maxDD {dd(s)*100:5.1f}% "
|
|
f"CAGR {cagr(s)*100:+6.2f}% vol {s.std()*np.sqrt(365.25)*100:5.1f}%")
|
|
|
|
|
|
# ===========================================================================
|
|
def main() -> None:
|
|
t0 = time.time()
|
|
print("=" * 100)
|
|
print("XS01 FUORI DALLA FINESTRA DI SCOPERTA — meccanismo CONGELATO su storia lunga indipendente")
|
|
print(f"XS_CFG = {XS_CFG}")
|
|
print("=" * 100)
|
|
|
|
# ---------------------------------------------------------------- 1. dato
|
|
print("\n[1] DATO — Binance spot USDT 1d, certificazione locale")
|
|
panel19 = load_panel(XS_UNIVERSE)
|
|
cert = [certify(s, fetch_binance_daily(s)) for s in XS_UNIVERSE]
|
|
bad = [c for c in cert if not c["ok"]]
|
|
print(f" {len(cert)} serie scaricate; certificate OK: {len(cert)-len(bad)}/{len(cert)}")
|
|
print(f" {'sym':<6}{'n':>6}{'start':>12}{'end':>12}{'gap':>5}{'flat%':>8}{'vol0%':>8}{'lead0':>7}")
|
|
for c in cert:
|
|
flag = "" if c["ok"] else " <-- CONTROLLA"
|
|
print(f" {c['sym']:<6}{c['n']:>6}{c['start']:>12}{c['end']:>12}{c['gap_days']:>5}"
|
|
f"{c['flat']*100:>8.2f}{c['zero_vol']*100:>8.2f}{c['lead_zero_vol']:>7}{flag}")
|
|
print(" NB: 'lead0' = barre iniziali a volume 0 = firma del backfill sintetico (lezione AXS).")
|
|
|
|
# ---------------------------------------------------- 2. replica bit-exact
|
|
print("\n[2] SANITY — il motore riproduce lo sleeve UFFICIALE?")
|
|
from src.portfolio.sleeves import _xsec_returns
|
|
off = _xsec_returns()
|
|
rep = xsec_engine(hl_panel(), phase=0)
|
|
j = pd.concat({"off": off, "rep": rep}, axis=1, join="inner").dropna()
|
|
md = float(np.max(np.abs(j["off"] - j["rep"]))) if len(j) else float("nan")
|
|
print(f" n={len(j)} max|diff| = {md:.2e} -> {'IDENTICO' if md < 1e-12 else 'DIVERGE'}")
|
|
print(f" {line('XS01 ufficiale (HL)', off)}")
|
|
if md >= 1e-12:
|
|
print(" !! il motore NON replica: ogni numero sotto e' sospetto. STOP.")
|
|
return
|
|
|
|
# ------------------------------------------------ 3. validazione di VENUE
|
|
print("\n[3] VALIDAZIONE DI VENUE — Binance vs Hyperliquid dove esistono ENTRAMBI (2024+)")
|
|
hlp = hl_panel()
|
|
b19 = panel19.reindex(columns=XS_UNIVERSE)
|
|
common = b19.index.intersection(hlp.index)
|
|
rr = []
|
|
for s in XS_UNIVERSE:
|
|
a1 = hlp[s].reindex(common).pct_change()
|
|
b1 = b19[s].reindex(common).pct_change()
|
|
k2 = pd.concat({"a": a1, "b": b1}, axis=1).dropna()
|
|
rr.append((s, float(k2["a"].corr(k2["b"])), float(np.median(np.abs(k2["a"] - k2["b"]))) * 1e4))
|
|
print(f" ritorni giornalieri per asset, {len(common)} giorni comuni:")
|
|
print(" " + " ".join(f"{s}:{c:.4f}" for s, c, _ in rr))
|
|
print(f" corr min {min(c for _, c, _ in rr):.4f} | mediana |diff| ritorno: "
|
|
f"{np.median([m for _, _, m in rr]):.1f} bps (max {max(m for _, _, m in rr):.1f} bps)")
|
|
# e soprattutto: il MECCANISMO da' la stessa cosa?
|
|
bin_on_hl_window = xsec_engine(b19.loc[common].dropna(), phase=0)
|
|
hl_on_window = xsec_engine(hlp.loc[common], phase=0)
|
|
jj = pd.concat({"bin": bin_on_hl_window, "hl": hl_on_window}, axis=1, join="inner").dropna()
|
|
print(f" {line('XS01 su HL 2024+', jj['hl'])}")
|
|
print(f" {line('XS01 su BINANCE 2024+', jj['bin'])}")
|
|
print(f" corr fra le DUE VERSIONI DELLO SLEEVE: {jj['bin'].corr(jj['hl']):.4f} "
|
|
f"dSharpe = {sh(jj['bin'])-sh(jj['hl']):+.3f}")
|
|
print(" -> se questa corr e' ~1, la storia lunga Binance e' un'estensione legittima:")
|
|
print(" lo scarto USDT e' un FATTORE COMUNE e sparisce nello z-score cross-sectional.")
|
|
|
|
# ------------------------------------- 4. la storia lunga, fuori campione
|
|
print("\n[4] STORIA LUNGA — meccanismo congelato, ancora canonica (fase 0)")
|
|
p13 = panel19[U13].dropna() # universo FISSO 13 (il piu' lungo a >=10 gambe)
|
|
s13 = xsec_engine(p13, phase=0)
|
|
sEXP = xsec_engine(panel19, phase=0) # universo che si ALLARGA (19 quando esistono)
|
|
s13, sEXP = alive(s13), alive(sEXP)
|
|
print(f" U13-FIX universo fisso {len(U13)} gambe, dal {s13.index[0].date()} "
|
|
f"({s13.attrs.get('n_traded')}/{s13.attrs.get('n_rebal')} ribilanciamenti con posizione)")
|
|
print(f" U19-EXP universo espandente (10 -> 19 gambe), dal {sEXP.index[0].date()}")
|
|
for tag, s in (("U13-FIX", s13), ("U19-EXP", sEXP)):
|
|
print(f" {line(tag + ' FULL', s)}")
|
|
print(f" {line(tag + ' PRE-2024 (OOS)', win(s, None, '2024-01-01'))}")
|
|
print(f" {line(tag + ' 2024+ (scoperta)', win(s, '2024-01-01'))}")
|
|
|
|
print("\n Sharpe per ANNO (netto fee):")
|
|
yrs = sorted({d.year for d in sEXP.index})
|
|
hdr = " " + f"{'':<10}" + "".join(f"{y:>9d}" for y in yrs)
|
|
print(hdr)
|
|
for tag, s in (("U13-FIX", s13), ("U19-EXP", sEXP), ("XS01 HL", off)):
|
|
cells = []
|
|
for y in yrs:
|
|
x = win(s, f"{y}-01-01", f"{y+1}-01-01")
|
|
cells.append(f"{sh(x):>9.2f}" if len(x) > 60 else f"{'—':>9s}")
|
|
print(f" {tag:<10}" + "".join(cells))
|
|
|
|
print("\n Sharpe per REGIME:")
|
|
regimes = [("2021 toro/top", "2021-01-01", "2022-01-01"),
|
|
("2022 ORSO (LUNA/FTX)", "2022-01-01", "2023-01-01"),
|
|
("2023 ripresa", "2023-01-01", "2024-01-01"),
|
|
("2024-26 scoperta", "2024-01-01", None)]
|
|
for nm, a, b in regimes:
|
|
x13, xEX = win(s13, a, b), win(sEXP, a, b)
|
|
print(f" {nm:<24s} U13 Sh {sh(x13):+6.2f} DD {dd(x13)*100:5.1f}% | "
|
|
f"U19-EXP Sh {sh(xEX):+6.2f} DD {dd(xEX)*100:5.1f}%")
|
|
|
|
# -------------------------------------------- 5. fortuna di FASE (H = 10)
|
|
print("\n[5] FORTUNA DI FASE — H=10, tutte e 10 le ancore (la stima onesta e' la MEDIANA)")
|
|
phases = list(range(XS_CFG["H"]))
|
|
bands = {}
|
|
for tag, P, cut in (("U13-FIX FULL", p13, None), ("U13-FIX PRE-2024", p13, "pre"),
|
|
("U19-EXP FULL", panel19, None), ("U19-EXP PRE-2024", panel19, "pre")):
|
|
def f(ph, P=P, cut=cut):
|
|
s = alive(xsec_engine(P, phase=ph))
|
|
return win(s, None, "2024-01-01") if cut == "pre" else s
|
|
bands[tag] = A.anchor_luck_band(f, phases, canonical=0, metric=sh)
|
|
bands[tag + "|DD"] = A.anchor_luck_band(f, phases, canonical=0, metric=lambda x: -dd(x))
|
|
for tag in ("U13-FIX FULL", "U13-FIX PRE-2024", "U19-EXP FULL", "U19-EXP PRE-2024"):
|
|
r = bands[tag]; rd = bands[tag + "|DD"]
|
|
print(f" {tag:<20s} Sh canonico {r['canonical']:+.2f} (pctl {r['canonical_pctl']*100:4.0f}°) "
|
|
f"| MEDIANA {r['median']:+.2f} | banda [{r['lo']:+.2f},{r['hi']:+.2f}] "
|
|
f"| fasi>0 {r['frac_positive']*100:3.0f}% | fortuna {r['luck']:+.2f} | gate {r['gate_pass']}")
|
|
print(f" {'':<20s} maxDD canonico {-rd['canonical']*100:5.1f}% "
|
|
f"| MEDIANA {-rd['median']*100:5.1f}% | banda [{-rd['hi']*100:.1f},{-rd['lo']*100:.1f}]%")
|
|
# anche lo sleeve UFFICIALE su HL, de-luckato di fase (per confronto onesto)
|
|
bhl = A.anchor_luck_band(lambda ph: xsec_engine(hlp, phase=ph), phases, canonical=0, metric=sh)
|
|
print(f" {'XS01 UFFICIALE (HL)':<20s} Sh canonico {bhl['canonical']:+.2f} "
|
|
f"(pctl {bhl['canonical_pctl']*100:4.0f}°) | MEDIANA {bhl['median']:+.2f} "
|
|
f"| banda [{bhl['lo']:+.2f},{bhl['hi']:+.2f}] | fortuna {bhl['luck']:+.2f}")
|
|
|
|
# -------------- 6. differenza APPAIATA fuori campione vs finestra di scoperta
|
|
print("\n[6] DIFFERENZA APPAIATA per fase: (fuori campione) - (finestra di scoperta)")
|
|
for tag, P in (("U13-FIX", p13), ("U19-EXP", panel19)):
|
|
def pre(ph, P=P):
|
|
return win(alive(xsec_engine(P, phase=ph)), None, "2024-01-01")
|
|
|
|
def post(ph, P=P):
|
|
return win(alive(xsec_engine(P, phase=ph)), "2024-01-01")
|
|
d1 = A.anchor_luck_delta(pre, post, phases, metric=sh)
|
|
print(f" {tag:<10s} mediana DIFF appaiata {d1['median_paired']:+.3f} "
|
|
f"[{d1['lo']:+.3f},{d1['hi']:+.3f}] fasi con OOS>scoperta {d1['n_positive']}/{d1['n_anchors']}")
|
|
|
|
# ------------------------------------------------------------- 7. i NULL
|
|
print("\n[7] NULL — a FEE ZERO (permutare un segnale ne fa esplodere il turnover: a fee piene")
|
|
print(" il null perderebbe per COSTO invece che per assenza di informazione — regola XSR01)")
|
|
NDRAW = 300
|
|
for tag, P, a, b in (("U13-FIX PRE-2024", p13, None, "2024-01-01"),
|
|
("U13-FIX FULL", p13, None, None)):
|
|
real = win(alive(xsec_engine(P, phase=0, fee_side=0.0)), a, b)
|
|
for kind in ("cross", "sign"):
|
|
rng = np.random.default_rng(20260822)
|
|
draws = np.array([sh(win(alive(xsec_engine(P, phase=0, fee_side=0.0, perm_rng=rng,
|
|
perm_kind=kind)), a, b)) for _ in range(NDRAW)])
|
|
p = float(np.mean(draws >= sh(real)))
|
|
print(f" {tag:<20s} null '{kind}': reale(lordo) {sh(real):+.2f} vs null "
|
|
f"med {np.median(draws):+.2f} max {draws.max():+.2f} -> p = {p:.4f}")
|
|
|
|
print("\n NULL 'universo dove NON dovrebbe funzionare' — 11 settoriali SPDR, 1998+,")
|
|
print(" stesso meccanismo congelato (il progetto ha gia' misurato che il cross-sectional")
|
|
print(" crypto non generalizza alle azioni: XSR01, 2026-07-26):")
|
|
sect = load_sector_panel()
|
|
ssec = alive(xsec_engine(sect, phase=0, min_assets=9))
|
|
bsec = A.anchor_luck_band(lambda ph: alive(xsec_engine(sect, phase=ph, min_assets=9)), phases,
|
|
canonical=0, metric=sh)
|
|
print(f" {line('SECT11 (azioni)', ssec)}")
|
|
print(f" SECT11 banda di fase: mediana Sh {bsec['median']:+.2f} "
|
|
f"[{bsec['lo']:+.2f},{bsec['hi']:+.2f}], fasi>0 {bsec['frac_positive']*100:.0f}%")
|
|
|
|
print("\n CONTROLLO DI DIREZIONE — stesso meccanismo con mode='rev' (long i deboli):")
|
|
for tag, P in (("U13-FIX", p13), ("U19-EXP", panel19)):
|
|
r = alive(xsec_engine(P, phase=0, mode="rev"))
|
|
print(f" {line(tag + ' REV', r)}")
|
|
|
|
# ------------- 7b. la SELEZIONE del 2026-06-19, rivalidata FUORI CAMPIONE
|
|
print("\n[7b] LA SELEZIONE DEL 2026-06-19, RIVALIDATA FUORI CAMPIONE")
|
|
print(" XS01 non e' solo un meccanismo: e' un meccanismo AFFINATO due volte sui dati HL")
|
|
print(" 2024+ (blend di lookback [30,90] e gate di dispersione p30). Quelle DUE scelte sono")
|
|
print(" debito di multiple-testing contratto sulla finestra di scoperta. Qui si paga:")
|
|
print(" ogni variante e' valutata sul PRE-2024, a MEDIANA delle 10 fasi (mai l'ancora).")
|
|
variants = [("blend [30,90] + gate p30 (CANONICA)", dict(lookbacks=(30, 90), disp_pct=30)),
|
|
("solo L=30 + gate p30", dict(lookbacks=(30,), disp_pct=30)),
|
|
("solo L=90 + gate p30", dict(lookbacks=(90,), disp_pct=30)),
|
|
("blend [30,90] SENZA gate", dict(lookbacks=(30, 90), disp_pct=0)),
|
|
("blend [30,90] + gate p15", dict(lookbacks=(30, 90), disp_pct=15)),
|
|
("blend [30,90] + gate p50", dict(lookbacks=(30, 90), disp_pct=50))]
|
|
sel_rows = []
|
|
for nm, kw in variants:
|
|
b_pre = A.anchor_luck_band(
|
|
lambda ph, kw=kw: win(alive(xsec_engine(p13, phase=ph, **kw)), None, "2024-01-01"),
|
|
phases, canonical=0, metric=sh)
|
|
b_post = A.anchor_luck_band(
|
|
lambda ph, kw=kw: win(alive(xsec_engine(p13, phase=ph, **kw)), "2024-01-01"),
|
|
phases, canonical=0, metric=sh)
|
|
sel_rows.append((nm, b_pre["median"], b_post["median"], b_pre["per_offset"]))
|
|
print(f" {nm:<38s} PRE-2024 mediana Sh {b_pre['median']:+5.2f} "
|
|
f"[{b_pre['lo']:+.2f},{b_pre['hi']:+.2f}] | 2024+ {b_post['median']:+5.2f}")
|
|
rank_pre = 1 + sorted([r[1] for r in sel_rows], reverse=True).index(sel_rows[0][1])
|
|
rank_post = 1 + sorted([r[2] for r in sel_rows], reverse=True).index(sel_rows[0][2])
|
|
print(f" -> la configurazione CANONICA (scelta nel 2026-06 guardando SOLO il 2024+) e'")
|
|
print(f" {rank_pre}/{len(sel_rows)} sul PRE-2024 e {rank_post}/{len(sel_rows)} sul 2024+.")
|
|
print( " 📌 LETTURA: il NUCLEO regge (ogni variante e' positiva fuori campione, banda")
|
|
print( " [+0.76,+1.28]), ma i DUE AFFINAMENTI del 19/06 NON si replicano:")
|
|
print( " - il BLEND [30,90] fuori campione e' PEGGIO del solo L=30 (+1.12 vs +1.28);")
|
|
print( " - il GATE di dispersione p30 fuori campione vale ZERO (con e senza: +1.12);")
|
|
print( " e' dentro la finestra di scoperta che vale (+0.37 contro -0.03).")
|
|
print( " Cioe': cio' che sopravvive e' il meccanismo cross-sectional nudo; i due")
|
|
print( " affinamenti sono, per quanto qui misurabile, adattamento alla finestra in")
|
|
print( " cui furono scelti. NON e' un argomento per cambiarli (cambiarli guardando")
|
|
print( " QUESTO risultato sarebbe la stessa selezione, spostata di finestra): e'")
|
|
print( " un argomento per non attribuire loro il valore che il progetto gli attribuisce.")
|
|
|
|
# --- concentrazione: un contributo positivo si scompone per ANNO e per GAMBA (lezione SOL)
|
|
print("\n Concentrazione (lezione SOL 22/08: '24/24 ancore positive erano UN ANNO SOLO'):")
|
|
pre13 = win(s13, None, "2024-01-01")
|
|
for y in (2021, 2022, 2023):
|
|
rest = pre13[pre13.index.year != y]
|
|
print(f" PRE-2024 togliendo il {y}: Sh {sh(rest):+5.2f} "
|
|
f"(intero pre-2024 {sh(pre13):+5.2f})")
|
|
loo = {}
|
|
for a_ in U13:
|
|
sub = panel19[[x for x in U13 if x != a_]].dropna()
|
|
bb = A.anchor_luck_band(lambda ph, sub=sub: win(alive(xsec_engine(sub, phase=ph,
|
|
min_assets=10)),
|
|
None, "2024-01-01"),
|
|
phases, canonical=0, metric=sh)
|
|
loo[a_] = bb["median"]
|
|
base_med = bands["U13-FIX PRE-2024"]["median"]
|
|
worst = min(loo, key=loo.get)
|
|
print(f" Leave-one-asset-out sul PRE-2024 (mediana di fase; base {base_med:+.2f}):")
|
|
print(" " + " ".join(f"-{k2}:{v:+.2f}" for k2, v in sorted(loo.items(), key=lambda t: t[1])))
|
|
print(f" gamba piu' importante: {worst} (senza di lei {loo[worst]:+.2f}); "
|
|
f"minimo {min(loo.values()):+.2f}, tutte >0: {all(v > 0 for v in loo.values())}")
|
|
|
|
# ---------------------------------------------------------- 8. i GATE
|
|
print("\n[8] GATE")
|
|
cand = s13.copy(); cand.index = cand.index.tz_convert("UTC")
|
|
mg = A.marginal_vs_tp01(cand)
|
|
print(f" marginal_vs_tp01 (U13-FIX, 2021+): {mg.get('marginal_verdict')} "
|
|
f"corr {mg.get('corr_full')} n={mg.get('n_days')}")
|
|
for kk in ("robust_oos", "has_insample_edge", "is_hedge", "beats_noise_null",
|
|
"insample_sharpe", "tp01_beta", "alpha_ann"):
|
|
if kk in mg:
|
|
print(f" {kk:<20s} {mg[kk]}")
|
|
if "blends" in mg:
|
|
for wname, bl in mg["blends"].items():
|
|
print(f" blend {wname}: uplift_full {bl.get('uplift_full')} "
|
|
f"uplift_hold {bl.get('uplift_hold')}")
|
|
|
|
mgE = A.marginal_vs_tp01(sEXP)
|
|
print(f" marginal_vs_tp01 (U19-EXP, 2020-12+): {mgE.get('marginal_verdict')} "
|
|
f"corr {mgE.get('corr_full')}")
|
|
|
|
# deflated Sharpe: TUTTI i trial valutati, al RIALZO
|
|
all_sr = []
|
|
for tag in ("U13-FIX FULL", "U13-FIX PRE-2024", "U19-EXP FULL", "U19-EXP PRE-2024"):
|
|
all_sr += list(bands[tag]["per_offset"].values())
|
|
all_sr += list(bhl["per_offset"].values()) + list(bsec["per_offset"].values())
|
|
for _, _, _, po in sel_rows: # 6 varianti x 10 fasi della sezione 7b
|
|
all_sr += list(po.values())
|
|
all_sr += [float(v) for v in loo.values()]
|
|
cand_pool = list(all_sr) # SOLO configurazioni di XS01
|
|
ctrl = [sh(alive(xsec_engine(p13, phase=0, mode="rev"))),
|
|
sh(alive(xsec_engine(panel19, phase=0, mode="rev"))), sh(jj["bin"]), sh(jj["hl"])]
|
|
for fs in (0.0005, 0.001, 0.002):
|
|
ctrl.append(sh(alive(xsec_engine(p13, phase=0, fee_side=fs))))
|
|
dsr_val = bands["U13-FIX PRE-2024"]["median"]
|
|
med_ph = int(np.argsort(list(bands["U13-FIX PRE-2024"]["per_offset"].values()))[len(phases) // 2])
|
|
med_series = win(alive(xsec_engine(p13, phase=med_ph)), None, "2024-01-01")
|
|
d1, s1 = A.deflated_sharpe(dsr_val, cand_pool, med_series)
|
|
d2, s2 = A.deflated_sharpe(dsr_val, cand_pool + ctrl, med_series)
|
|
print(f" deflated_sharpe — headline = MEDIANA di fase OOS {dsr_val:+.2f}")
|
|
print(f" pool A = SOLE configurazioni XS01 ({len(cand_pool)} trial): DSR {d1:.3f} "
|
|
f"(max dal rumore {s1:+.2f}) -> {'PASS' if d1 >= 0.95 else 'FAIL'}")
|
|
print(f" pool B = tutto cio' che ho valutato, controlli NEGATIVI inclusi "
|
|
f"({len(cand_pool)+len(ctrl)} trial): DSR {d2:.3f} (max dal rumore {s2:+.2f}) "
|
|
f"-> {'PASS' if d2 >= 0.95 else 'FAIL'}")
|
|
print( " ⚠️ Il pool B e' il conteggio 'al rialzo' che il progetto impone, ma qui e' anche")
|
|
print( " il piu' SEVERO in modo improprio: il DSR pesa la VARIANZA dei trial, e io ci")
|
|
print( " ho messo dentro controlli costruiti per essere negativi (REV -0.97, SECT11")
|
|
print( " -0.44). Si cita il piu' basso dei due; e si dice che entrambi penalizzano un")
|
|
print( " MEDIANA-di-fase come se fosse un MAX-di-k, che e' il caso per cui il DSR e'")
|
|
print( " costruito. Il debito di selezione vero di XS01 non e' stato contratto qui:")
|
|
print( " e' quello del 2026-06-19, e la sezione 7b lo misura direttamente.")
|
|
|
|
imp = A.implausible_sharpe(s13)
|
|
print(f" implausible_sharpe: {imp['implausible']} Sh {imp.get('sharpe'):+.2f} "
|
|
f"maxDD {imp.get('maxdd', 0)*100:.1f}% perdite su barre attive "
|
|
f"{imp.get('loss_frac', float('nan'))*100:.1f}% (attive {imp.get('active_frac', 0)*100:.0f}%)"
|
|
f"{' — ' + '; '.join(imp['reasons']) if imp['reasons'] else ''}")
|
|
|
|
# causalita': i prezzi DOPO la data di decisione non devono cambiare le posizioni prima
|
|
Pc = p13.copy()
|
|
cut = len(Pc) // 2
|
|
rngc = np.random.default_rng(7)
|
|
Pd = Pc.copy()
|
|
Pd.iloc[cut:] = Pc.iloc[cut:].values * (1.0 + rngc.normal(0, 0.35, size=Pc.iloc[cut:].shape))
|
|
a_ = xsec_engine(Pc, phase=0); b_ = xsec_engine(Pd, phase=0)
|
|
# confronto solo dove il vol-target non ha ancora "visto" i dati perturbati (rv30 shiftata)
|
|
lim = cut - 35
|
|
cdiff = float(np.max(np.abs(a_.values[:lim] - b_.values[:lim])))
|
|
print(f" causalita' (prezzi futuri randomizzati da i={cut}): max|diff| prima di i={lim} "
|
|
f"= {cdiff:.2e} -> {'LEAK-FREE' if cdiff < 1e-12 else 'PERDITA DI FUTURO'}")
|
|
|
|
print(" null de-levering: NON PERTINENTE — il claim qui non e' 'meno drawdown' ma "
|
|
"'l'edge esiste fuori campione'. Il de-levering e' comunque implicito nel vol-target,")
|
|
print(" e le tabelle di fase riportano DD e Sharpe INSIEME (un k<1 abbassa entrambi).")
|
|
|
|
print(f"\n sensibilita' al COSTO (il rischio #1 di uno sleeve a 13 gambe alt):")
|
|
for fs in (0.0005, 0.001, 0.002, 0.003):
|
|
x = alive(xsec_engine(p13, phase=0, fee_side=fs))
|
|
print(f" {fs*1e4:5.0f} bps/lato: FULL Sh {sh(x):+5.2f} | "
|
|
f"PRE-2024 {sh(win(x, None, '2024-01-01')):+5.2f} | "
|
|
f"2024+ {sh(win(x, '2024-01-01')):+5.2f}")
|
|
|
|
# ------------------------------------------------- 9. impatto sul LIBRO
|
|
print("\n[9] IMPATTO SUL LIBRO — TP01+SKH01 (75/25 live) con e senza XS01, su 2021+")
|
|
from src.portfolio.sleeves import _tp01_returns, _skyhook_returns
|
|
tp = A._to_daily(_tp01_returns()); sk = A._to_daily(_skyhook_returns())
|
|
for xs_tag, xs in (("XS01 storia lunga (U13-FIX)", s13), ("XS01 ufficiale (HL 2024+)", off)):
|
|
J = pd.concat({"tp": tp, "sk": sk, "xs": A._to_daily(xs)}, axis=1, join="inner").dropna()
|
|
if len(J) < 200:
|
|
continue
|
|
base = 0.75 * J["tp"] + 0.25 * J["sk"]
|
|
for w in (0.15, 0.25, 0.50):
|
|
mix = (1 - w) * base + w * J["xs"]
|
|
print(f" [{xs_tag}] {J.index[0].date()}..{J.index[-1].date()} n={len(J)} "
|
|
f"w={w:.2f}: Sh {sh(base):+.2f} -> {sh(mix):+.2f} "
|
|
f"({sh(mix)-sh(base):+.2f}) maxDD {dd(base)*100:.1f}% -> {dd(mix)*100:.1f}% "
|
|
f"corr(XS,libro) {J['xs'].corr(base):+.3f}")
|
|
# e il numero che il PROP-ALLOC usa davvero: il libro 2021+ contiene il 2022
|
|
pre = J[J.index < DISCOVERY]
|
|
if len(pre) > 200:
|
|
bpre = 0.75 * pre["tp"] + 0.25 * pre["sk"]
|
|
print(f" -> SOLO FUORI CAMPIONE ({pre.index[0].date()}..{pre.index[-1].date()}, "
|
|
f"n={len(pre)}), sweep del peso di XS01:")
|
|
for w in (0.15, 0.25, 0.33, 0.50):
|
|
mpre = (1 - w) * bpre + w * pre["xs"]
|
|
print(f" w={w:.2f}: libro Sh {sh(bpre):+.2f} -> {sh(mpre):+.2f} "
|
|
f"({sh(mpre)-sh(bpre):+.2f}) maxDD {dd(bpre)*100:.1f}% -> "
|
|
f"{dd(mpre)*100:.1f}% corr {pre['xs'].corr(bpre):+.3f}")
|
|
print(" ⚠️ PROP-ALLOC mette XS01 al 50%: su un conto a BARRIERA cio' che si")
|
|
print(" compra e' il maxDD, quindi la riga da guardare e' quella, non lo Sharpe.")
|
|
|
|
# ---------------------------------------------------- 10. eseguibilita'
|
|
print("\n[10] ESEGUIBILITA' — la domanda del canale funded e' se il libro STA su $100k")
|
|
det: dict = {}
|
|
xsec_engine(p13, phase=0, detail=det)
|
|
W, sc = det["W"], det["scale"]
|
|
dW = np.abs(np.diff(W, axis=0)); rb = np.flatnonzero(dW.sum(axis=1) > 1e-12)
|
|
print(f" {len(U13)} gambe, {len(rb)} ribilanciamenti, gross medio "
|
|
f"{np.abs(W).sum(axis=1).mean():.2f}, leva media (vol-target) {np.mean(sc[sc > 0]):.2f}x")
|
|
print(f" {'capitale':>10} {'ticket mediano':>16} {'ticket p10':>12} {'<$5':>7} {'<$10':>7}")
|
|
for cap in (600, 5000, 20000, 100000):
|
|
parts = [dW[i][dW[i] > 1e-12] * sc[i + 1] * cap for i in rb if sc[i + 1] > 0]
|
|
tk = np.concatenate(parts) if parts else np.array([])
|
|
if not tk.size:
|
|
print(" (nessun ticket calcolabile — controlla il motore)"); break
|
|
print(f" {cap:>10,d} {np.median(tk):>15.2f}$ {np.percentile(tk, 10):>11.2f}$ "
|
|
f"{(tk < 5).mean()*100:>6.1f}% {(tk < 10).mean()*100:>6.1f}%")
|
|
print(" ⚠️ Approssimazione DICHIARATA: il modello congelato applica il vol-target alla")
|
|
print(" SERIE dei rendimenti, non alle posizioni -> qui il ticket e' calcolato sui soli")
|
|
print(" delta di RIBILANCIAMENTO (ogni 10 giorni), ignorando il micro-turnover")
|
|
print(" giornaliero che una implementazione vera del vol-target genererebbe. E' un")
|
|
print(" LIMITE INFERIORE del numero di ordini, quindi ottimistico sul lato costi.")
|
|
print(" ⚠️ E NON rimette in discussione il ~$20k citato dal progetto: quella soglia e' sui")
|
|
print(" 19 alt Hyperliquid (min-order e liquidita' del venue), non su questi 13.")
|
|
|
|
print(f"\n[fine] {time.time()-t0:.1f}s")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|