Files
Adriano Dal Pastro caa7cc4020 research(book-3rd): nessun terzo sleeve ammissibile oggi, e per sette candidati ci sono sette muri diversi — nessuno dei quali e' il capitale
§50 BOOK-3RD. Domanda: con ~$635 su Deribit, qual e' il MIGLIOR terzo sleeve realmente
eseguibile del LIBRO LIVE a 2 sleeve (`deribit_book_sleeves`, TP01 75 / SKH01 25) — e se la
risposta e' "nessuno", a quale capitale cambia? Un solo script, nessun file di produzione
toccato, nessun ordine.

REPLICA DI CONTROLLO PRIMA DI OGNI DELTA: ShFULL 1,813 / ShHOLD 1,437 / maxDD 9,42%
riprodotti al terzo decimale; replica ancorata (h=0, off=0) bit-exact contro lo sleeve di
produzione (max|dif| = 0,0), idem XS01, VRP01 e STATARB (0,746 contro il motore originale).

IL VINCOLO MAI MESSO IN TABELLA — IL NETTING. Letto dal sorgente: `book_net_target` somma i
due sleeve in UN numero per asset e `build_book_order` manda UN ordine, quindi un terzo
sleeve DIREZIONALE su BTC/ETH non paga un min-order proprio (misurato: 163 -> 344 ordini/anno
a $635, ma il turnover sale solo del 14%; e per disuguaglianza triangolare le fee modellate
per-sleeve sono un LIMITE SUPERIORE). Il rovescio: non puo' avere un proprio stop. Un terzo
sleeve su ALTRI strumenti richiede una riga in `_CONTRACT` = codice su un percorso con soldi veri.

LA MISURA (1000 estrazioni congiunte TP01 x24 · SKH01 x23 · candidato, mediana delle
DIFFERENZE APPAIATE, funding dentro, peso 15%):
  PREVDAY    +0,192 ShFULL / +0,363 ShHOLD / +1,31pp drift — eseguibile, netta, ADDS, e SENZA
             gate pre-registrato: e' il piu' forte ed e' il meno disciplinato.
  STATARB    +0,159 / +0,081 / -0,12pp — gate aperto 27/09; NEUTRAL, robust_oos FALSE.
  VRP01      +0,121 / +0,067 / +0,19pp — fermo per REGOLA, non per lotto (sotto).
  XS01(19)   +0,110 / +0,378 / +0,79pp — NON eseguibile: Deribit non quota 5 delle 19 gambe.
  DVOLSPREAD +0,073 / +0,062 / -1,12pp — gate aperto 24/10; il maxDD PEGGIORA sopra il 15%.
  XS01-D(11) -0,024 / -0,128 / -0,73pp — l'unica versione eseguibile oggi PEGGIORA il libro.

DUE FATTI DI VENUE, LETTI DALL'API E NON DALLA MEMORIA:
  (a) il muro del LOTTO di VRP01 era misurato sulla famiglia INVERSE, che un conto USDC non
      puo' marginare: sulla USDC-lineare il lotto ETH e' $242 (non $1.832) e il BTC $772 (non
      $6.210) -> 1 lotto ETH = peso 12% gia' da ~$2.000. Cade il lotto, NON la regola
      "niente short-vol da modello in deploy". 5a occorrenza dello schema `fee_watch`.
  (b) Deribit quota 14 dei 19 major di XS01, ma 3 sono listati fra il 15 e il 21 agosto (APT
      non ha nemmeno una quota). Sulle 11 con >=1 anno di listino il meccanismo collassa, e il
      null dei sottoinsiemi (100 estrazioni da 11 fra le 19) separa le due cause: il
      sottoinsieme MEDIANO fa gia' 0,548 contro 1,265 (62% della caduta = AMPIEZZA, non
      riparabile col capitale) e quello di Deribit sta al 9° percentile (il resto = QUALI
      gambe mancano). Aspettare 2-3 listing non basta.

`weights_tilt_null`: 25/28 PASS — e il numero da leggere non e' quello ma `frac_random_beat_hold`,
che arriva a 0,94: dove vale cosi', "questo candidato migliora l'hold-out" e' un claim GENERICO.
Il gate ha potenza (fallisce su XS01-D a 10/15/20%), ma resta necessario e non sufficiente.

LA SCALA: non esiste una soglia di capitale che ammette un terzo sleeve. Il capitale sposta
solo VRP01 (~$2.000 gamba ETH, ~$6.440 gamba BTC), che e' fermo per una regola. Le due date
che contano sono un GATE (27/09, gratis) e la DECISIONE DI VENUE ($20k), che e' cio' che
riapre XS01 sulle 19 gambe.

ONESTA': attesa a priori dichiarata prima di misurare, esito A1 confermata, A2 meta' giusta,
A3 e A4 REFUTATE, A5 confermata al rovescio. Caveat pubblicato: sono 7 candidati x 4 pesi = 28
configurazioni sullo stesso hold-out, nessuna passata per un deflated-Sharpe DI SCREEN — il
modo giusto di usare la tabella e' scegliere un candidato per una ragione dichiarata PRIMA.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf
2026-08-23 02:16:18 +00:00

1263 lines
72 KiB
Python

#!/usr/bin/env python
"""r0823_book_3rd.py — §50 BOOK-3RD: qual e' il MIGLIOR TERZO SLEEVE del LIBRO LIVE, e vale la pena?
IL PROBLEMA (e perche' non e' una domanda gia' fatta). Il progetto ha DUE oggetti che vengono
continuamente confusi:
* il BOOK DI RICERCA a 5 sleeve (TP01 33 / XS01 15 / VRP01 12 / SKH01 20 / GTAA01 20) —
`sleeves.active_sleeves()` — su cui e' calcolato quasi ogni numero pubblicato;
* il LIBRO LIVE a 2 sleeve (`sleeves.deribit_book_sleeves()`, TP01 75 / SKH01 25) — l'unica
cosa che gira con soldi veri, e che non ha MAI avuto un terzo sleeve.
Il libro a 5 non e' deployabile: GTAA01 e' bloccato dal PRIIPs (verificato sul conto reale il
26/07) e vuole $3k; XS01 e' dichiarato STAT-MODE per taglia; VRP01 cade sotto la regola
"niente short-vol da modello in deploy" e a $3.000 sarebbe ETH-only con 0 lotti al peso di book.
DOMANDA DECIDIBILE: oggi, con ~$635 su Deribit, quale e' il miglior terzo sleeve REALMENTE
eseguibile — e vale la pena? Se la risposta e' "nessuno": a quale capitale cambia, e chi entra?
COSA FA
0. ATTESA A PRIORI, dichiarata PRIMA di misurare (e poi confrontata con l'esito).
1. CENSIMENTO dello stato REALE dei candidati — non quello dichiarato in memoria:
strumenti richiesti, disponibilita' su Deribit (GET pubblica), LOTTO MINIMO letto dal
venue (non stimato), data di quotazione, gate pre-registrato in corso.
2. IL VINCOLO DIMENTICATO — il NETTING. Su Deribit c'e' UNA sola posizione netta per
strumento, e TP01+SKH01 gia' si nettano su BTC/ETH_USDC-PERPETUAL. Un terzo sleeve
DIREZIONALE sugli stessi strumenti si netta con loro (zero costo di strumento, zero
separazione); uno su strumenti diversi no (serve una riga in `_CONTRACT`, cioe' CODICE su
un percorso con soldi veri). Verificato per introspezione sul sorgente di produzione.
3. REPLICA DI CONTROLLO: riprodurre i numeri pubblicati del libro live (ShFULL ~1,81 /
ShHOLD ~1,44 / maxDD ~9,4%) PRIMA di stampare qualunque delta. Due lenti, dichiarate.
4. SERIE ANCORATE, ognuna con sanity bit-exact contro lo sleeve/segnale di produzione.
5. LA MISURA: dSharpe / d drift / d maxDD del libro live aggiungendo ogni candidato a pesi
{5,10,15,20}%, col FUNDING dentro, su ESTRAZIONI CONGIUNTE dello spazio d'ancora
(mediana delle DIFFERENZE APPAIATE — mai la differenza delle mediane).
6. `weights_tilt_null` — il gate del progetto per ogni cambio di pesi.
7. ESEGUIBILITA' a $635 / $2k / $5k / $20k: ticket per gamba, min-order, haircut.
8. LA SCALA: a quale capitale entra il primo candidato, e quale.
ONESTA'. Nessuna promozione. L'output e' una TABELLA DECIDIBILE. I gate pre-registrati NON si
anticipano: per i candidati sotto gate si dice cosa succederebbe SE passassero, non si decide
al loro posto (anticipare un gate e' selezione-sull'hold-out, regola scritta del progetto).
VINCOLI RISPETTATI: nessun file di produzione toccato (src/, config/, scripts/live/,
scripts/cron_*.sh, tests/, data/paper_*, data/live); NESSUN ordine; rete = sole GET pubbliche
Deribit <=2 req/s, con astensione nei minuti :05-:10 e :24-:30 (cron_book al :07,
collect_chain al :25). Cache in scratchpad, fuori dal repo.
USO
nice -n 19 timeout 900 uv run python scripts/research/r0823_book_3rd.py
... --no-net solo cache (nessuna richiesta di rete)
... --draws N estrazioni d'ancora (default 200)
... --force ignora la cache delle serie
"""
from __future__ import annotations
import argparse
import datetime as dt
import inspect
import json
import pickle
import sys
import time
import urllib.request
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path("/opt/docker/PythagorasGoal")
for _p in (str(ROOT), str(ROOT / "scripts" / "research"),
str(ROOT / "scripts" / "research" / "alt"),
str(ROOT / "scripts" / "research" / "ortho")):
if _p not in sys.path:
sys.path.insert(0, _p)
# --- produzione (importata, MAI riscritta) ---------------------------------------------------
from src.portfolio.portfolio import (HOLDOUT, combine_outer, metrics, # noqa: E402
to_daily, weights_tilt_null)
from src.portfolio import sleeves as SL # noqa: E402
from src.live import book as LB # noqa: E402
from src.live import deribit as DB # noqa: E402
from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, # noqa: E402
simple_returns)
from src.strategies.prevday_breakout import target as prevday_target # noqa: E402
# --- macchineria di ricerca gia' validata (riusata, non riscritta) ---------------------------
import altlib as al # noqa: E402
import ortholib as ol # noqa: E402
import orthogonal_signals as OS # noqa: E402 (STATARB-RESID: segnale ESATTO)
import r0702_tp01_offset as A_TP # noqa: E402 (daily_off: ancora oraria di TP01)
import r0703_vrpimp_anchor as A_VRP # noqa: E402 (fasi settimanali di VRP01)
import r0822d_funding as FU # noqa: E402 (funding: fetch/rate/cumF/skh_daily)
from r0726_dvolspread_gate import make_book as dvs_make_book # noqa: E402
# =============================================================================================
# COSTANTI DICHIARATE
# =============================================================================================
SCRATCH = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/b3rd")
if not SCRATCH.parent.exists():
SCRATCH = Path("/tmp/pythagoras_book3rd")
CACHE = SCRATCH / "series_v3.pkl"
CAPITAL_TODAY = 635.0 # il conto reale (CLAUDE.md 2026-08)
CAP_LADDER = (635.0, 2000.0, 5000.0, 20000.0)
MIN_ORDER = 5.0 # config/live.json -> min_order_usd
FEE_SIDE = al.FEE_SIDE # 0.05%/lato (0.10% RT), il default del progetto
W_TP, W_SKH = LB.W_TP01, LB.W_SKH # 0.75 / 0.25, LETTI dalla produzione
BOOK_START = pd.Timestamp("2019-03-14", tz="UTC")
WEIGHTS_TESTED = (0.05, 0.10, 0.15, 0.20)
TP_HOURS = tuple(range(24)) # ancora giornaliera di TP01
SKH_OFFSETS = tuple(range(0, 690, 30)) # 23 offset della griglia 230m/690m
XS_PHASES = tuple(range(10)) # ciclo H=10 di XS01
VRP_PHASES = tuple(range(7)) # ciclo settimanale di VRP01
SEED = 20260823
# XS01 paga funding perche' il momentum compra i perp col funding piu' caro: +1,45%/anno
# MISURATO nell'ondata 2026-08-22 §37 (banda onesta +0,69/+1,45 se HL e' ~2,1x il venue vero).
# Qui e' applicato come DRAG DICHIARATO, non modellato per gamba: gli alt su Deribit sono
# quotati da giorni (vedi sezione 1) e non ne esiste storico di funding.
XS_FUNDING_DRAG_YR = 0.0145
API = "https://www.deribit.com/api/v2/public"
CACHE_FRESH_S = 6 * 3600
T0 = time.time()
_ARGS = None
def sez(t: str) -> None:
print("\n" + "=" * 104)
print(f" {t}")
print("=" * 104)
def sub(t: str) -> None:
print("\n" + "-" * 104)
print(f" {t}")
print("-" * 104)
def clock() -> str:
return f"[{time.time() - T0:5.0f}s]"
# =============================================================================================
# RETE — sole GET pubbliche, pacing, astensione nelle finestre del cron
# =============================================================================================
def _pace() -> None:
while dt.datetime.now(dt.timezone.utc).minute in (5, 6, 7, 8, 9, 10, 24, 25, 26, 27, 28, 29, 30):
time.sleep(15)
time.sleep(0.55)
def get_json(path: str, cache_name: str) -> dict | None:
"""GET pubblica con cache su scratchpad. Con --no-net usa solo la cache."""
SCRATCH.mkdir(parents=True, exist_ok=True)
fp = SCRATCH / cache_name
if _ARGS and _ARGS.no_net:
return json.loads(fp.read_text()) if fp.exists() else None
# cache fresca -> NESSUNA richiesta, e quindi nessuna attesa nelle finestre del cron.
# (`_pace` blocca 15s per volta nei minuti :05-:10 e :24-:30: farlo quando non si sta
# per chiamare nessuno e' solo tempo perso.)
if fp.exists() and (time.time() - fp.stat().st_mtime) < CACHE_FRESH_S:
return json.loads(fp.read_text())
try:
_pace()
with urllib.request.urlopen(f"{API}/{path}", timeout=30) as r:
d = json.loads(r.read())
fp.write_text(json.dumps(d))
return d
except Exception as e: # noqa: BLE001
print(f" [rete KO su {path}: {type(e).__name__}] -> cache")
return json.loads(fp.read_text()) if fp.exists() else None
# =============================================================================================
# 0) ATTESA A PRIORI — dichiarata PRIMA di misurare
# =============================================================================================
ATTESA = """
Registrata prima di guardare qualunque numero di questo script:
(A1) NESSUN candidato e' deployabile oggi, e il muro NON sara' il lotto minimo del venue.
Nell'ondata del 22/08 l'eseguibilita' a $600 non ha bocciato niente per TRE volte di
fila; mi aspetto che il muro sia (a) i gate pre-registrati non scaduti, (b)
`weights_tilt_null`, (c) per XS01/XSR01 il fatto che il venue e' Hyperliquid, non Deribit.
(A2) Il candidato col dSharpe piu' alto sara' XS01 (LOO 26/07: +0,125 di FULL nel libro a 5,
corr ~0 a tutto), ma `weights_tilt_null` FALLIRA' per la stessa ragione strutturale per
cui fallisce su XSR01: storia dal 2024, quindi quasi nessun in-sample.
(A3) I due candidati "eseguibili a $600" gia' etichettati tali (STATARB-RESID, DVOLSPREAD)
daranno un uplift piccolo (Sharpe standalone 0,75 e 0,61 contro 1,81 del libro): mi
aspetto |dSharpe| < 0,10 e il segno incerto sulla banda d'ancora.
(A4) La scala: il primo a entrare sara' XS01 fra $2k e $5k, NON ai $20k della memoria — ma
per una ragione di VENUE, non di taglia (serve che Deribit quoti abbastanza gambe).
(A5) Se qualcosa mi sorprende, sara' un candidato dato per eseguibile che NON lo e' (e' gia'
successo due volte: GTAA01/PRIIPs e VRP01/lotto minimo).
"""
# =============================================================================================
# 1) CENSIMENTO — lo stato REALE dei candidati
# =============================================================================================
# I 19 major di XS01, letti dalla PRODUZIONE (non riscritti: se cambiano li', cambiano qui).
XS_UNIVERSE = list(SL.XS_UNIVERSE)
# gate pre-registrati: (nome, data, cosa decide). Letti da CLAUDE.md / docstring dei monitor.
GATES = {
"STATARB-RESID": ("2026-09-27", "decisione di deploy (gate `r0724_statarb_deploy_gate`)"),
"XSR01": ("2026-10-23", "deploy solo se Sharpe>=1,0 E haircut a $5k <=40%"),
"DVOLSPREAD": ("2026-10-24", "kill se Sharpe forward < -0,50; decisione 2027-01-24"),
}
def venue_census() -> dict:
"""Strumenti Deribit REALI: perpetual USDC, spot, opzioni. Lotto minimo in DOLLARI."""
out = {"perp": {}, "spot": {}, "opt": {}, "read_ok": True}
ins = {}
for cur in ("BTC", "ETH", "USDC"):
d = get_json(f"get_instruments?currency={cur}&expired=false", f"instr_{cur}.json")
if d is None:
out["read_ok"] = False
continue
ins[cur] = d["result"]
bs = get_json("get_book_summary_by_currency?currency=USDC&kind=future", "bs_usdc_fut.json")
marks = {}
if bs:
for x in bs["result"]:
p = x.get("mark_price") or x.get("last")
if p:
marks[x["instrument_name"]] = (float(p), float(x.get("volume_usd") or 0.0))
for cur, lst in ins.items():
for x in lst:
n = x["instrument_name"]
if x["kind"] == "future" and x.get("settlement_period") == "perpetual":
mk, vol = marks.get(n, (None, None))
out["perp"][x["base_currency"]] = dict(
instr=n, min_amt=float(x["min_trade_amount"]),
created=dt.datetime.fromtimestamp(x["creation_timestamp"] / 1000,
dt.timezone.utc).date().isoformat(),
mark=mk, min_usd=(mk * float(x["min_trade_amount"])) if mk else None,
vol24=vol)
elif x["kind"] == "spot":
out["spot"][n] = dict(min_amt=float(x["min_trade_amount"]))
elif x["kind"] == "option":
key = (cur, x["base_currency"])
out["opt"].setdefault(key, set()).add(float(x["min_trade_amount"]))
return out
def sezione1(V: dict) -> dict:
sez("1) CENSIMENTO — lo stato REALE dei candidati (venue letto ORA, non memoria)")
if not V["read_ok"]:
print(" ⚠️ lista strumenti NON letta dal venue in questa corsa: si usa la cache.")
sub("1a) I 19 major di XS01 contro cio' che Deribit quota DAVVERO (perp USDC lineari)")
print(f" {'asset':<7}{'su Deribit':<12}{'strumento':<26}{'quotato dal':<13}"
f"{'lotto min':>11}{'vol 24h $':>14}")
xs_ok, xs_stable = [], []
oggi = dt.date.today()
for s in XS_UNIVERSE:
p = V["perp"].get(s)
if not p:
print(f" {s:<7}{'NO':<12}{'—':<26}{'—':<13}{'—':>11}{'—':>14}")
continue
xs_ok.append(s)
eta = (oggi - dt.date.fromisoformat(p["created"])).days
if eta >= 365:
xs_stable.append(s)
mu = f"${p['min_usd']:.2f}" if p["min_usd"] else "n/d"
v = f"{p['vol24']:,.0f}" if p["vol24"] is not None else "NON QUOTATO"
print(f" {s:<7}{'SI':<12}{p['instr']:<26}{p['created']:<13}{mu:>11}{v:>14}")
print(f"\n -> gambe su Deribit: {len(xs_ok)}/19 ({', '.join(xs_ok)})")
print(f" -> di cui quotate da >=1 anno: {len(xs_stable)}/19 ({', '.join(xs_stable)})")
print(" ⚠️ Deribit sta ampliando la lista PROPRIO ORA: le gambe piu' recenti hanno giorni")
print(" di vita e volume ~0. Una gamba quotata NON e' una gamba negoziabile.")
sub("1b) LOTTO MINIMO DELLE OPZIONI — la famiglia conta (5a occorrenza dello schema fee_watch)")
for (cur, base), mins in sorted(V["opt"].items()):
if base not in ("BTC", "ETH"):
continue
m = min(mins)
mk = (V["perp"].get(base) or {}).get("mark")
fam = "USDC-LINEARE" if cur == "USDC" else "INVERSE"
usd = f"${m * mk:,.0f}" if mk else "n/d"
print(f" {fam:<14} {base:<4} min {m:<8} = {usd:>9} di sottostante per lotto")
print("\n 📌 Il numero pubblicato il 30/07 (\"BTC min 0.1 = $6.210 -> FUORI; ETH 1 contratto")
print(" = $1.832\") e' della famiglia INVERSE, che un conto USDC non puo' marginare.")
print(" Sulla famiglia che il conto PUO' usare il lotto e' ~10x piu' piccolo.")
return dict(xs_deribit=xs_ok, xs_stable=xs_stable)
def sezione1c(V: dict, cens: dict) -> None:
sub("1c) LA TABELLA — stato REALE di ogni candidato a terzo sleeve")
eth = (V["perp"].get("ETH") or {}).get("mark") or 0.0
btc = (V["perp"].get("BTC") or {}).get("mark") or 0.0
opt_eth = min(V["opt"].get(("USDC", "ETH"), {0.1})) * eth
opt_btc = min(V["opt"].get(("USDC", "BTC"), {0.01})) * btc
n_d, n_s = len(cens["xs_deribit"]), len(cens["xs_stable"])
rows = [
("STATARB-RESID", "BTC+ETH perp USDC", "SI", f"${MIN_ORDER:.0f} (soglia NOSTRA)",
"SI — netta", "27/09 (aperto)"),
("DVOLSPREAD", "BTC+ETH perp USDC", "SI", f"${MIN_ORDER:.0f} (soglia NOSTRA)",
"SI — netta", "24/10 kill · 24/01 dec."),
("PREVDAY", "BTC+ETH perp USDC", "SI", f"${MIN_ORDER:.0f} (soglia NOSTRA)",
"SI — netta", "nessuno (mai registrato)"),
("XS01 (19 gambe)", "19 alt", "NO — 5 mancano",
f"{n_d}/19 quotate, {n_s} da >=1a", "PARZIALE (BTC/ETH)", "nessuno"),
(f"XS01-D ({n_s} gambe)", f"{n_s} alt su Deribit", "SI (ridotta)",
"$0,09-$7,72 dal venue", "PARZIALE (BTC/ETH)", "nessuno"),
("XSR01", "50 alt Hyperliquid", "NO — venue diverso",
"n/d su Deribit", "NO", "23/10 (aperto)"),
("VRP01", "opzioni BTC/ETH USDC", "SI (ETH)",
f"lotto ETH ${opt_eth:,.0f} · BTC ${opt_btc:,.0f}", "NO — altro strumento",
"nessuno (regola short-vol)"),
("GTAA01", "6 ETF USA (IB)", "NO — altro venue",
"PRIIPs: ordine RIFIUTATO", "NO", "nessuno"),
]
print(f" {'candidato':<19}{'strumenti':<24}{'su Deribit?':<19}{'capitale/lotto min':<28}"
f"{'netta col libro?':<22}{'gate'}")
print(" " + "-" * 100)
for r in rows:
print(f" {r[0]:<19}{r[1]:<24}{r[2]:<19}{r[3]:<28}{r[4]:<22}{r[5]}")
print("\n Finestra FORWARD dei monitor (SOLA LETTURA di data/paper_*/, nessuna scrittura):")
print(f" {'monitor':<12}{'barre':>7}{'ultima':>13}{'Sh fwd MODELED':>17} stato §32")
for nm, d, cad, sano in (("STATARB", "paper_statarb", 365.25, False),
("XSR01", "paper_xsr", 365.25, False),
("DVOLSPREAD", "paper_dvolspread", 365.25, False),
("PREVDAY", "paper_prevday", 365.25 * 24, True)):
try:
st = json.loads((ROOT / "data" / d / "state.json").read_text())
ts = pd.Timestamp(int(st["last_ts"]), unit="ms", tz="UTC")
rr = [json.loads(l)["net_modeled"]
for l in (ROOT / "data" / d / "returns.jsonl").read_text().splitlines() if l.strip()]
v = np.asarray(rr, float)
sh = (float(v.mean() / v.std() * np.sqrt(cad)) if len(v) > 3 and v.std() > 0
else float("nan"))
flag = "SANO (1259/1319)" if sano else "ROTTO: frazione di giorno"
print(f" {nm:<12}{st.get('n_bars', len(v)):>7}{ts.strftime('%Y-%m-%d'):>13}"
f"{sh:>17.2f} {flag}")
except Exception as e: # noqa: BLE001
print(f" {nm:<12} stato illeggibile ({type(e).__name__})")
print(" ⚠️ §32 (22/08): `advance()` consuma la barra del giorno IN CORSO -> 4 monitor su 6")
print(" registrano una FRAZIONE del giorno (STATARB ~4 min/giorno): quei tre Sharpe")
print(" NON sono un risultato. `paper_prevday` e' l'unico misurato SANO (orario), ed e'")
print(" l'unica lettura forward indipendente disponibile in questo filone.")
print(" ⚠️ Anche quello sano ha ~63 giorni: SE(Sharpe) ~ 2,3 -> non decide niente da solo.")
print("\n ⚠️ 'gate aperto' = pre-registrato e NON scaduto: in questo script si dice cosa")
print(" succederebbe SE passasse, non si decide al suo posto (anticipare un gate")
print(" pre-registrato E' selezione-sull'hold-out, ed e' una regola scritta).")
# =============================================================================================
# 2) IL NETTING — misurato sul CODICE di produzione, non ricordato
# =============================================================================================
def sezione2() -> None:
sez("2) IL VINCOLO DIMENTICATO — il NETTING su strumento unico (letto dal sorgente)")
src_net = inspect.getsource(LB.book_net_target)
src_ord = inspect.getsource(LB.build_book_order)
print(f" src/live/deribit.INSTRUMENT = {DB.INSTRUMENT}")
print(f" src/live/deribit._CONTRACT = {len(DB._CONTRACT)} strumenti dichiarati: "
f"{list(DB._CONTRACT)}")
print(f" src/live/book.W_TP01 / W_SKH = {W_TP} / {W_SKH}")
uno = ("W_TP01 * max(tp_frac" in src_net) and ("W_SKH * float(skh_sign)" in src_net)
print(f" `book_net_target` somma i due sleeve in UN solo numero per asset : {uno}"
f" (riga: `raw = weight * equity * (W_TP01*tp_frac + W_SKH*skh_sign)`)")
print(f" `build_book_order` manda UN ordine per asset, soglia min_usd : "
f"{'min_usd' in src_ord}")
print("\n CONSEGUENZA, e nessuno l'aveva messa in tabella:")
print(" * un terzo sleeve DIREZIONALE su BTC/ETH e' un TERZO ADDENDO della stessa formula:")
print(" non aggiunge un ordine, non aggiunge un min-order, non aggiunge margine lordo —")
print(" e non aggiunge nemmeno SEPARAZIONE (nessuna gamba puo' avere il proprio stop).")
print(" E' l'esatto motivo per cui la raccomandazione T1 del 26/07 fu abbandonata.")
print(" * un terzo sleeve su ALTRI strumenti (opzioni, spot, alt-perp) richiede una riga in")
print(f" `_CONTRACT` (oggi {len(DB._CONTRACT)} voci, CABLATE A MANO) piu' il ramo di")
print(" sizing corrispondente = CODICE su un percorso con soldi veri, piu' un lotto")
print(" minimo e un margine propri.")
print(" * il min-order si applica al DELTA NETTO: due sleeve che si contraddicono possono")
print(" produrre zero ordini. E' un vantaggio di costo E una perdita di controllo.")
# =============================================================================================
# 3) IL FUNDING (importato) + 4) LE SERIE ANCORATE
# =============================================================================================
def get_rate() -> dict:
"""Tassi di funding orari per asset (LINEARE dove esiste, INVERSE come proxy prima)."""
F = {}
for instr, (start, a, kind) in FU.INSTR.items():
d = FU.fetch_funding(instr, start).copy()
d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True)
F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt")
return FU.build_rate(F)
def tp01_at(h: int, rate: dict | None) -> pd.Series:
"""TP01 all'ancora oraria h (replica di sleeves._tp01_returns), funding opzionale."""
tp = TrendPortfolio(**CANONICAL)
series = {}
for a in ("BTC", "ETH"):
df = A_TP.daily_off(a, h)
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
r = simple_returns(df["close"].values.astype(float))
tgt = tp.target_series(df)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0))
if rate is not None:
net = net - held * FU.daily_funding(rate[a], idx)
net[0] = 0.0
series[a] = pd.Series(np.clip(net, -0.99, None), index=idx)
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index))
def skh_at(off: int, rate: dict | None) -> pd.Series:
"""SKH01 all'offset `off` della griglia 230m, lente CANONICA (quella dello sleeve)."""
return to_daily(FU.skh_book(off, "canonical", rate))
def _joint_off(h: int) -> pd.DataFrame:
"""Frame BTC/ETH allineato all'ancora oraria h, colonne di OS.build_joint."""
b = A_TP.daily_off("BTC", h)[["timestamp", "datetime", "close"]].rename(columns={"close": "cb"})
e = A_TP.daily_off("ETH", h)[["timestamp", "close"]].rename(columns={"close": "ce"})
j = b.merge(e, on="timestamp", how="inner").sort_values("timestamp").reset_index(drop=True)
j["r_btc"] = al.simple_returns(j["cb"].values)
j["r_eth"] = al.simple_returns(j["ce"].values)
j["log_ratio"] = np.log(j["ce"].values / j["cb"].values)
return j
def statarb_at(h: int, rate: dict | None):
"""STATARB-RESID (W=45, sgn=+1) all'ancora oraria h. Ritorna (serie daily, pos)."""
j = _joint_off(h)
pos = np.nan_to_num(np.asarray(OS.f_statarb_resid(W=45, sgn=+1)(j), float))
idx = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True))
s = OS.spread_ret(j)
held = np.zeros(len(pos)); held[1:] = pos[:-1]
net = held * s - FEE_SIDE * 2.0 * np.abs(np.diff(held, prepend=0.0))
if rate is not None: # +1 = long ETH / short BTC -> paga f_eth - f_btc
net = net - held * (FU.daily_funding(rate["ETH"], idx) - FU.daily_funding(rate["BTC"], idx))
net[0] = 0.0
return to_daily(pd.Series(net, index=idx)), pos # pos NON shiftata (7b shifta da se')
DVS_FROZEN = dict(zwin=180, tanh_k=2.0, lw=0.6, zw=1.1, tgt=0.17, svw=60)
def dvolspread(rate: dict | None):
"""DVOLSPREAD coi parametri CONGELATI del monitor. Ancora non variabile: il DVOL e' 1d."""
btc, eth = ol.aligned()
wb, we = dvs_make_book(**DVS_FROZEN)(btc, eth)
wb = np.clip(np.nan_to_num(np.asarray(wb, float)), -1, 1)
we = np.clip(np.nan_to_num(np.asarray(we, float)), -1, 1)
n = len(btc)
rb = al.simple_returns(btc["close"].values.astype(float))
re = al.simple_returns(eth["close"].values.astype(float))
pb = np.zeros(n); pb[1:] = wb[:-1]
pe = np.zeros(n); pe[1:] = we[:-1]
net = pb * rb + pe * re - FEE_SIDE * (np.abs(np.diff(pb, prepend=0.0))
+ np.abs(np.diff(pe, prepend=0.0)))
idx = pd.DatetimeIndex(btc["dt"])
if rate is not None:
net = net - (pb * FU.daily_funding(rate["BTC"], idx)
+ pe * FU.daily_funding(rate["ETH"], idx))
net[0] = 0.0
assert float(np.max(np.abs(wb + we))) < 1e-12, "il book DVOLSPREAD non e' we == -wb"
return to_daily(pd.Series(net, index=idx)), wb # gamba BTC, NON shiftata
def prevday(rate: dict | None):
"""PREVDAY range-breakout 1h, libro 50/50. Funding ORARIO (il segnale e' a 1h)."""
series, tk = {}, []
for a in ("BTC", "ETH"):
df = al.get(a, "1h")
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
tgt = np.nan_to_num(np.asarray(prevday_target(df), float))
r = al.simple_returns(df["close"].values.astype(float))
pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1]
net = pos * r - FEE_SIDE * np.abs(np.diff(pos, prepend=0.0))
if rate is not None:
t = FU.to_ms(idx)
fh = FU.cumF(rate[a], t + FU.MSH) - FU.cumF(rate[a], t)
net = net - pos * fh
net[0] = 0.0
series[a] = pd.Series(net, index=idx)
tk.append(np.abs(np.diff(pos, prepend=0.0)))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
d = to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index))
return d, np.concatenate(tk)
def xs_at(universe: list[str], phase: int, funding_drag: float = 0.0):
"""XS01 su un universo arbitrario, alla fase `phase` del ciclo H. Replica ESATTA della
formula di sleeves._xsec_returns (verificata bit-exact a universo pieno / fase 0)."""
cols = {}
for sym in universe:
p = SL._HL_DIR / 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))
C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
px = C.values; n, A = px.shape
cfg = SL.XS_CFG
lookbacks, H, k, mode, tv = cfg["lookbacks"], cfg["H"], cfg["k"], cfg["mode"], cfg["target_vol"]
disp_pct, minhist = cfg.get("disp_pct", 0), cfg.get("disp_minhist", 20)
kk = min(k, A // 2)
mlb = max(lookbacks)
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
W = np.zeros((n, A)); w = np.zeros(A); disp_hist = []
for i in range(n):
if i >= mlb and i % H == phase:
rLs = [px[i] / px[i - L] - 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(A); cnt = 0
for rL in rLs:
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd; cnt += 1
if cnt:
score /= cnt
order = np.argsort(score)
w = np.zeros(A); lo, hi = order[:kk], order[-kk:]
if mode == "mom":
w[hi] = 0.5 / kk; w[lo] = -0.5 / kk
else:
w[lo] = 0.5 / kk; w[hi] = -0.5 / kk
else:
w = np.zeros(A)
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 * (0.001 / 2.0)
s = pd.Series(net, index=C.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=C.index)
if funding_drag:
out = out - funding_drag / 365.25
# ticket per gamba: |ΔW| a ogni ribilanciamento, scalato dal vol-target del giorno
tick = np.abs(np.diff(W, axis=0, prepend=0.0)) * scale[:, None]
return to_daily(out), tick, list(C.columns)
def vrp_at(phase: int) -> pd.Series:
return to_daily(A_VRP.to_daily_lumped(A_VRP.combo_weekly(phase, f=SL.VRP_CFG["f"])))
# =============================================================================================
# 3) REPLICA DI CONTROLLO
# =============================================================================================
def sezione3(rate: dict) -> dict:
sez("3) REPLICA DI CONTROLLO — riprodurre i numeri PUBBLICATI prima di stampare un delta")
from src.portfolio.portfolio import StrategyPortfolio
P = StrategyPortfolio(SL.deribit_book_sleeves(), capital=CAPITAL_TODAY)
ref = P.combined_daily()
m, mh = metrics(ref), metrics(ref[ref.index >= HOLDOUT])
print(" LENTE A — 'sleeve' (`deribit_book_sleeves` via StrategyPortfolio): e' quella dei")
print(" numeri pubblicati. SKH01 con fill AL LIVELLO, TP01 senza min-order,")
print(" NESSUN funding.")
print(f" ShFULL {m['sharpe']:.3f} ShHOLD {mh['sharpe']:.3f} maxDD {100*m['maxdd']:.2f}%"
f" CAGR {100*m['cagr']:.2f}% n={m['n']}")
print(" atteso dal briefing: ShFULL ~1,81 / ShHOLD ~1,44 / maxDD ~9,4% -> "
f"{'RIPRODOTTO' if (abs(m['sharpe']-1.81)<0.02 and abs(mh['sharpe']-1.44)<0.02 and abs(100*m['maxdd']-9.4)<0.15) else '⚠️ NON riproduce'}")
mine0 = combine_outer({"TP01": tp01_at(0, None), "SKH01": skh_at(0, None)},
{"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START)
j = pd.concat({"a": mine0, "b": ref}, axis=1, join="outer")
dmax = float(np.nanmax(np.abs(j["a"].values - j["b"].values)))
print(f"\n [SANITY] la mia replica ancorata a (h=0, off=0) vs lo sleeve di produzione: "
f"max|dif| = {dmax:.3e} {'OK' if dmax < 1e-12 else 'FAIL'}")
assert dmax < 1e-12, "replica ancorata NON bit-exact: tutto il resto misurerebbe altro"
withf = combine_outer({"TP01": tp01_at(0, rate), "SKH01": skh_at(0, rate)},
{"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START)
mf, mfh = metrics(withf), metrics(withf[withf.index >= HOLDOUT])
print(f"\n LENTE A + FUNDING (misurato §35, non modellato prima del 22/08):")
print(f" ShFULL {mf['sharpe']:.3f} ShHOLD {mfh['sharpe']:.3f} "
f"maxDD {100*mf['maxdd']:.2f}% CAGR {100*mf['cagr']:.2f}%")
print(f" costo del funding: dSharpe {mf['sharpe']-m['sharpe']:+.3f}, "
f"dCAGR {100*(mf['cagr']-m['cagr']):+.2f} pp")
lp = FU.book_75_25(rate, 0, "hourly", 600.0)
mlp, mlph = metrics(lp), metrics(lp[lp.index >= HOLDOUT])
print("\n LENTE B — 'path live' (`CC.book_series('hourly')` + funding): min-order $5, fill al")
print(" close 5m del prossimo confine orario. E' cio' che il cron esegue.")
print(f" ShFULL {mlp['sharpe']:.3f} ShHOLD {mlph['sharpe']:.3f} "
f"maxDD {100*mlp['maxdd']:.2f}% CAGR {100*mlp['cagr']:.2f}%")
print("\n 📌 Le due lenti NON danno lo stesso libro (1,81 contro 1,50 di Sharpe FULL). La")
print(" misura di questo filone gira sulla LENTE A col funding, perche' e' quella in cui")
print(" sono espressi TUTTI i candidati (serie di sleeve) e quella dei numeri pubblicati.")
print(" Il delta e' comunque un confronto APPAIATO: la lente si cancella in gran parte.")
return dict(ref=ref, m=m, mh=mh, base_f=withf)
# =============================================================================================
# 4) COSTRUZIONE DELLE SERIE ANCORATE (con cache)
# =============================================================================================
def build_series(rate: dict, cens: dict, force: bool = False) -> dict:
if CACHE.exists() and not force:
try:
S = pickle.loads(CACHE.read_bytes())
if S.get("_universe") == (cens["xs_stable"], cens["xs_deribit"]):
print(f" {clock()} serie da cache ({CACHE.name})")
return S
except Exception: # noqa: BLE001
pass
S: dict = {"_universe": (cens["xs_stable"], cens["xs_deribit"])}
t = time.time()
S["TP01"] = {h: tp01_at(h, rate) for h in TP_HOURS}
print(f" {clock()} TP01 x{len(TP_HOURS)} ancore orarie ({time.time()-t:.0f}s)")
t = time.time()
S["SKH01"] = {o: skh_at(o, rate) for o in SKH_OFFSETS}
print(f" {clock()} SKH01 x{len(SKH_OFFSETS)} offset di griglia ({time.time()-t:.0f}s)")
t = time.time()
sa = {h: statarb_at(h, rate) for h in TP_HOURS}
S["STATARB"] = {h: v[0] for h, v in sa.items()}
S["_statarb_pos"] = sa[0][1]
print(f" {clock()} STATARB x{len(TP_HOURS)} ancore ({time.time()-t:.0f}s)")
t = time.time()
d, dpos = dvolspread(rate)
S["DVOLSPREAD"] = {0: d}; S["_dvs_pos"] = dpos
p, ptk = prevday(rate)
S["PREVDAY"] = {0: p}; S["_prevday_turn"] = ptk
print(f" {clock()} DVOLSPREAD + PREVDAY ({time.time()-t:.0f}s)")
t = time.time()
xs19 = {ph: xs_at(XS_UNIVERSE, ph, XS_FUNDING_DRAG_YR) for ph in XS_PHASES}
S["XS01"] = {ph: v[0] for ph, v in xs19.items()}
S["_xs19_tick"] = xs19[0][1]; S["_xs19_cols"] = xs19[0][2]
xsd = {ph: xs_at(cens["xs_stable"], ph, XS_FUNDING_DRAG_YR) for ph in XS_PHASES}
S["XS01-D"] = {ph: v[0] for ph, v in xsd.items()}
S["_xsd_tick"] = xsd[0][1]; S["_xsd_cols"] = xsd[0][2]
xsd14 = {ph: xs_at(cens["xs_deribit"], ph, XS_FUNDING_DRAG_YR) for ph in XS_PHASES}
S["XS01-D14"] = {ph: v[0] for ph, v in xsd14.items()}
S["_xsd14_cols"] = xsd14[0][2]
print(f" {clock()} XS01 (19 gambe) + XS01-D ({len(cens['xs_stable'])} gambe) x10 fasi "
f"({time.time()-t:.0f}s)")
t = time.time()
S["VRP01"] = {ph: vrp_at(ph) for ph in VRP_PHASES}
print(f" {clock()} VRP01 x{len(VRP_PHASES)} fasi ({time.time()-t:.0f}s)")
CACHE.parent.mkdir(parents=True, exist_ok=True)
CACHE.write_bytes(pickle.dumps(S))
return S
def sezione4(S: dict, cens: dict) -> None:
sez("4) LE SERIE ANCORATE — sanity obbligatorio, poi il profilo standalone")
x0, _, cols = xs_at(XS_UNIVERSE, 0, 0.0)
ref = to_daily(SL._xsec_returns())
jj = pd.concat({"a": x0, "b": ref}, axis=1, join="outer")
dm = float(np.nanmax(np.abs(jj["a"].values - jj["b"].values)))
print(f" [SANITY] xs_at(19 gambe, fase 0, drag 0) vs sleeves._xsec_returns : "
f"max|dif| = {dm:.3e} {'OK' if dm < 1e-12 else 'FAIL'}")
assert dm < 1e-12, "la replica XS01 non e' bit-exact"
v0 = vrp_at(0); vr = to_daily(SL._vrp_combo_returns())
jj = pd.concat({"a": v0, "b": vr}, axis=1, join="outer")
dm2 = float(np.nanmax(np.abs(np.nan_to_num(jj["a"].values) - np.nan_to_num(jj["b"].values))))
print(f" [SANITY] vrp_at(fase 0) vs sleeves._vrp_combo_returns : "
f"max|dif| = {dm2:.3e} {'OK' if dm2 < 1e-12 else 'ATTENZIONE'}")
jj = OS.build_joint("1d")
ev = OS.eval_spread(jj, OS.f_statarb_resid(W=45, sgn=+1)(jj))
mine = metrics(statarb_at(0, None)[0])["sharpe"]
print(f" [SANITY] STATARB senza funding, mio percorso {mine:.3f} vs il motore originale "
f"`OS.eval_spread` {ev['full']['sharpe']:.3f} (scarto {mine-ev['full']['sharpe']:+.3f})")
print(" -> `daily_off(h=0)` e `al.get('1d')` sono lo STESSO resample: due percorsi")
print(" indipendenti danno la stessa serie, quindi l'ancora h=0 e' la canonica.")
print(f"\n {'candidato':<16}{'da':>12}{'n':>7}{'ShFULL':>9}{'ShHOLD':>9}{'maxDD':>9}"
f"{'CAGR':>9}{'corr libro':>12}")
base = combine_outer({"TP01": S["TP01"][0], "SKH01": S["SKH01"][0]},
{"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START)
for nm in CANDIDATI:
s = S[nm][sorted(S[nm])[0]]
s = s[s.index >= BOOK_START] if nm != "XS01" else s
m = metrics(s); mh = metrics(s[s.index >= HOLDOUT])
j = pd.concat({"a": s, "b": base}, axis=1, join="inner").dropna()
c = float(j["a"].corr(j["b"])) if len(j) > 60 else float("nan")
print(f" {nm:<16}{str(s.index.min().date()):>12}{m['n']:>7}{m['sharpe']:>9.3f}"
f"{mh['sharpe']:>9.3f}{100*m['maxdd']:>8.1f}%{100*m['cagr']:>8.1f}%{c:>12.3f}")
print(f"\n NB XS01/XS01-D includono il drag di funding dichiarato "
f"({100*XS_FUNDING_DRAG_YR:.2f}%/anno, §37); gli altri hanno il funding modellato "
"gamba per gamba.")
print(f" XS01-D gira su {len(cens['xs_stable'])} gambe: {', '.join(cens['xs_stable'])}")
def sezione4b(S: dict) -> None:
sub("4b) `marginal_vs_tp01` — il verdetto STANDARD del progetto su ogni candidato")
print(" Un candidato direzionale/relative-value si giudica su ADDS (corr, uplift OOS,")
print(" alpha residua, robust_oos multicut, has_insample_edge, is_hedge), non sullo Sharpe.")
print(f"\n {'candidato':<12}{'verdetto':>10}{'corr':>8}{'robust_oos':>12}"
f"{'insample_edge':>15}{'is_hedge':>10}{'beats_noise':>13}")
for c in CANDIDATI:
d = S[c][sorted(S[c])[0]]
try:
m = al.marginal_vs_tp01(d)
except Exception as e: # noqa: BLE001
print(f" {c:<12} non calcolabile: {type(e).__name__}: {e}")
continue
cf = m.get("corr_full")
print(f" {c:<12}{str(m.get('marginal_verdict')):>10}"
f"{(cf if cf is not None else float('nan')):>8.3f}"
f"{str(m.get('robust_oos')):>12}{str(m.get('has_insample_edge')):>15}"
f"{str(m.get('is_hedge')):>10}{str(m.get('beats_noise_null')):>13}")
print("\n ⚠️ Su una serie che comincia nel 2024 (XS01*) `marginal_vs_tp01` ha poco campione")
print(" in-sample: il suo verdetto li' e' debole per costruzione, non informativo.")
def sezione4c(cens: dict, n_sub: int = 100) -> None:
sub("4c) IL COLLASSO DI XS01-D: e' l'AMPIEZZA o e' QUEL sottoinsieme? (null dei subset)")
k = len(cens["xs_stable"])
print(f" XS01 a 19 gambe fa Sharpe ~1,27; ristretto alle {k} quotate su Deribit da >=1 anno")
print(" crolla. Due spiegazioni possibili, e vanno separate: (a) con poche gambe il")
print(f" long-5/short-5 prende 10 posizioni su {k} e non seleziona piu' niente (AMPIEZZA);")
print(" (b) le gambe che Deribit non quota sono proprio quelle che portavano l'edge.")
print(f" Test: {n_sub} sottoinsiemi CASUALI di {k} gambe fra le 19, stesso meccanismo,")
print(" e si guarda dove cade quello di Deribit. (Stessa forma del null dei 6-subset che")
print(" il 25/07 mise GTAA6 al 95° pctl.)")
rng = np.random.default_rng(SEED)
sh = []
for _ in range(n_sub):
u = list(rng.choice(XS_UNIVERSE, size=k, replace=False))
sh.append(metrics(xs_at(u, 0, XS_FUNDING_DRAG_YR)[0])["sharpe"])
sh = np.array(sh)
mine = metrics(xs_at(cens["xs_stable"], 0, XS_FUNDING_DRAG_YR)[0])["sharpe"]
pct = float((sh < mine).mean() * 100.0)
print(f"\n {n_sub} sottoinsiemi casuali da {k}: mediana {np.median(sh):.3f} "
f"[p10 {np.percentile(sh,10):.3f}, p90 {np.percentile(sh,90):.3f}] "
f"min {sh.min():.3f} max {sh.max():.3f}")
print(f" il sottoinsieme DERIBIT: {mine:.3f} -> {pct:.0f}° percentile")
print(f" riferimento a 19 gambe: {metrics(xs_at(XS_UNIVERSE, 0, XS_FUNDING_DRAG_YR)[0])['sharpe']:.3f}")
full = metrics(xs_at(XS_UNIVERSE, 0, XS_FUNDING_DRAG_YR)[0])["sharpe"]
med = float(np.median(sh))
q_ampiezza = (full - med) / (full - mine) if abs(full - mine) > 1e-9 else float("nan")
print(f"\n -> DIAGNOSI, e sono DUE effetti, non uno (la mia prima stesura ne dichiarava uno")
print(" solo e sarebbe stata una semplificazione comoda):")
print(f" (1) AMPIEZZA: gia' il sottoinsieme MEDIANO da {k} gambe scende {full:.3f} -> "
f"{med:.3f}.")
print(f" Vale il {100*q_ampiezza:.0f}% della caduta totale, e nessun capitale la ripara:")
print(f" con {k} gambe il long-5/short-5 tiene 10 posizioni su {k}.")
print(f" (2) SFORTUNA DI LISTINO: dentro quella distribuzione il sottoinsieme che Deribit")
print(f" quota davvero sta al {pct:.0f}° percentile ({mine:.3f} contro mediana {med:.3f}).")
print(f" Le 8 gambe mancanti non sono un campione casuale: sono gli alt piu' recenti")
print(" e piu' dispersi, cioe' quelli da cui un cross-sectional prende il segnale.")
print(" Conseguenza operativa identica in entrambi i casi: aspettare che Deribit quoti")
print(f" 2-3 alt in piu' non basta — il sottoinsieme mediano a {k} gambe e' gia' meta'")
print(" del 19-gambe, e la ricostruzione dell'universo pieno passa dal SECONDO VENUE.")
print(" ⚠️ Questo test gira sui prezzi HYPERLIQUID anche per il sottoinsieme 'Deribit': e'")
print(" il segnale, non il fill. La divergenza fra venue su un ranking cross-sezionale e'")
print(" gia' stata misurata trascurabile (22/08: corr 0,9991 fra i due venue).")
# =============================================================================================
# 5) LA MISURA — delta APPAIATI sulla banda d'ancora congiunta
# =============================================================================================
PASSED_TXT = "?"
CANDIDATI = ["XS01", "XS01-D", "XS01-D14", "STATARB", "DVOLSPREAD", "PREVDAY", "VRP01"]
ANCHOR_OF = {"XS01": XS_PHASES, "XS01-D": XS_PHASES, "XS01-D14": XS_PHASES,
"STATARB": TP_HOURS,
"DVOLSPREAD": (0,), "PREVDAY": (0,), "VRP01": VRP_PHASES}
def _stats(s: pd.Series) -> tuple[float, float, float, float]:
m = metrics(s)
mh = metrics(s[s.index >= HOLDOUT])
return m["sharpe"], mh["sharpe"], m["cagr"], m["maxdd"]
def sezione5(S: dict, draws: int) -> dict:
sez(f"5) LA MISURA — dSharpe / d drift / d maxDD del LIBRO LIVE con un terzo sleeve")
print(f" Spazio d'ancora congiunto: TP01 x{len(TP_HOURS)} · SKH01 x{len(SKH_OFFSETS)} · "
f"candidato x(1..{len(XS_PHASES)}).")
print(f" {draws} estrazioni UNIFORMI INDIPENDENTI (seed {SEED}); la statistica e' la")
print(" MEDIANA DELLE DIFFERENZE APPAIATE (mai la differenza delle mediane: cadono su")
print(" ancore diverse — errore ribaltante catturato il 26/07).")
rng = np.random.default_rng(SEED)
hs = rng.choice(TP_HOURS, size=draws)
os_ = rng.choice(SKH_OFFSETS, size=draws)
ph = {c: rng.choice(ANCHOR_OF[c], size=draws) for c in CANDIDATI}
out: dict = {c: {w: {"sh": [], "shh": [], "cagr": [], "dd": []} for w in WEIGHTS_TESTED}
for c in CANDIDATI}
base_stats = []
t = time.time()
for i in range(draws):
cols2 = {"TP01": S["TP01"][hs[i]], "SKH01": S["SKH01"][os_[i]]}
b = combine_outer(cols2, {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START)
bs = _stats(b)
base_stats.append(bs)
for c in CANDIDATI:
cols3 = dict(cols2); cols3["CAND"] = S[c][ph[c][i]]
for w in WEIGHTS_TESTED:
a = combine_outer(cols3, {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w),
"CAND": w}, lo=BOOK_START)
st = _stats(a)
out[c][w]["sh"].append(st[0] - bs[0])
out[c][w]["shh"].append(st[1] - bs[1])
out[c][w]["cagr"].append(st[2] - bs[2])
out[c][w]["dd"].append(st[3] - bs[3])
if i == 4:
print(f" {clock()} ritmo: {(time.time()-t)/5:.2f}s/estrazione -> stimati "
f"{(time.time()-t)/5*draws:.0f}s")
B = np.array(base_stats)
print(f"\n LIBRO LIVE DE-LUCKATO (2 sleeve, funding dentro), mediana delle {draws} estrazioni:")
print(f" ShFULL {np.median(B[:,0]):.3f} [p10 {np.percentile(B[:,0],10):.3f}, "
f"p90 {np.percentile(B[:,0],90):.3f}] ShHOLD {np.median(B[:,1]):.3f} "
f"CAGR {100*np.median(B[:,2]):.2f}% maxDD {100*np.median(B[:,3]):.2f}%")
can = _stats(combine_outer({"TP01": S["TP01"][0], "SKH01": S["SKH01"][0]},
{"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START))
pc = float((B[:, 0] < can[0]).mean() * 100.0)
print(f" ancora CANONICA (h=0, off=0): ShFULL {can[0]:.3f} = {pc:.1f}° pctl della banda "
f"-> fortuna d'ancora {can[0]-np.median(B[:,0]):+.3f}")
print(" ⚠️ `d maxDD` esce ~0,00 per XS01*: il maxDD FULL del libro e' fissato dal 2022, e")
print(" quei candidati cominciano nel 2024 -> non possono toccarlo. Non e' 'non protegge',")
print(" e' 'non c'era'. Per loro la colonna che informa e' d maxDD sull'hold-out, non FULL.")
for c in CANDIDATI:
sub(f"5.{CANDIDATI.index(c)+1}) {c}")
print(f" {'peso':>6}{'dShFULL':>12}{'>0':>7}{'[p10,p90]':>20}{'dShHOLD':>11}{'>0':>7}"
f"{'d CAGR pp':>12}{'d maxDD pp':>13}{'>0(peggio)':>12}")
for w in WEIGHTS_TESTED:
r = out[c][w]
a = np.array(r["sh"]); ah = np.array(r["shh"])
ac = np.array(r["cagr"]); ad = np.array(r["dd"])
print(f" {w:>5.0%}{np.median(a):>+12.4f}{100*(a>0).mean():>6.0f}%"
f" [{np.percentile(a,10):+.3f},{np.percentile(a,90):+.3f}]"
f"{np.median(ah):>+11.4f}{100*(ah>0).mean():>6.0f}%"
f"{100*np.median(ac):>+12.2f}{100*np.median(ad):>+13.2f}"
f"{100*(ad>0).mean():>11.0f}%")
return dict(out=out, base=B)
def sezione5b(S: dict) -> None:
sez("5b) A COSA SERVE UN dSharpe — il guadagno e' RITORNO o solo RISCHIO IN MENO?")
print(" In sezione 5 quasi tutti i candidati alzano lo Sharpe e ABBASSANO il drift: a")
print(" nozionale invariato il libro guadagna MENO. E' la regola gia' scritta il 25/07")
print(" (\"un diversificatore a basso CAGR si giudica a ISO-RISCHIO, mai a iso-nozionale\").")
print(" ⚠️ Il null del de-levering qui sarebbe DEGENERE (regola 22/08 §8d: sh(k·base) ≡")
print(" sh(base)) -> non e' un test, e' un cambio di unita'. La domanda giusta e' l'altra:")
print(" quanto vale quel dSharpe SE lo si riconverte in ritorno, e la leva ESISTE?")
print(" Qui: si leva il libro a 2 sleeve del fattore k che ne eguaglia la vol al libro a 3,")
print(" e si confrontano i DRIFT. k<1 = il terzo sleeve compra rischio in meno.")
print(" ⚠️ E la leva NON esiste come knob: `config/live.json` non ha una chiave di scala")
print(" (§38, verificato: servirebbe toccare W_TP01/W_SKH in src/live/book.py = CODICE")
print(" su un percorso con soldi veri). Finche' non esiste, la colonna 'd drift' di")
print(" sezione 5 e' quella che si incassa, e questa e' quella che si potrebbe incassare.")
base = combine_outer({"TP01": S["TP01"][0], "SKH01": S["SKH01"][0]},
{"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START)
rb = np.asarray(base.values, float)
v2 = float(rb.std() * np.sqrt(365.25))
d2 = float(np.mean(np.log1p(rb)) * 365.25)
print(f"\n libro a 2 sleeve (ancora canonica): vol {100*v2:.2f}% drift log {100*d2:.2f}%/anno")
print(f"\n {'candidato':<12}{'peso':>6}{'vol 3sl':>10}{'k iso-vol':>11}"
f"{'drift 2sl levato':>18}{'drift 3sl':>12}{'d drift pp':>12}{'verdetto':>14}")
for c in CANDIDATI:
for w in (0.15,):
cols = {"TP01": S["TP01"][0], "SKH01": S["SKH01"][0], "CAND": S[c][sorted(S[c])[0]]}
a = combine_outer(cols, {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), "CAND": w},
lo=BOOK_START)
# confronto sulla FINESTRA COMUNE: uno sleeve che parte nel 2024 non si giudica
# contro un libro che comincia nel 2019 (sarebbero due campioni diversi).
j = pd.concat({"a": a, "b": base}, axis=1, join="inner").dropna()
ra = np.asarray(j["a"].values, float); rbb = np.asarray(j["b"].values, float)
v3 = float(ra.std() * np.sqrt(365.25)); vb = float(rbb.std() * np.sqrt(365.25))
k = v3 / vb if vb > 0 else float("nan")
dr3 = float(np.mean(np.log1p(ra)) * 365.25)
dr2k = float(np.mean(np.log1p(np.clip(k * rbb, -0.99, None))) * 365.25)
dd = 100 * (dr3 - dr2k)
verdict = "AGGIUNGE" if dd > 0.05 else ("PEGGIORA" if dd < -0.05 else "neutro")
print(f" {c:<12}{w:>5.0%}{100*v3:>9.2f}%{k:>11.3f}{100*dr2k:>17.2f}%"
f"{100*dr3:>11.2f}%{dd:>+12.2f}{verdict:>14}")
print("\n (finestra COMUNE ai due libri: per XS01* e VRP01 e' la loro storia, non il 2019+.)")
# =============================================================================================
# 6) weights_tilt_null
# =============================================================================================
def sezione6(S: dict) -> dict:
sez("6) `weights_tilt_null` — IL gate del progetto per ogni cambio di pesi")
print(" Ogni proposta di cambio pesi si giudica contro il null dei tilt casuali")
print(" cap-respecting: gate_pass solo se delta_insample >= 0 E percentile < firma")
print(f" best-of-k. k_seen = {len(CANDIDATI)}x{len(WEIGHTS_TESTED)} = "
f"{len(CANDIDATI)*len(WEIGHTS_TESTED)} configurazioni guardate sull'hold-out qui.")
k_seen = len(CANDIDATI) * len(WEIGHTS_TESTED)
res = {}
print(f"\n {'candidato':<14}{'peso':>6}{'d_insample':>13}{'d_hold':>10}{'d_full':>10}"
f"{'pctl_hold':>11}{'best-of-k':>11}{'rnd>cur':>9}{'GATE':>9}")
for c in CANDIDATI:
cols = {"TP01": S["TP01"][0], "SKH01": S["SKH01"][0], c: S[c][sorted(S[c])[0]]}
w_cur = {"TP01": W_TP, "SKH01": W_SKH, c: 0.0}
for w in WEIGHTS_TESTED:
w_prop = {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), c: w}
try:
r = weights_tilt_null(cols, w_cur, w_prop, floor=0.03, n=300, k_seen=k_seen)
except Exception as e: # noqa: BLE001
print(f" {c:<14}{w:>5.0%} non calcolabile: {type(e).__name__}")
continue
res[(c, w)] = r
print(f" {c:<14}{w:>5.0%}{r['delta_insample']:>+13.4f}{r['delta_hold']:>+10.4f}"
f"{r['delta_full']:>+10.4f}{r['pctl_hold']:>10.1f}°{r['bestofk_pctl']:>10.1f}°"
f"{r['frac_random_beat_hold']:>9.2f}"
f"{('PASS' if r['gate_pass'] else 'FAIL'):>9}")
npass = sum(1 for v in res.values() if v["gate_pass"])
print(f"\n PASS {npass}/{len(res)}. ⚠️ VA LETTO BENE, e la colonna che conta e' `rnd>cur`:")
print(" e' la frazione di tilt CASUALI a 3 sleeve che battono il libro attuale sull'hold-out.")
print(" Dove vale 0,9 il claim \"questo candidato migliora l'hold-out\" e' GENERICO: lo fa")
print(" quasi qualunque cosa gli si metta accanto, e il gate passa proprio perche' la")
print(" proposta NON e' eccezionale fra i tilt casuali (pctl basso = non e' un best-of-k).")
print(" `weights_tilt_null` e' NECESSARIO, non sufficiente — e' scritto nel suo docstring.")
print(" ✅ Controllo positivo: il gate HA potenza — fallisce su XS01-D a 10/15/20% (dove il")
print(" delta in-sample e' negativo). Un gate che passa tutto sarebbe indistinguibile")
print(" da uno rotto.")
return res
# =============================================================================================
# 7) ESEGUIBILITA' PER CAPITALE
# =============================================================================================
def _gated_sharpe(pos: np.ndarray, ret: np.ndarray, idx, alloc: float,
n_legs: int, fee_side: float = FEE_SIDE) -> tuple[float, int, float]:
"""Sharpe con min-order per GAMBA: un Δ di nozionale sotto $5 non si esegue."""
per_leg = alloc / max(n_legs, 1)
held = np.empty(len(pos)); cur = 0.0; n_tr = 0; n_skip = 0
for i in range(len(pos)):
if abs(pos[i] - cur) * per_leg >= MIN_ORDER:
cur = pos[i]; n_tr += 1
elif abs(pos[i] - cur) > 1e-12:
n_skip += 1
held[i] = cur
h = np.zeros(len(held)); h[1:] = held[:-1]
net = h * ret - fee_side * n_legs * np.abs(np.diff(h, prepend=0.0)); net[0] = 0.0
s = pd.Series(net, index=idx)
return metrics(to_daily(s))["sharpe"], n_tr, (n_skip / max(n_tr + n_skip, 1))
def sezione7(S: dict, V: dict, cens: dict) -> None:
sez("7) ESEGUIBILITA' PER CAPITALE — ticket per gamba, min-order, haircut")
print(f" min_order_usd = ${MIN_ORDER:.0f} (config/live.json, letto). E' la soglia NOSTRA:")
print(" il lotto del VENUE e' 1-2 ordini di grandezza sotto (sezione 1).")
sub("7a) TICKET MEDIO PER GAMBA al peso 15% del libro")
rows = []
j = _joint_off(0)
idx_sa = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True))
sa_pos = S["_statarb_pos"]
sa_dtk = np.abs(np.diff(sa_pos, prepend=0.0))
btc, eth = ol.aligned()
dvs_tk = np.abs(np.diff(S["_dvs_pos"], prepend=0.0))
xs19_tk = S["_xs19_tick"]; xsd_tk = S["_xsd_tick"]
pv_tk = S["_prevday_turn"]
spec = [
("STATARB", sa_dtk[sa_dtk > 1e-9], 2, "BTC+ETH perp (NETTA col libro)"),
("DVOLSPREAD", dvs_tk[dvs_tk > 1e-9], 2, "BTC+ETH perp (NETTA col libro)"),
("PREVDAY", pv_tk[pv_tk > 1e-9], 2, "BTC+ETH perp (NETTA col libro)"),
("XS01", xs19_tk[xs19_tk > 1e-9], 2 * SL.XS_CFG["k"], "19 gambe HL (venue diverso)"),
(f"XS01-D", xsd_tk[xsd_tk > 1e-9], 2 * min(SL.XS_CFG["k"], len(cens["xs_stable"]) // 2),
f"{len(cens['xs_stable'])} gambe Deribit (>=1a)"),
]
print(f" {'candidato':<12}{'gambe':>7}{'Δpeso/gamba med':>18}" +
"".join(f"{('$'+str(int(c))):>12}" for c in CAP_LADDER) + " nota")
for nm, tk, legs, nota in spec:
med = float(np.median(tk)) if len(tk) else 0.0
cells = ""
for C in CAP_LADDER:
tick = med * C * 0.15 # Δpeso di UNA gamba x nozionale allocato
cells += f"{('$%.2f' % tick):>12}"
print(f" {nm:<12}{legs:>7}{med:>18.4f}{cells} {nota}")
print(f" (ticket = Δpeso mediano di UNA gamba x (capitale x 15%). Sotto ${MIN_ORDER:.0f} "
"l'ordine NON parte.)")
sub("7b) HAIRCUT DA MIN-ORDER — Sharpe modellato vs Sharpe eseguibile")
print(f" {'candidato':<12}" + "".join(f"{('$'+str(int(c))):>26}" for c in CAP_LADDER))
print(f" {'':<12}" + "".join(f"{'Sh (haircut, %salt)':>26}" for _ in CAP_LADDER))
sa_ret = OS.spread_ret(j)
# il book DVOLSPREAD e' (wb su BTC, -wb su ETH) -> il ritorno per unita' di wb e' rb - re.
dvs_ret = (al.simple_returns(btc["close"].values.astype(float))
- al.simple_returns(eth["close"].values.astype(float)))
for nm, pos, ret, idx, legs in (
("STATARB", sa_pos, sa_ret, idx_sa, 2),
("DVOLSPREAD", S["_dvs_pos"], dvs_ret, pd.DatetimeIndex(btc["dt"]), 2)):
line = f" {nm:<12}"
base_sh = _gated_sharpe(pos, ret, idx, 1e12, legs)[0]
for C in CAP_LADDER:
sh, ntr, skip = _gated_sharpe(pos, ret, idx, C * 0.15, legs)
line += f"{('%.3f (%+.3f, %.0f%%)' % (sh, sh-base_sh, 100*skip)):>26}"
print(line)
print(" (Sh a capitale infinito = modellato. Il segno del haircut puo' essere POSITIVO: il")
print(" pavimento salta ribilanci piccoli e risparmia fee — e' de-levering, non merito.)")
sub("7c) VRP01 — l'aritmetica dei LOTTI (non e' min-order, e' granularita' intera)")
e_mk = (V["perp"].get("ETH") or {}).get("mark") or 0.0
b_mk = (V["perp"].get("BTC") or {}).get("mark") or 0.0
lot_e = min(V["opt"].get(("USDC", "ETH"), {0.1})) * e_mk
lot_b = min(V["opt"].get(("USDC", "BTC"), {0.01})) * b_mk
print(f" Il collaterale dello sleeve e' il PUT VENDUTO (cash-secured sullo strike): un lotto")
print(f" vale ~1 unita' di sottostante x min_trade_amount.")
print(f" ETH_USDC opzioni : lotto minimo = ${lot_e:,.0f} di nozionale")
print(f" BTC_USDC opzioni : lotto minimo = ${lot_b:,.0f} di nozionale")
print(f"\n {'capitale':>10}{'peso 12%':>11}{'lotti ETH':>11}{'lotti BTC':>11}"
f"{'% del conto per 1 lotto ETH':>30}")
for C in CAP_LADDER:
a = C * 0.12
print(f" {('$%.0f' % C):>10}{('$%.0f' % a):>11}{int(a // lot_e) if lot_e else 0:>11}"
f"{int(a // lot_b) if lot_b else 0:>11}{(100*lot_e/C):>29.1f}%")
print(f"\n -> 1 lotto ETH = peso di book del 12% a capitale ~${lot_e/0.12:,.0f}; il BTC entra")
print(f" a ~${lot_b/0.12:,.0f}. Il 30/07 questi numeri erano ${1832/0.12:,.0f} e "
f"${6210/0.12:,.0f} perche' misurati sulla famiglia INVERSE.")
print(" ⚠️ NON promuove VRP01: resta fuori per la REGOLA 'niente short-vol da modello in")
print(" deploy' e per f=0,73 misurato sulle quote vere. Cade il muro del LOTTO, non la regola.")
def sezione7d(rate: dict) -> None:
sub("7d) IL CONTO GIUSTO PER UN CANDIDATO CHE NETTA — min-order sul TARGET NETTO")
print(" 7a/7b trattano il candidato come un libro a se': per uno sleeve che NETTA e' il")
print(" conto SBAGLIATO. Il cron manda UN ordine per asset sul target NETTO, e il")
print(" pavimento di $5 morde li'. Qui il target netto e' ricostruito come in produzione:")
print(" net_a = 0.5 · E · [0.75(1-w)·tp_frac_a + 0.25(1-w)·skh_sign_a + w·cand_pos_a]")
tp = TrendPortfolio(**CANONICAL)
base_pos, cand_pos = {}, {"STATARB": {}, "DVOLSPREAD": {}}
for a in ("BTC", "ETH"):
df = A_TP.daily_off(a, 0)
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
frac = np.nan_to_num(np.asarray(tp.target_series(df), float))
_, tr = FU.skh_daily(a, 0, "canonical", rate)
sign = np.zeros(len(idx))
tms = FU.to_ms(idx)
for t in tr: # segno SKH01 sui giorni in cui e' aperto
m = (tms >= t["t_in"]) & (tms < t["t_out"])
sign[m] = t["dir"]
base_pos[a] = pd.DataFrame({"frac": frac, "sign": sign}, index=idx)
j = _joint_off(0)
ix = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True))
sp = np.nan_to_num(np.asarray(OS.f_statarb_resid(W=45, sgn=+1)(j), float))
cand_pos["STATARB"]["ETH"] = pd.Series(sp, index=ix)
cand_pos["STATARB"]["BTC"] = pd.Series(-sp, index=ix)
btc, eth = ol.aligned()
wb, _we = dvs_make_book(**DVS_FROZEN)(btc, eth)
wb = np.clip(np.nan_to_num(np.asarray(wb, float)), -1, 1)
ixd = pd.DatetimeIndex(btc["dt"])
cand_pos["DVOLSPREAD"]["BTC"] = pd.Series(wb, index=ixd)
cand_pos["DVOLSPREAD"]["ETH"] = pd.Series(-wb, index=ixd)
def net_orders(cand: str | None, w: float, E: float) -> tuple[int, float, float]:
n_ord, tot = 0, 0.0
yrs = max(len(base_pos[a]) for a in ("BTC", "ETH")) / 365.25
for a in ("BTC", "ETH"):
b = base_pos[a]
raw = 0.5 * E * ((W_TP * (1 - w)) * np.maximum(b["frac"].values, 0.0)
+ (W_SKH * (1 - w)) * b["sign"].values)
ser = pd.Series(raw, index=b.index)
if cand:
cp = cand_pos[cand][a].reindex(ser.index).fillna(0.0)
ser = ser + 0.5 * E * w * cp.values
cur = 0.0
for v in ser.values:
if abs(v - cur) >= MIN_ORDER:
n_ord += 1; tot += abs(v - cur); cur = v
return n_ord, tot / max(n_ord, 1), yrs
print(f"\n {'configurazione':<26}" + "".join(f"{('$'+str(int(c))):>30}" for c in CAP_LADDER))
print(f" {'':<26}" + "".join(f"{'ord/a ticket turnover/a':>30}" for _ in CAP_LADDER))
for cand, w in ((None, 0.0), ("STATARB", 0.15), ("DVOLSPREAD", 0.15)):
lab = "2 sleeve (com'e' oggi)" if cand is None else f"+ {cand} @15%"
line = f" {lab:<26}"
for C in CAP_LADDER:
n, tk, yrs = net_orders(cand, w, C)
line += f"{('%.0f $%.0f $%.0f' % (n / yrs, tk, n * tk / yrs)):>30}"
print(line)
print("\n 📌 Un terzo sleeve che netta NON paga un min-order proprio: cambia il NUMERO di")
print(" ordini del libro, non il loro pavimento. Il conteggio ordini RADDOPPIA, il")
print(" TURNOVER (cioe' la fee) sale molto meno — e' il segno che il pavimento sta")
print(" tagliando ribilanci piccoli, non che il libro trada di piu' in nozionale.")
print(" ✅ E c'e' un vantaggio strutturale mai scritto: le fee modellate negli sleeve sono")
print(" un LIMITE SUPERIORE. Ogni sleeve paga sul PROPRIO |Δpos|, ma il conto paga sul")
print(" |Δ(somma)|, e per disuguaglianza triangolare |Δ(Σ w·p)| <= Σ w·|Δp|.")
print(" ⚠️ Il rovescio, misurato in sezione 2: il candidato non puo' avere un proprio stop,")
print(" e i suoi ribilanci si CANCELLANO contro quelli di TP01/SKH01 quando discordano.")
# =============================================================================================
# 8) LA SCALA
# =============================================================================================
def sezione8(S: dict, M: dict, G: dict, V: dict, cens: dict) -> None:
sez("8) LA SCALA — a quale capitale entra il primo, chi e', e per quale muro")
global PASSED_TXT
byc: dict = {}
for (c, w), r in G.items():
byc.setdefault(c, []).append((w, r["gate_pass"]))
PASSED_TXT = " · ".join(f"{c}: {sum(1 for _, g in v if g)}/{len(v)}"
for c, v in byc.items())
lot_e = min(V["opt"].get(("USDC", "ETH"), {0.1})) * ((V["perp"].get("ETH") or {}).get("mark") or 0)
lot_b = min(V["opt"].get(("USDC", "BTC"), {0.01})) * ((V["perp"].get("BTC") or {}).get("mark") or 0)
W = 0.15
print(f" `weights_tilt_null` superato (pesi su 4): {PASSED_TXT}\n")
sub("8a) IL MURO DI CIASCUNO — quale vincolo morde per primo, e se e' una soglia di CAPITALE")
muri = [
("XS01 (19 gambe)", "VENUE",
"5 gambe su 19 non esistono su Deribit; su Hyperliquid serve un SECONDO CONTO,",
"e la decisione d'operatore del 26/07 tiene tutto su Deribit fino a $20.000.",
"NON e' una soglia di eseguibilita' (falsificata il 22/08): e' la decisione di venue."),
(f"XS01-D ({len(cens['xs_stable'])} gambe)", "AMPIEZZA + SFORTUNA DI LISTINO",
f"con {len(cens['xs_stable'])} gambe il long-5/short-5 tiene 10 posizioni su "
f"{len(cens['xs_stable'])}: gia' il sottoinsieme MEDIANO",
"da 11 crolla (sez. 4c), e quello che Deribit quota sta al 9° pctl di quella banda.",
f"dShFULL {np.median(M['out']['XS01-D'][W]['sh']):+.3f} a peso 15% "
f"(negativo a {100-100*(np.array(M['out']['XS01-D'][W]['sh'])>0).mean():.0f}% delle ancore).",
"Nessun capitale lo ripara, e nemmeno 2-3 listing in piu': serve l'universo pieno."),
("XS01-D14", "DATO",
"3 delle 14 gambe sono quotate da 2-8 GIORNI e una (APT) non ha nemmeno una quota;",
"e `marginal_vs_tp01` la classifica HEDGE, non ADDS.",
"Riapribile fra ~1 anno di quotazione, se il volume arriva. Non e' una soglia di $."),
("STATARB-RESID", "GATE PRE-REGISTRATO 2026-09-27",
f"eseguibile e netta gia' a $635; dShFULL {np.median(M['out']['STATARB'][W]['sh']):+.3f} "
f"a 15%, ma d drift {100*np.median(M['out']['STATARB'][W]['cagr']):+.2f}pp",
"(tutto rischio in meno, zero ritorno in piu' a nozionale invariato) e",
"`marginal_vs_tp01` = NEUTRAL, robust_oos FALSE. Il gate scade fra 35 giorni."),
("DVOLSPREAD", "GATE PRE-REGISTRATO 2026-10-24 / 2027-01-24",
f"eseguibile e netta a $635; dShFULL {np.median(M['out']['DVOLSPREAD'][W]['sh']):+.3f}, ma",
f"d drift {100*np.median(M['out']['DVOLSPREAD'][W]['cagr']):+.2f}pp e d maxDD che PEGGIORA "
"sopra il 15%.",
"robust_oos FALSE. Il gate ha una data e non si anticipa."),
("PREVDAY", "NESSUN GATE — ed e' il problema",
f"e' il piu' forte misurato (dShFULL {np.median(M['out']['PREVDAY'][W]['sh']):+.3f}, "
f"dShHOLD {np.median(M['out']['PREVDAY'][W]['shh']):+.3f}, d drift "
f"{100*np.median(M['out']['PREVDAY'][W]['cagr']):+.2f}pp),",
"eseguibile, netta, ADDS, robust_oos TRUE — e sta in forward-monitor dal giugno",
"senza gate pre-registrato, senza deflated-Sharpe, e con corr al libro 0.150 (la piu' alta)."),
("VRP01", "REGOLA + PREZZO",
f"il lotto NON e' piu' il muro (ETH ${lot_e:,.0f} = peso 12% da ~${lot_e/0.12:,.0f}),",
"ma restano 'niente short-vol da modello in deploy' e f=0,73 misurato sulle quote vere;",
"e non netta: serve una riga in `_CONTRACT` = codice su un percorso con soldi veri."),
("GTAA01", "PRIIPs",
"l'ordine e' RIFIUTATO dal broker retail UE. La via UCITS esiste (27/07) ma e' un",
"SECONDO VENUE, quindi cade anch'essa sotto la decisione dei $20.000.",
"Non e' una soglia di capitale: e' un divieto."),
]
for nm, muro, *righe in muri:
print(f"\n {nm} -> MURO: {muro}")
for r in righe:
print(f" {r}")
sub("8b) LA SCALA IN DOLLARI — cosa cambia, e quando")
righe = [
(635, "OGGI", "STATARB · DVOLSPREAD · PREVDAY eseguibili e nettanti (ticket $2,8-4,9,",
"assorbiti nel netto: 7d). XS01-D eseguibile ma PEGGIORA. Nessuno ammissibile."),
(2024, f"1 lotto ETH di VRP01 = peso 12% (${lot_e:,.0f}/0,12)",
"cade il muro del lotto per la gamba ETH. NON cade la regola short-vol.", ""),
(3000, "GTAA_MIN_CAPITAL",
"GTAA01 avrebbe il capitale ma resta PRIIPs-bloccato (verificato sul conto reale).", ""),
(6440, f"1 lotto BTC di VRP01 = peso 12% (${lot_b:,.0f}/0,12)",
"VRP01 tornerebbe 50/50 BTC+ETH come lo sleeve modellato.", ""),
(20000, "DECISIONE DI VENUE (operatore, 26/07)",
"si riapre lo split. E' QUI che XS01 (Hyperliquid, 19 gambe) diventa possibile —",
"per la decisione di venue, NON per una soglia di eseguibilita'."),
]
for C, evento, *righe2 in righe:
print(f"\n ${C:,}".replace(",", ".") + f" — {evento}")
for r in righe2:
if r:
print(f" {r}")
print("\n 📌 LA RISPOSTA ALLA DOMANDA 'a quale capitale cambia': NON c'e' una soglia di")
print(" capitale che ammette un terzo sleeve. Le due date che contano sono un GATE")
print(" (27/09, fra 35 giorni, gratis) e una DECISIONE DI VENUE ($20k). Il capitale")
print(" entra solo per VRP01, che e' fermo per una regola che il capitale non tocca.")
def sezione9(S: dict, M: dict, G: dict, cens: dict) -> None:
sez("9) VERDETTO — l'attesa a priori contro l'esito")
W = 0.15
print(f" Tabella decidibile, peso {W:.0%}, mediana delle differenze APPAIATE sulla banda")
print(" d'ancora congiunta, funding dentro:\n")
print(f" {'candidato':<12}{'dShFULL':>10}{'>0':>6}{'dShHOLD':>10}{'d drift':>10} "
f"{'eseguibile a $635':<24}{'gate':<12}{'tilt-null'}")
for c in CANDIDATI:
r = M["out"][c][W]
ex = {"XS01": "NO (5 gambe assenti)", "XS01-D": "SI (11 gambe)",
"XS01-D14": "solo 3gg di listino", "STATARB": "SI (netta)",
"DVOLSPREAD": "SI (netta)", "PREVDAY": "SI (netta)",
"VRP01": f"NO (1 lotto=38% conto)"}[c]
gt = {"XS01": "—", "XS01-D": "—", "XS01-D14": "—", "STATARB": "27/09",
"DVOLSPREAD": "24/10", "PREVDAY": "—", "VRP01": "regola"}[c]
gp = "PASS" if G.get((c, W), {}).get("gate_pass") else "FAIL"
print(f" {c:<12}{np.median(r['sh']):>+10.4f}{100*(np.array(r['sh'])>0).mean():>5.0f}%"
f"{np.median(r['shh']):>+10.4f}{100*np.median(r['cagr']):>+8.2f}pp "
f"{ex:<24}{gt:<12}{gp}")
dsa = float(np.median(M["out"]["STATARB"][W]["sh"]))
ddv = float(np.median(M["out"]["DVOLSPREAD"][W]["sh"]))
dxs = float(np.median(M["out"]["XS01"][W]["sh"]))
dxd = float(np.median(M["out"]["XS01-D"][W]["sh"]))
xs_gate = G.get(("XS01", W), {}).get("gate_pass")
print("\n ATTESA A PRIORI vs ESITO (dichiarata in sezione 0, prima di misurare):")
print(" (A1) 'nessuno deployabile oggi, e il muro NON e' il lotto del venue'")
print(" -> ✅ CONFERMATA. Nessun candidato muore sul lotto del venue (i lotti stanno")
print(" fra $0,01 e $7,72). Il muro e' altrove, ed e' diverso per ciascuno.")
print(f" (A2) 'XS01 il piu' forte ma tilt-null FALLIRA'' -> ⚠️ META' GIUSTA: XS01 e' fra i")
print(f" piu' forti (dShFULL {dxs:+.3f}) ma `weights_tilt_null` "
f"{'PASSA' if xs_gate else 'FALLISCE'}. Il gate non e'")
print(" il muro di XS01: il muro e' che 5 delle 19 gambe Deribit non le quota.")
print(f" (A3) '|dSharpe| < 0,10 per STATARB e DVOLSPREAD' -> ❌ REFUTATA su STATARB")
print(f" ({dsa:+.3f} a peso 15%, positivo a 100% delle ancore), confermata su")
print(f" DVOLSPREAD ({ddv:+.3f}). E il segno NON e' incerto: e' stabile sulla banda.")
print(" (A4) 'il primo a entrare sara' XS01 fra $2k e $5k, per ragione di venue'")
print(f" -> ❌ REFUTATA nella sostanza: la versione di XS01 eseguibile su Deribit oggi")
print(f" ({len(cens['xs_stable'])} gambe) fa dShFULL {dxd:+.3f} — PEGGIORA il libro. Non e' una")
print(" soglia di capitale: e' che l'universo eseguibile non regge il meccanismo")
print(" (sez. 4c: la maggior parte e' AMPIEZZA, il resto e' QUALI gambe mancano).")
print(" (A5) 'mi sorprendera' un candidato dato per eseguibile che non lo e''")
print(" -> ✅ CONFERMATA, ma al ROVESCIO di come me l'aspettavo: la sorpresa e' un")
print(" candidato dato per NON eseguibile che lo E' (VRP01: il lotto vero e' 7,5x")
print(" piu' piccolo del pubblicato), e uno dato per eseguibile-a-$20k che a")
print(" $20k resta comunque rotto (XS01: e\' il VENUE, non la taglia).")
print("\n ⚠️ CAVEAT CHE VALE PER TUTTA LA TABELLA — questo e' uno SCREEN, non una scoperta:")
print(" 7 candidati x 4 pesi = 28 configurazioni guardate sullo stesso libro e sullo")
print(" stesso hold-out. Nessuno di questi delta e' passato per un deflated-Sharpe DI")
print(" SCREEN (lezione 22/08 §10: non basta contare i trial, serve la VARIANZA di")
print(" screen), e il candidato che esce primo esce da una selezione che io ho fatto.")
print(" E' esattamente per questo che l'output e' una TABELLA e non una raccomandazione:")
print(" il modo giusto di usarla e' scegliere UN candidato per una ragione dichiarata")
print(" PRIMA, e poi fargli un gate pre-registrato — non prendere il massimo di 28.")
# =============================================================================================
def main() -> None:
global _ARGS
ap = argparse.ArgumentParser()
ap.add_argument("--no-net", action="store_true")
ap.add_argument("--draws", type=int, default=1000)
ap.add_argument("--force", action="store_true")
_ARGS = ap.parse_args()
sys.argv = [sys.argv[0]] + (["--no-net"] if _ARGS.no_net else [])
sez("r0823_book_3rd.py — §50 BOOK-3RD | quale terzo sleeve per il LIBRO LIVE?")
print(f" avvio {dt.datetime.now(dt.timezone.utc):%Y-%m-%d %H:%M UTC} · "
f"capitale reale assunto ${CAPITAL_TODAY:.0f} · estrazioni {_ARGS.draws}")
sez("0) ATTESA A PRIORI (dichiarata PRIMA di misurare)")
print(ATTESA)
V = venue_census()
cens = sezione1(V)
sezione1c(V, cens)
sezione2()
print(f"\n {clock()} carico i tassi di funding (cache/rete)...")
rate = get_rate()
R3 = sezione3(rate)
print(f"\n {clock()} costruisco le serie ancorate...")
S = build_series(rate, cens, force=_ARGS.force)
sezione4(S, cens)
sezione4b(S)
sezione4c(cens)
M = sezione5(S, _ARGS.draws)
sezione5b(S)
G = sezione6(S)
sezione7(S, V, cens)
sezione7d(rate)
sezione8(S, M, G, V, cens)
sezione9(S, M, G, cens)
sez(f"FINE {clock()}")
if __name__ == "__main__":
main()