research(wave-0822): XSR-REPRO trova un difetto di produzione (i monitor forward registrano ~41 min/giorno); SKEW scompone il f di VRP01 — il 42% e' struttura a termine

This commit is contained in:
Adriano Dal Pastro
2026-08-22 17:29:19 +00:00
parent f0122d97a1
commit ea22a0e20c
9 changed files with 1445 additions and 93 deletions
+530
View File
@@ -0,0 +1,530 @@
"""ALT-OPT — la famiglia opzioni USDC-LINEARE di Deribit, mai guardata dal progetto (2026-08-22).
CONTESTO. Il filone VRP-QUOTE-VERE di questa stessa ondata ha trovato che `collect_chain.py`
interroga `{"currency": "BTC"|"ETH"}`, che su Deribit restituisce le sole opzioni INVERSE
(regolate in BTC/ETH). Il conto e' in USDC. Esiste accanto una famiglia USDC-LINEARE con lotti
~10x piu' piccoli, e su alcuni sottostanti (SOL, XRP, HYPE, AVAX) e' molto piu' popolata di
BTC_USDC. Il progetto non l'ha mai misurata.
DOMANDA. Esiste su questa famiglia una struttura a RISCHIO DEFINITO (put credit spread, la stessa
geometria di VRP01: vendi ~-0.28, compri ~-0.10) eseguibile con qualche centinaio di dollari,
con spread e liquidita' che non se la mangino?
COSA MISURA, in ordine, e si FERMA appena un vincolo la uccide:
1. i fatti del venue letti dall'API pubblica (min_trade_amount, tick_size, contract_size,
costo in $ di UN lotto), e la LIQUIDITA' vera: spread relativo (ask-bid)/mid ai delta che
servono davvero, non sull'ATM, + profondita' in cima al book;
2. il collaterale: margine della gamba corta secondo la formula pubblicata dal venue, e
max-loss della struttura (i due numeri differiscono per un fatto — se il venue netta le
gambe — che l'API PUBBLICA NON dice: e' dichiarato come tale, non indovinato);
3. quanta storia c'e' (risposta: nessuna) e quindi cosa si potrebbe al massimo concludere.
COSA NON PUO' FARE, e va detto prima dei numeri: **non esiste un backtest possibile**. Il nostro
collettore raccoglie solo le inverse BTC/ETH e l'archivio ereditato pure. Questo file misura le
condizioni di UN ISTANTE. Il massimo verdetto ottenibile e' `LEAD` = "vale la pena raccogliere il
dato", con il costo di raccolta stimato in chiamate/ora sul giro esistente.
VINCOLI DI PROGETTO CITATI, non aggirati:
(a) "niente short-vol da modello in deploy" (19/06). Qui i prezzi sono VERI, non modellati —
che e' un'altra cosa — ma la regola resta finche' non esiste un campione che contenga uno
STRESS. Un istante di calma non e' un campione.
(b) SOL e' escluso dall'universo DIREZIONALE per decisione dell'operatore del 22/08. Quella
decisione riguarda TP01/SKH01 congelati come terza gamba di trend, e il suo stesso testo
dichiara che un meccanismo diverso la riapre. Un put credit spread non e' direzionale nel
senso di quella decisione. Ma il guardrail sul DATO vale lo stesso: nessun file nuovo in
`data/raw/`, e i prezzi del sottostante qui vengono da `underlying_price` della catena,
mai da `load_data`.
(c) il gate IV-rank>0.30 e' l'unico alpha misurato di VRP01, e su questi sottostanti NON e'
calcolabile: non abbiamo un giorno di storia di vol implicita. Senza quel gate la struttura
non e' VRP01, e' vendere vol a caso.
CONVENZIONE DI UNITA' (la trappola di questo file): le opzioni INVERSE si quotano in CRIPTO per
contratto, le USDC-LINEARI in USDC per unita' di sottostante. Confrontare i due premi senza
convertire darebbe numeri sbagliati di un fattore ~10^4-10^5. Qui ogni importo in dollari passa
per `prem_usd()`, e le colonne unit-free (spread relativo, costo/credito) sono le uniche
direttamente confrontabili fra le due famiglie.
RETE: sole letture pubbliche, nessun ordine, pacing a ~2 richieste/s, e il file SI RIFIUTA di
partire nella finestra :24-:29 (e' quando `cron_chain` usa lo stesso IP: il rate limit Deribit e'
per-IP e il progetto ha gia' avuto un guasto per questo, 29/07).
nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py
nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py --cache # riusa
"""
from __future__ import annotations
import argparse
import json
import math
import time
from datetime import UTC, datetime
from pathlib import Path
import numpy as np
import requests
API = "https://www.deribit.com/api/v2/public"
TIMEOUT = 20
RPS = 2.0
CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/altopt_cache.json")
SHORT_DELTA, LONG_DELTA = -0.28, -0.10 # la geometria di VRP01, non ri-scelta qui
DTE_LO, DTE_HI = 4.0, 45.0 # dal settimanale di VRP01 (4-10) fino a ~1.5 mesi
OI_MIN = 100.0 # la stessa soglia di collect_chain.py
CAPITAL = 635.0 # il conto vero
TAKER_CAP = 0.125 # cap fee opzioni Deribit: 12.5% del premio
LINEAR = ("SOL_USDC", "XRP_USDC", "ETH_USDC", "HYPE_USDC", "AVAX_USDC", "BTC_USDC")
INVERSE = ("BTC", "ETH") # termine di paragone: il progetto le conosce
# ---------------------------------------------------------------- rete (parca, sola lettura)
class Budget:
def __init__(self, rps: float = RPS) -> None:
self.rps, self._next, self.calls, self.err, self.r429 = rps, 0.0, 0, 0, 0
def wait(self) -> None:
now = time.monotonic()
if now < self._next:
time.sleep(self._next - now)
self._next = max(now, self._next) + 1.0 / self.rps
def guardia_finestra() -> None:
m = datetime.now(UTC).minute
if 24 <= m <= 29:
raise SystemExit(f"minuto {m}: finestra di cron_chain (:25-:29). Rilancia dopo il :30.")
def get(path: str, params: dict, b: Budget, tries: int = 3):
for k in range(tries):
b.wait()
b.calls += 1
try:
r = requests.get(f"{API}/{path}", params=params, timeout=TIMEOUT)
except Exception:
b.err += 1
time.sleep(1.5 * (k + 1))
continue
if r.status_code == 429:
b.r429 += 1
time.sleep(2.0 * (k + 1))
continue
if r.status_code != 200:
b.err += 1
time.sleep(1.0 * (k + 1))
continue
try:
return r.json()["result"]
except Exception:
b.err += 1
return None
return None
def fetch_all(b: Budget) -> dict:
"""6 chiamate: instruments + book_summary per USDC, BTC, ETH. Niente per-strumento."""
out = {}
for cur in ("USDC", "BTC", "ETH"):
out[f"inst_{cur}"] = get("get_instruments",
{"currency": cur, "kind": "option", "expired": "false"}, b) or []
out[f"summ_{cur}"] = get("get_book_summary_by_currency",
{"currency": cur, "kind": "option"}, b) or []
if not out["inst_USDC"]: # il venue potrebbe non accettare currency=USDC
out["inst_USDC"] = get("get_instruments",
{"currency": "any", "kind": "option", "expired": "false"}, b) or []
out["summ_USDC"] = get("get_book_summary_by_currency",
{"currency": "any", "kind": "option"}, b) or []
return out
# ---------------------------------------------------------------- Black-Scholes (r=0, come il sleeve)
def put_delta(S: float, K: float, T: float, sig: float) -> float:
if not (S > 0 and K > 0 and T > 0 and sig > 0):
return float("nan")
d1 = (math.log(S / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T))
return 0.5 * (1.0 + math.erf(d1 / math.sqrt(2.0))) - 1.0
# ---------------------------------------------------------------- unita'
def is_inverse(fam: str) -> bool:
return fam in INVERSE
def prem_usd(price: float, r: dict) -> float:
"""Premio in DOLLARI di `min_trade_amount` contratti quotati a `price`.
Lineare: il prezzo e' gia' in USDC per unita' di sottostante.
Inverse: il prezzo e' in CRIPTO per contratto -> si moltiplica per il sottostante.
"""
k = r["csize"] * r["minamt"]
return price * k * (r["S"] if is_inverse(r["fam"]) else 1.0)
def loss_usd(Ks: float, Kl: float, r: dict) -> float:
"""Perdita massima strutturale in dollari, per `min_trade_amount` contratti.
Lineare: esatta, (Ks-Kl) x quantita'. Inverse: il payoff e' in cripta (max(0,K-S)/S), il
peggiore e' a S=Kl e vale (Ks-Kl)/Kl cripto = (Ks-Kl) dollari a quel prezzo -> stessa
formula, ma APPROSSIMATA (il valore in dollari della perdita dipende da dove finisce S).
"""
return (Ks - Kl) * r["csize"] * r["minamt"]
# ---------------------------------------------------------------- margine (FONTE SECONDARIA)
def im_short_put_usd(S: float, K: float, mark_usd: float, qty: float) -> float:
"""Initial margin di una short put, in USDC, secondo la formula PUBBLICATA da Deribit.
⚠️ FONTE SECONDARIA, dichiarata: l'API PUBBLICA non espone il margine. Il numero che decide
davvero — se Deribit NETTA le due gambe di uno spread in margine standard — non e' leggibile
da qui: si conferma con `private/get_margins`, che richiede una chiave (lettura, ma non
pubblica) e quindi NON e' stato usato.
"""
if not (S > 0 and K > 0):
return float("nan")
otm = max(0.0, S - K) / S # una put e' OTM quando K < S
per_contratto = max(max(0.15 - otm, 0.10) * S, 0.075 * S) + max(0.0, mark_usd / max(qty, 1e-12))
return per_contratto * qty
# ---------------------------------------------------------------- costruzione della vista
def build(raw: dict) -> list[dict]:
inst, summ = {}, {}
for cur in ("USDC", "BTC", "ETH"):
for i in raw.get(f"inst_{cur}") or []:
inst[i["instrument_name"]] = i
for s in raw.get(f"summ_{cur}") or []:
summ[s["instrument_name"]] = s
rows = []
now_ms = datetime.now(UTC).timestamp() * 1000.0
for name, i in inst.items():
s = summ.get(name)
if s is None:
continue
fam = name.split("-")[0]
dte = (float(i.get("expiration_timestamp") or 0) - now_ms) / 86400_000.0
bid, ask = s.get("bid_price"), s.get("ask_price")
bid = float(bid) if bid else None # Deribit manda 0/None per "nessun lato"
ask = float(ask) if ask else None
iv, S = s.get("mark_iv"), s.get("underlying_price")
K = float(i.get("strike") or 0)
typ = "P" if i.get("option_type") == "put" else "C"
T = max(dte, 0.0) / 365.25
d = (put_delta(float(S), K, T, float(iv) / 100.0)
if (S and iv and K and T > 0 and typ == "P") else float("nan"))
rows.append(dict(
name=name, fam=fam, typ=typ, K=K, dte=dte, S=float(S) if S else float("nan"),
iv=float(iv) if iv else float("nan"), delta_bs=d,
bid=bid, ask=ask, mid=((bid + ask) / 2 if (bid and ask) else None),
mark=float(s.get("mark_price") or 0.0),
oi=float(s.get("open_interest") or 0.0), vol=float(s.get("volume") or 0.0),
tick=float(i.get("tick_size") or 0.0), steps=i.get("tick_size_steps") or [],
minamt=float(i.get("min_trade_amount") or 0.0),
csize=float(i.get("contract_size") or 0.0),
taker=i.get("taker_commission"), settle=i.get("settlement_currency"),
))
return rows
def rel_spread(r: dict) -> float:
if r["bid"] is None or r["ask"] is None or not r["mid"]:
return float("nan")
return (r["ask"] - r["bid"]) / r["mid"]
# ---------------------------------------------------------------- 1. i fatti del venue
def famiglie(rows: list[dict]) -> dict:
print("\n" + "=" * 104)
print("1a. I FATTI DEL VENUE — public/get_instruments + get_book_summary_by_currency")
print("=" * 104)
print(f"{'famiglia':<10} {'strum':>6} {'OI>=100':>8} {'min_amt':>9} {'c_size':>7} "
f"{'tick':>9} {'sottost.$':>11} {'$/lotto':>10} {'taker':>8} {'settle':>7}")
tab = {}
for f in sorted({r["fam"] for r in rows}):
sub = [r for r in rows if r["fam"] == f]
liq = [r for r in sub if r["oi"] >= OI_MIN]
S = float(np.nanmedian([r["S"] for r in sub]))
mins = {r["minamt"] for r in sub}
cs = {r["csize"] for r in sub}
a = sub[0]
lot = a["minamt"] * a["csize"] * S
tab[f] = dict(n=len(sub), nliq=len(liq), minamt=a["minamt"], csize=a["csize"],
tick=a["tick"], S=S, lot=lot, taker=a["taker"])
if f in LINEAR or f in INVERSE:
avv = "" if (len(mins) == 1 and len(cs) == 1) else " <- min/c_size NON uniformi"
print(f"{f:<10} {len(sub):>6} {len(liq):>8} {a['minamt']:>9.4g} {a['csize']:>7.4g} "
f"{a['tick']:>9.5g} {S:>11.2f} {lot:>10.2f} {str(a['taker']):>8} "
f"{str(a['settle']):>7}{avv}")
print("\n `$/lotto` = min_trade_amount x contract_size x sottostante = il NOZIONALE di un")
print(" lotto (NON il premio, che e' molto minore). E' la granularita' minima imposta dal venue.")
print(" `taker` per le opzioni Deribit e' una frazione del SOTTOSTANTE con cap al 12.5% del")
print(" premio: su un'ala molto OTM morde il CAP, non la percentuale.")
return tab
def replica(tab: dict) -> None:
print("\n" + "-" * 104)
print("1b. CONTROLLO POSITIVO — riproduco i conteggi dichiarati dal coordinatore (22/08)?")
print("-" * 104)
atteso = {"BTC": (1038, 415), "ETH": (932, 548), "BTC_USDC": (686, 5), "ETH_USDC": (660, 119),
"SOL_USDC": (574, 341), "XRP_USDC": (522, 250), "HYPE_USDC": (396, 172),
"AVAX_USDC": (294, 146), "TRX_USDC": (310, 115)}
print(f"{'famiglia':<10} {'strum att':>10} {'strum oggi':>11} {'liq att':>8} {'liq oggi':>9}")
for f, (n_a, l_a) in atteso.items():
t = tab.get(f)
if t is None:
print(f"{f:<10} {n_a:>10} {'ASSENTE':>11} {l_a:>8} {'-':>9}")
else:
print(f"{f:<10} {n_a:>10} {t['n']:>11} {l_a:>8} {t['nliq']:>9}")
print(" La catena si muove di ora in ora (scadenze che nascono, OI che cambia): scarti di")
print(" poche unita' sono la misura, non un disaccordo. Uno scarto GRANDE sarebbe un problema.")
# ---------------------------------------------------------------- 2. liquidita' ai delta utili
def gambe(rows: list[dict], fam: str) -> list[dict]:
puts = [r for r in rows if r["fam"] == fam and r["typ"] == "P"
and DTE_LO <= r["dte"] <= DTE_HI and np.isfinite(r["delta_bs"])]
out = []
for e in sorted({round(r["dte"], 4) for r in puts}):
g = [r for r in puts if round(r["dte"], 4) == e]
s = min(g, key=lambda r: abs(r["delta_bs"] - SHORT_DELTA))
lo = min(g, key=lambda r: abs(r["delta_bs"] - LONG_DELTA))
if s["K"] > lo["K"]:
out.append({"dte": e, "short": s, "long": lo, "n_strike": len(g)})
return out
def liquidita(rows: list[dict]) -> dict:
print("\n" + "=" * 104)
print(f"2. LA LIQUIDITA' AI DELTA CHE SERVONO ({SHORT_DELTA} corta / {LONG_DELTA} lunga),")
print(f" NON sull'ATM. Finestra {DTE_LO:.0f}-{DTE_HI:.0f} giorni. Delta = BS su mark_iv, r=0.")
print("=" * 104)
print(f"{'famiglia':<10} {'scad':>5} {'2lati':>6} {'d corta':>8} {'d lunga':>8} "
f"{'spr% corta':>11} {'spr% lunga':>11} {'credito$':>9} {'costo/cred':>11} "
f"{'fee/cred':>9} {'maxloss$':>9}")
res = {}
for fam in list(LINEAR) + list(INVERSE):
gg = gambe(rows, fam)
if not gg:
print(f"{fam:<10} {'0':>5} nessuna scadenza utilizzabile nella finestra")
continue
ss, ll, ds, dl, cred, cost, fee, ml = [], [], [], [], [], [], [], []
due = neg = 0
for g in gg:
s, lo = g["short"], g["long"]
rs, rl = rel_spread(s), rel_spread(lo)
if not (np.isfinite(rs) and np.isfinite(rl)):
continue
due += 1
c_mid = prem_usd(s["mid"], s) - prem_usd(lo["mid"], lo)
c_exe = prem_usd(s["bid"], s) - prem_usd(lo["ask"], lo)
if c_mid <= 0:
neg += 1
continue
# fee taker Deribit: min(0.03% del sottostante, 12.5% del premio) per gamba, x2 gambe,
# x2 (apertura + chiusura/esercizio). Conservativo ma e' il listino.
def _fee(r: dict) -> float:
notion = r["S"] * r["csize"] * r["minamt"]
return min(float(r["taker"] or 0.0003) * notion, TAKER_CAP * prem_usd(r["mid"], r))
ss.append(rs); ll.append(rl)
ds.append(s["delta_bs"]); dl.append(lo["delta_bs"])
cred.append(c_mid)
cost.append((c_mid - c_exe) / c_mid)
fee.append(2.0 * (_fee(s) + _fee(lo)) / c_mid)
ml.append(loss_usd(s["K"], lo["K"], s) - c_exe)
if not cred:
print(f"{fam:<10} {len(gg):>5} {due:>6} nessuna struttura con credito positivo a mid "
f"({neg} a credito<=0)")
continue
r = dict(n=len(gg), due=due, neg=neg, s=float(np.median(ss)), l=float(np.median(ll)),
ds=float(np.median(ds)), dl=float(np.median(dl)), cred=float(np.median(cred)),
cost=float(np.median(cost)), fee=float(np.median(fee)), ml=float(np.median(ml)))
res[fam] = r
print(f"{fam:<10} {r['n']:>5} {due:>6} {r['ds']:>8.3f} {r['dl']:>8.3f} "
f"{r['s']*100:>10.1f}% {r['l']*100:>10.1f}% {r['cred']:>9.2f} "
f"{r['cost']*100:>10.1f}% {r['fee']*100:>8.1f}% {r['ml']:>9.2f}")
print("\n `2lati` = strutture in cui ENTRAMBE le gambe hanno bid E ask. Una riga presente non")
print(" e' un dato presente: una gamba con un solo lato non e' negoziabile a un prezzo noto.")
print(" `d corta`/`d lunga` = delta REALIZZATI (non i target): se la griglia degli strike e'")
print(" grossolana la struttura che compreresti non e' quella che hai chiesto.")
print(" `costo/cred` = quanto dell'incasso a mid resta sul tavolo attraversando lo spread su")
print(" ENTRAMBE le gambe. Riferimento del progetto su BTC/ETH inverse: ~10% del credito.")
print(" `fee/cred` = listino taker Deribit, 2 gambe x apertura+chiusura, col cap 12.5%.")
print(" `credito$` e `maxloss$` sono per UN LOTTO MINIMO.")
return res
def profondita(rows: list[dict], fams: list[str], b: Budget) -> None:
print("\n" + "=" * 104)
print("2b. PROFONDITA' IN CIMA AL BOOK (public/get_order_book depth=5), sulle gambe scelte")
print("=" * 104)
print(f"{'famiglia':<10} {'gamba':<6} {'n':>3} {'lotti al best':>14} {'$ premio al best':>17} "
f"{'ask/tick lunga':>15} {'max|delta venue-BS|':>21}")
for fam in fams:
gg = gambe(rows, fam)[:4] # max 4 scadenze/famiglia: budget di rete
for lato, key in (("corta", "short"), ("lunga", "long")):
lots, prem, dd, tk = [], [], [], []
for g in gg:
r = g[key]
ob = get("get_order_book", {"instrument_name": r["name"], "depth": 5}, b)
if not ob:
continue
side = ob.get("bids") if key == "short" else ob.get("asks")
if not side:
continue
px, sz = float(side[0][0]), float(side[0][1])
lots.append(sz / r["minamt"] if r["minamt"] else float("nan"))
prem.append(prem_usd(px, r) * (sz / max(r["minamt"], 1e-12)))
if r["tick"]:
tk.append(px / r["tick"])
gv = (ob.get("greeks") or {}).get("delta")
if gv is not None and np.isfinite(r["delta_bs"]):
dd.append(abs(float(gv) - r["delta_bs"]))
if not lots:
print(f"{fam:<10} {lato:<6} {'0':>3} book vuoto su tutte le scadenze provate")
continue
print(f"{fam:<10} {lato:<6} {len(lots):>3} {np.median(lots):>14.1f} "
f"{np.median(prem):>17.2f} "
f"{(np.median(tk) if tk else float('nan')):>15.1f} "
f"{(max(dd) if dd else float('nan')):>21.4f}")
print("\n `lotti al best` = size in cima al book / lotto minimo: quante strutture minime")
print(" entrano senza muovere il prezzo. `ask/tick` sulla gamba LUNGA e' il controllo")
print(" d'artefatto del 30/07: un'ala a 1-2 tick e' un prezzo di griglia, non un prezzo.")
print(" `max|delta venue-BS|` valida il MIO calcolo del delta contro le greche del venue: se")
print(" non e' piccolo, tutta la selezione degli strike qui sopra e' sbagliata.")
# ---------------------------------------------------------------- 3. collaterale
def collaterale(rows: list[dict], res: dict) -> None:
print("\n" + "=" * 104)
print("3. IL COLLATERALE — quanto conto serve per UNA struttura minima (conto reale $635)")
print("=" * 104)
print(f"{'famiglia':<10} {'IM corta+ala $':>15} {'max-loss $':>11} {'IM/conto':>9} "
f"{'strutture @635 (IM)':>20} {'(max-loss)':>12}")
for fam in res:
gg = gambe(rows, fam)
ims, mls = [], []
for g in gg:
s, lo = g["short"], g["long"]
if s["mid"] is None or lo["ask"] is None:
continue
qty = s["csize"] * s["minamt"]
im = im_short_put_usd(s["S"], s["K"], prem_usd(s["mark"], s), qty)
ims.append(im + prem_usd(lo["ask"], lo)) # l'ala si PAGA: e' cassa, non margine
mls.append(loss_usd(s["K"], lo["K"], s))
if not ims:
continue
im, ml = float(np.median(ims)), float(np.median(mls))
print(f"{fam:<10} {im:>15.2f} {ml:>11.2f} {im/CAPITAL:>8.1%} "
f"{CAPITAL/im:>20.1f} {CAPITAL/ml:>12.1f}")
print("\n `IM corta+ala` = margine iniziale della sola put venduta (formula PUBBLICATA dal")
print(" venue, FONTE SECONDARIA) + il premio dell'ala, che si paga in contanti. E' il")
print(" fabbisogno se il venue NON netta le gambe.")
print(" `max-loss` = perdita massima strutturale = il fabbisogno se il venue LE NETTA.")
print(" ⚠️ La differenza fra i due numeri e' l'unica cosa importante di questa tabella e NON")
print(" E' LEGGIBILE dall'API pubblica: si chiude con `private/get_margins` (lettura, ma")
print(" autenticata -> fuori dal perimetro dichiarato di questo file).")
def costo_raccolta(tab: dict) -> None:
print("\n" + "=" * 104)
print("4. COSTO DI RACCOLTA — cosa costerebbe aggiungere una famiglia al giro esistente")
print("=" * 104)
print(" `collect_chain.py` oggi: ~650 chiamate in ~160s a 4 rps (BTC+ETH inverse), al :25.")
print(f"{'famiglia':<10} {'chiamate/giro':>14} {'+s a 4 rps':>11} {'+% sul giro':>12}")
for f in LINEAR:
t = tab.get(f)
if t:
n = t["nliq"] + 2 # + get_instruments + get_book_summary
print(f"{f:<10} {n:>14.0f} {n/4.0:>11.1f} {n/650*100:>11.0f}%")
print("\n ⚠️ Il vincolo non e' la CPU ma il rate limit Deribit PER-IP, gia' costato un guasto")
print(" il 29/07 (fallback silenzioso del feed 5m di SKH01). Ogni famiglia aggiunta allunga")
print(" la finestra in cui il giro occupa l'IP.")
# ---------------------------------------------------------------- main
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--cache", action="store_true", help="riusa lo snapshot su disco (niente rete)")
a = ap.parse_args()
print("=" * 104)
print("ALT-OPT — opzioni USDC-LINEARI su Deribit: c'e' una struttura eseguibile a $635?")
print("=" * 104)
print("Sola lettura pubblica, nessun ordine. UNO SNAPSHOT: qui non esiste un backtest.")
b = Budget()
if a.cache and CACHE.exists():
raw = json.loads(CACHE.read_text())
print(f"\n[cache] {CACHE}")
else:
guardia_finestra()
t0 = time.time()
raw = fetch_all(b)
CACHE.parent.mkdir(parents=True, exist_ok=True)
CACHE.write_text(json.dumps(raw))
print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429, "
f"{time.time()-t0:.0f}s")
rows = build(raw)
print(f"[dato] {len(rows)} strumenti con specifiche E quote — snapshot "
f"{datetime.now(UTC):%Y-%m-%d %H:%M} UTC")
if not rows:
print("NESSUN DATO: il venue non ha risposto. Verdetto: non misurabile.")
return 1
tab = famiglie(rows)
replica(tab)
res = liquidita(rows)
vive = [f for f in res if res[f]["due"] >= 2]
if a.cache:
print("\n2b. PROFONDITA': non girata (--cache non usa la rete).")
elif not vive:
print("\n2b. PROFONDITA': non girata — nessuna famiglia con >=2 strutture a due lati.")
else:
guardia_finestra()
profondita(rows, vive, b)
collaterale(rows, res)
costo_raccolta(tab)
print("\n" + "=" * 104)
print("5. COSA QUESTO FILE NON PUO' DIRE (dichiarato prima di qualunque verdetto)")
print("=" * 104)
print(" * Non c'e' STORIA: il collettore raccoglie BTC/ETH inverse, l'archivio ereditato pure.")
print(" Zero giorni di catena su SOL/XRP/HYPE/AVAX/*_USDC -> nessun backtest, nessun")
print(" hold-out, nessun deflated-Sharpe. Il massimo verdetto ottenibile e' LEAD.")
print(" * Non c'e' IV-RANK: il gate>0.30 e' l'unico alpha misurato di VRP01 e richiede la")
print(" distribuzione storica della vol implicita del sottostante. Senza, la struttura non")
print(" e' VRP01: e' vendere vol a caso. (Su BTC/ETH quel gate e' passato 0/19 settimane.)")
print(" * Non c'e' STRESS nel campione: la regola 'niente short-vol da modello in deploy' e'")
print(" del 19/06 e la condizione dichiarata per rivalutarla e' un crash CATTURATO.")
print(" * Il margine e' da FORMULA PUBBLICATA, non dall'API: il netting delle gambe non e'")
print(" leggibile pubblicamente.")
print(" * E' UN ISTANTE: uno spread relativo mediano misurato a un'ora del sabato non e' la")
print(" sua distribuzione. Servirebbe almeno un ciclo settimanale per sapere se e' tipico.")
print(f"[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+60 -46
View File
@@ -283,24 +283,29 @@ def certify(asset: str, wide: pd.DataFrame, vwide: pd.DataFrame, meta: pd.DataFr
if b in fb.index:
print(f" {b:9s} flat={fb.loc[b,'flat']*100:5.1f}% barre={int(fb.loc[b,'n']):,}")
# (b) volume orario mediano per bucket (il flat da solo non basta)
# (b) volume orario per bucket (il flat da solo non basta).
# ATTENZIONE: il `volume` di get_tradingview_chart_data e' in VALUTA BASE (BTC/ETH),
# non in USD -> va moltiplicato per il prezzo, o si sbaglia di ~5 ordini di grandezza.
vrows = []
for nm in vwide.columns:
exp = ok.loc[ok.contract == nm, "exp"]
if exp.empty:
continue
s = vwide[nm].dropna()
dte = (exp.iloc[0] - s.index).total_seconds() / 86400.0
v = vwide[nm].dropna()
px = wide[nm].reindex(v.index)
usd = (v * px).dropna() # <- conversione in USD
dte = (exp.iloc[0] - usd.index).total_seconds() / 86400.0
for lo, hi, lab in [(0, 30, "0-30g"), (30, 90, "30-90g"),
(90, 180, "90-180g"), (180, 400, ">180g")]:
m = (dte >= lo) & (dte < hi)
if m.sum() > 50:
vrows.append(dict(bucket=lab, med=float(s[m].median()), zero=float((s[m] == 0).mean())))
mm = (dte >= lo) & (dte < hi)
if mm.sum() > 50:
vrows.append(dict(bucket=lab, med=float(usd[mm].median()),
zero=float((usd[mm] == 0).mean())))
vb = pd.DataFrame(vrows).groupby("bucket").agg(med=("med", "median"), zero=("zero", "mean"))
print(" volume orario mediano (USD di nozionale) e quota ore a volume ZERO:")
print(" volume orario mediano in USD (volume base x prezzo) e quota ore a volume ZERO:")
for b in order:
if b in vb.index:
print(f" {b:9s} mediano=${vb.loc[b,'med']:>12,.0f} ore a vol 0 = {vb.loc[b,'zero']*100:5.1f}%")
print(f" {b:9s} mediano=${vb.loc[b,'med']:>12,.0f}/ora ore a vol 0 = {vb.loc[b,'zero']*100:5.1f}%")
# (c) accordo con l'INDICE alla scadenza: il datato DEVE convergere
conv = []
@@ -385,34 +390,36 @@ def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame,
ok = meta[meta.status == "ok"].sort_values("exp")
exps = {r.contract: r.exp for r in ok.itertuples()}
idx = wide.index
F = fund.set_index("ts").reindex(idx)
F = fund.set_index("ts").reindex(idx).ffill(limit=2)
n = len(idx)
front = np.full(n, "", dtype=object)
back = np.full(n, "", dtype=object)
contracts = list(wide.columns)
tau = {c: (exps[c] - idx).total_seconds() / 3600.0 for c in contracts if c in exps}
avail = {c: wide[c].notna().values for c in contracts}
# front = il primo (per scadenza) ancora vivo con dte >= roll_dte; back = il successivo
order = [c for c in ok.contract if c in wide.columns]
for i in range(n):
picked = []
for c in order:
if avail[c][i] and tau[c][i] >= roll_dte * 24:
picked.append(c)
if len(picked) == 2:
break
if len(picked) == 2:
front[i], back[i] = picked
order = [c for c in ok.contract if c in wide.columns] # ordine di SCADENZA
if len(order) < 2:
raise RuntimeError("meno di due contratti utilizzabili")
# ALIVE[i, k] = il k-esimo contratto (per scadenza) e' quotato a i E ha dte >= roll
TAU = np.column_stack([(exps[c] - idx).total_seconds().values / 3600.0 for c in order])
AV = np.column_stack([wide[c].notna().values for c in order])
ALIVE = AV & (TAU >= roll_dte * 24)
any1 = ALIVE.any(axis=1)
k1 = np.argmax(ALIVE, axis=1) # primo vivo = FRONT
A2 = ALIVE.copy()
A2[np.arange(n), k1] = False
any2 = A2.any(axis=1)
k2 = np.argmax(A2, axis=1) # secondo vivo = BACK
m = any1 & any2
front = np.where(m, np.array(order, dtype=object)[k1], "")
back = np.where(m, np.array(order, dtype=object)[k2], "")
rows = np.arange(n)
Wv = wide[order].values.astype(float)
Ff = np.where(m, Wv[rows, k1], np.nan)
Fb = np.where(m, Wv[rows, k2], np.nan)
tf = np.where(m, TAU[rows, k1], np.nan)
tb = np.where(m, TAU[rows, k2], np.nan)
out = pd.DataFrame(index=idx)
out["front"], out["back"] = front, back
m = (front != "") & (back != "")
Ff = np.full(n, np.nan); Fb = np.full(n, np.nan)
tf = np.full(n, np.nan); tb = np.full(n, np.nan)
W = wide.values
colpos = {c: k for k, c in enumerate(wide.columns)}
for i in np.where(m)[0]:
Ff[i] = W[i, colpos[front[i]]]; Fb[i] = W[i, colpos[back[i]]]
tf[i] = tau[front[i]][i]; tb[i] = tau[back[i]][i]
out["F_front"], out["F_back"] = Ff, Fb
out["tau_front_h"], out["tau_back_h"] = tf, tb
out["index"] = F["index"].values
@@ -514,7 +521,8 @@ def signal_w(cv: pd.DataFrame, family: str, sig: str, win_h: int, thr: float) ->
def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str,
win_h: int, thr: float, slip_bps: float,
fee_side: float = FEE_SIDE, fi=None, bi=None, lag: int = 1) -> pd.Series:
fee_side: float = FEE_SIDE, fi=None, bi=None, lag: int = 1,
dec_hour: int | None = None) -> pd.Series:
"""Ritorno ORARIO netto per $1 di nozionale LORDO PER GAMBA.
Le posizioni vivono in spazio CONTRATTO: quando la coppia cambia (roll) la variazione
@@ -529,6 +537,11 @@ def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str,
if fi is None or bi is None:
fi, bi = pair_idx(cv, ctx)
w = signal_w(cv, family, sig, win_h, thr)
if dec_hour is not None:
# ANCORA: la posizione si aggiorna SOLO a quell'ora del giorno e si TIENE il resto
# (non "si guadagna solo a quell'ora", che sarebbe un'altra strategia).
ws = pd.Series(w, index=idx).where(idx.hour == dec_hour)
w = ws.ffill().fillna(0.0).values
P = np.zeros((n, ncol))
rows = np.arange(n)
@@ -564,6 +577,12 @@ def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str,
# ==========================================================================
# 4. METRICHE / GATE
# ==========================================================================
def combine(hs: list[pd.Series]) -> pd.Series:
"""50/50 BTC+ETH sulle ore COMUNI (inner join, come tp01_baseline_daily)."""
J = pd.concat(hs, axis=1, join="inner").fillna(0.0)
return J.mean(axis=1)
def to_daily(h: pd.Series) -> pd.Series:
return ((1.0 + h.fillna(0.0)).resample("1D").prod() - 1.0).dropna()
@@ -698,9 +717,7 @@ def main() -> None:
h = run_strategy(cvcache[(a, rl)], ctxs[a], fam, sig, win, th, slip_est,
fi=f_i, bi=b_i)
hs.append(h)
J = pd.concat(hs, axis=1).fillna(0.0)
comb = J.mean(axis=1) # 50/50 BTC+ETH, come il resto del progetto
d = to_daily(comb)
d = to_daily(combine(hs))
st = stats(d)
rows.append(dict(family=fam, sig=sig, win=win, roll=rl, thr=th,
daily=d, **st))
@@ -768,13 +785,10 @@ def main() -> None:
hs = []
for a in ASSETS:
f_i, b_i = pidx[(a, int(b.roll))]
h = run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig,
int(b.win), float(b.thr), slip_est, fi=f_i, bi=b_i)
hh = h.copy()
mask = (hh.index.hour != off)
hh[mask] = 0.0 # decide/agisce solo a quell'ora
hs.append(hh)
return to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1))
hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig,
int(b.win), float(b.thr), slip_est,
fi=f_i, bi=b_i, dec_hour=off))
return to_daily(combine(hs))
try:
ab = A.anchor_luck_band(by_off, list(range(24)), canonical=0)
if "median" in ab:
@@ -807,7 +821,7 @@ def main() -> None:
f_i, b_i = pidx[(a, int(b.roll))]
hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig,
int(b.win), float(b.thr), slip_est, fi=f_i, bi=b_i, lag=lg))
sl_ = stats(to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1)))
sl_ = stats(to_daily(combine(hs)))
lag_tab.append((lg, sl_["sharpe"], sl_["cagr"]))
print(f" ritardo {lg:2d}h -> Sharpe {sl_['sharpe']:+6.2f} CAGR {sl_['cagr']*100:+6.2f}%")
s1 = lag_tab[0][1]; s2 = lag_tab[1][1]
@@ -827,7 +841,7 @@ def main() -> None:
f_i, b_i = pidx[(a, int(b.roll))]
hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family,
b.sig, int(b.win), float(b.thr), sl, fi=f_i, bi=b_i))
s2 = stats(to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1)))
s2 = stats(to_daily(combine(hs)))
mark = " <= stima di oggi" if abs(sl - slip_est) < 0.6 else ""
print(f" mezzo-spread {sl:5.1f} bps/lato -> Sharpe {s2['sharpe']:+6.2f} "
f"CAGR {s2['cagr']*100:+6.2f}%{mark}")
+73 -10
View File
@@ -54,6 +54,7 @@ FEE_LEG_MODELLO = 0.0005 # [D] 0.05%/gamba, config CONGELATA di XSR01
XSR_SOGLIA_HAIRCUT = 0.40 # [D] soglia pre-registrata del gate 2026-10-23
CAPITALI = (600.0, 1000.0, 1500.0, 2000.0, 3000.0, 4000.0, 5000.0, 7500.0,
10000.0, 20000.0, 50000.0)
CAPITALI_XS = (100.0, 200.0, 300.0, 600.0, 1000.0, 2000.0, 3000.0, 5000.0, 10000.0)
TICKETS = (50.0, 100.0, 300.0)
ANN = np.sqrt(365.0)
@@ -257,6 +258,7 @@ def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0:
eq = np.empty(n)
nets = np.zeros(n)
n_fill = n_skip = n_lot0 = 0
ntl_req = ntl_done = 0.0
fee = np.broadcast_to(np.asarray(fee_leg, float).ravel(), (k,)) if np.ndim(fee_leg) else None
for i in range(n):
ret = float(np.dot(w, R[i]))
@@ -275,6 +277,8 @@ def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0:
n_lot0 += int(zero.sum())
move = move & (lots >= 1)
need = np.abs(w_t - w) > 1e-12 # una gamba gia' a posto NON e' un ordine saltato
ntl_req += float((np.abs(w_t - w) * cap).sum())
ntl_done += float((np.abs(w_t - w) * cap)[move].sum())
w_new = np.where(move, w_t, w)
n_fill += int((move & need).sum())
n_skip += int(((~move) & need).sum())
@@ -290,7 +294,8 @@ def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0:
w = w_new
tot = n_fill + n_skip
return dict(net=nets, eq=eq, n_fill=n_fill, n_skip=n_skip, n_lot0=n_lot0,
pct_eseguite=(100.0 * n_fill / tot) if tot else np.nan)
pct_eseguite=(100.0 * n_fill / tot) if tot else np.nan,
pct_nozionale=(100.0 * ntl_done / ntl_req) if ntl_req > 0 else np.nan)
def metriche(net: np.ndarray, idx: pd.DatetimeIndex) -> dict:
@@ -393,6 +398,19 @@ def main() -> None:
print(f" deviazione |%| mediana {ck['dev_pct'].abs().median():.2f}% "
f"max {ck['dev_pct'].abs().max():.2f}% ({ck['dev_pct'].abs().idxmax()}) "
f"asset controllati {len(ck)}")
mv = [float(np.abs(pd.read_parquet(RAW / f"hl_{sy.lower()}_1d.parquet", columns=["close"])
["close"].pct_change().dropna()).median()) * 100 for sy in ck.index]
print(f" movimento |giornaliero| tipico di questi asset: {float(np.median(mv)):.2f}% -> "
f"una deviazione mediana del {ck['dev_pct'].abs().median():.2f}% su un feed vecchio "
f"{ck['eta_h'].median():.0f}h e' cio' che ci si attende (non e' un disaccordo di venue).")
try:
from src.data.downloader import load_data as _ld
bd = float(_ld("BTC", "1h")["close"].iloc[-1])
hl = float(meta.loc["BTC", "markPx"])
print(f" cross-check su un TERZO venue: BTC Deribit certificato ${bd:,.0f} vs "
f"HL API ${hl:,.0f} = {100*(hl-bd)/bd:+.2f}%")
except Exception as e:
print(f" (cross-check Deribit non disponibile: {type(e).__name__})")
print(" -> un feed TESTNET non supera questo controllo: i suoi prezzi sono scollegati.")
tk = fee_sched
@@ -497,8 +515,7 @@ def main() -> None:
f"{'>=$5 [D]':>10}{'>=$10 [C]':>11}{'reb>=$10':>10}")
R_xs = dret
idx = C.index
base = None
for cap in CAPITALI:
for cap in CAPITALI_XS:
pct5 = 100.0 * float((tk_nz * cap >= MIN_ORDER_ASSUNTO).mean())
pct10 = 100.0 * float((tk_nz * cap >= MIN_ORDER_HL).mean())
pr10 = 100.0 * float((reb * cap >= MIN_ORDER_HL).mean())
@@ -513,15 +530,16 @@ def main() -> None:
print(f" (`turn=|dW|` prima dello scaling): e' il motivo per cui sta sotto il "
f"{m_off['sharpe']:.2f} pubblicato.")
print(f" {'cap XS01':>10}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}"
f"{'haircut $10':>13}{'ordini fatti':>14}")
for cap in CAPITALI:
f"{'haircut $10':>13}{'eseguito':>21}")
for cap in CAPITALI_XS:
m0 = sim_libro(Wxs, R_xs, None, cap, None, None, None, fee_xs_mod)
m5 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_ASSUNTO, None, None, fee_xs_mod)
m10 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_HL, lot_xs, px_xs, fee_xs_mod)
a0, a5, a10 = (metriche(m["net"], idx) for m in (m0, m5, m10))
hc = (a0["sharpe"] - a10["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan
print(f" {cap:>10,.0f}{a0['sharpe']:>9.2f}{a5['sharpe']:>11.2f}{a10['sharpe']:>12.2f}"
f"{hc:>12.0f}%{m10['pct_eseguite']:>9.0f}%")
f"{hc:>12.0f}%{m10['pct_eseguite']:>8.0f}% ord "
f"{m10['pct_nozionale']:>4.0f}% ntl")
# ---------------------------------------------------------------- 4. XSR01
print(f"\n[4] XSR01 — soglia PUBBLICATA ~$5.000, gate 2026-10-23 con haircut a $5.000 <= 40%")
@@ -555,7 +573,7 @@ def main() -> None:
print(f" gambe {len(sx)} lordo mediano {np.median(np.abs(Wx).sum(axis=1)):.2f}x "
f"peso mosso mediano/gamba {np.median(tkx)*100:.3f}% p10 {np.percentile(tkx,10)*100:.3f}%")
print(f"\n {'cap':>9}{'ticket med':>12}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}"
f"{'haircut $5':>12}{'haircut $10':>13}{'ordini fatti':>14}")
f"{'haircut $5':>12}{'haircut $10':>13}{'eseguito':>21}")
hc10_5000 = np.nan
for cap in CAPITALI:
m0 = sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO)
@@ -568,9 +586,22 @@ def main() -> None:
hc10_5000 = h10
print(f" {cap:>9,.0f}{np.median(tkx)*cap:>12.2f}{a0['sharpe']:>9.2f}"
f"{a5['sharpe']:>11.2f}{a10['sharpe']:>12.2f}{h5:>11.0f}%{h10:>12.0f}%"
f"{m10['pct_eseguite']:>9.0f}%")
f"{m10['pct_eseguite']:>8.0f}% ord {m10['pct_nozionale']:>4.0f}% ntl")
print(f" -> haircut a $5.000 col pavimento VERO ($10): {hc10_5000:.0f}% "
f"(soglia pre-registrata {XSR_SOGLIA_HAIRCUT*100:.0f}%)")
print(f"\n la colonna haircut NON e' monotona (+35% a $2k, -6% a $4k): a un capitale solo e'")
print(f" una statistica RUMOROSA. Banda: haircut ricalcolato a pavimento $8/$10/$12 ->")
print(f" {'cap':>9}{'h($8)':>9}{'h($10)':>9}{'h($12)':>9}{'ampiezza':>11} giudizio")
for cap in CAPITALI:
s0 = metriche(sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO)["net"], idxx)["sharpe"]
hs = []
for mo in (8.0, 10.0, 12.0):
sh = metriche(sim_libro(Wx, Rx, rbx, cap, mo, lot_x, px_x, FEE_LEG_MODELLO)["net"], idxx)["sharpe"]
hs.append((s0 - sh) / abs(s0) * 100)
amp = max(hs) - min(hs)
g = ("ROTTO" if min(hs) > XSR_SOGLIA_HAIRCUT * 100 else
"ok" if max(hs) < XSR_SOGLIA_HAIRCUT * 100 else "INDECIDIBILE")
print(f" {cap:>9,.0f}{hs[0]:>8.0f}%{hs[1]:>8.0f}%{hs[2]:>8.0f}%{amp:>10.0f}pp {g}")
print(f" NB: e' la lettura del PARAMETRO sul backtest, NON il gate del 23/10 — "
f"quello si decide sulla finestra FORWARD e anticiparlo sarebbe selezione.")
@@ -596,9 +627,41 @@ def main() -> None:
print(f" {nm:<44}{a_['sharpe']:>8.2f}{a_['maxdd']:>7.1f}%{a_['cagr']:>7.1f}%")
print(f" (a capitale infinito = nessun vincolo di min-order: isola il COSTO dal PAVIMENTO)")
print(f"\n [B] SENSIBILITA' AL PAVIMENTO a $5.000 (l'haircut a un punto solo e' rumore:")
print(f" qui si vede DOVE il parametro comincia a mordere)")
print(f" {'pavimento $':>13}{'Sharpe':>9}{'haircut':>10}{'ord fatti':>11}{'ntl mosso':>11}")
sh0 = metriche(sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, FEE_LEG_MODELLO)["net"], idxx)["sharpe"]
for mo in (0.0, 5.0, 10.0, 15.0, 20.0, 30.0, 50.0):
m = sim_libro(Wx, Rx, rbx, 5000.0, mo if mo > 0 else None, None, None, FEE_LEG_MODELLO)
aa = metriche(m["net"], idxx)
print(f" {mo:>13.0f}{aa['sharpe']:>9.2f}"
f"{(sh0-aa['sharpe'])/abs(sh0)*100:>9.0f}%"
f"{m['pct_eseguite'] if np.isfinite(m['pct_eseguite']) else 100:>10.0f}%"
f"{m['pct_nozionale'] if np.isfinite(m['pct_nozionale']) else 100:>10.0f}%")
print(f"\n [B] BREAK-EVEN DEL COSTO — quanto dovrebbe valere lo slippage perche' il "
f"'rischio #1' fosse reale:")
print(f" {'costo/gamba':>13}{'= taker + slip':>18}{'Sharpe':>9}{'CAGR':>8}")
be = None
for bps in (4.5, 5.0, 7.5, 10.0, 15.0, 20.0, 30.0, 40.0):
aa = metriche(sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, bps / 1e4)["net"], idxx)
if be is None and aa["sharpe"] < 1.0:
be = bps
print(f" {bps:>11.1f} bps{max(bps-4.5,0):>13.1f} slip{aa['sharpe']:>9.2f}"
f"{aa['cagr']:>7.1f}%")
slip_mis = float(np.median(fee_real) - taker) * 1e4
print(f" -> Sharpe scende sotto 1.0 a ~{be:.0f} bps/gamba = ~{be-4.5:.0f} bps di slippage,")
print(f" cioe' **{(be-4.5)/max(slip_mis,1e-9):.0f}x** il mezzo spread misurato oggi "
f"({slip_mis:.1f} bps).")
print("\n" + "=" * 104)
print(" LIMITI DICHIARATI: gli spread sono UNO/POCHI snapshot di OGGI su un venue la cui")
print(" liquidita' e' cresciuta -> sono la stima piu' FAVOREVOLE per i 2.6 anni di backtest.")
tt = sorted(b["time"] for v in books.values() for b in v)
print(f" LIMITI DICHIARATI (1) i {a.snaps} snapshot coprono {(tt[-1]-tt[0])/1000:.0f} SECONDI:")
print(" NON sono osservazioni indipendenti, sono UNA fotografia intraday di oggi. E la")
print(" liquidita' di HL e' cresciuta -> e' la stima piu' FAVOREVOLE per i 2.6 anni di")
print(" backtest. Cio' che regge al limite e' il MARGINE (21x), non il livello.")
print(" (2) il lotto in $ usa il mark di OGGI su 2.6 anni di storia: irrilevante perche' il")
print(" lotto non e' mai vincolante (max $1.69 contro $10 di pavimento), ma va detto.")
print(" Il pavimento $10 e' [C] (doc + messaggio d'errore), non provato da un ordine: provarlo")
print(" richiederebbe di mandarne uno, e questo audit non tocca il conto.")
print("=" * 104)
+155 -20
View File
@@ -302,11 +302,39 @@ def main() -> dict:
print("\n IL TEST DI SUPER-PROPORZIONALITA' (la domanda esatta del brief):")
dpk = [delta_p[j] / KS[j] for j in range(len(KS))]
rap = [dd_acc[j] / dd_close[j] for j in range(len(KS))]
j1 = KS.index(1.00)
print(f" Δ/k a k=0.50 {dpk[0]:+.3%} · 1.00 {dpk[j1]:+.3%} · 1.50 {dpk[KS.index(1.50)]:+.3%}"
print(" Δ per UNITA' di leva (se il minimo intraday amplificasse piu' che")
print(" proporzionalmente, questa riga CRESCEREBBE in modulo con k):")
print(f" Δ/k k=0.50 {dpk[0]:+.3%} · 1.00 {dpk[j1]:+.3%} · 1.50 {dpk[KS.index(1.50)]:+.3%}"
f" · 2.00 {dpk[KS.index(2.00)]:+.3%} · 5.00 {dpk[-1]:+.3%}")
cresce = dpk[KS.index(2.00)] < dpk[j1] # piu' negativo = piu' costoso per unita' di leva
print(f" -> Δ/k {'CRESCE in modulo con k = super-proporzionale' if cresce else 'NON cresce in modulo: l effetto e circa PROPORZIONALE'}.")
print(f" rapporto maxDD acc./close: {' · '.join(f'{r:.3f}' for r in rap)}")
super_prop = dpk[KS.index(2.00)] > dpk[j1] * 1.05
sotto = dpk[KS.index(2.00)] < dpk[j1] * 0.95
verdetto = ("SUPER-proporzionale" if super_prop else
("SOTTO-proporzionale" if sotto else "proporzionale entro il rumore"))
print(f"\n ❌ RISPOSTA: NO — Δ/k **CALA** in modulo (da {dpk[0]:.3%} a {dpk[-1]:.3%}) e il")
print(f" RAPPORTO fra le due maxDD e' PIATTO ({min(rap):.3f}-{max(rap):.3f}) su tutta la")
print(f" griglia. La lente accoppiata e' un RICARICO MOLTIPLICATIVO COSTANTE di ~"
f"{(np.mean(rap)-1)*100:.1f}%")
print(f" sul maxDD, non un'amplificazione: verdetto **{verdetto}**.")
print(" Meccanismo: il maxDD di 25 anni e' una statistica MULTI-GIORNO (una discesa di")
print(" settimane), il wick e' un fatto di UN giorno. Aggiungere qualche punto di tuffo")
print(" a una discesa gia' lunga sposta poco, e la parte della distribuzione che")
print(" conterebbe non c'e': nel 26,8% dei giorni il libro CHIUDE sul minimo (gap = 0)")
print(" e quei giorni sono proprio i brutti. A leva alta Δ/k cala anche perche' i")
print(" drawdown si allungano piu' in fretta di quanto il singolo tuffo cresca.")
print("\n ⚠️ MA C'E' UN'ECCEZIONE, ed e' quella che decide la frontiera: la PROBABILITA'")
print(" di sfondare una SOGLIA FISSA e' amplificata assai piu' del maxDD stesso,")
print(" perche' vicino alla soglia la densita' e' fitta.")
print(f"\n {'k':>6}{'P(DD>30%) close':>17}{'P(DD>30%) acc.':>16}{'amplificazione':>16}")
for j, k in enumerate(KS):
pc = float((s0["dd"][:, j, 0] > DD_SOGLIA).mean())
pa = float((s0["dd"][:, j, 1] > DD_SOGLIA).mean())
amp = (pa / pc) if pc > 0 else float("nan")
print(f" {k:>6.2f}{pc:>17.1%}{pa:>16.1%}"
f"{(f'{amp:.2f}x' if np.isfinite(amp) else 'n.d.'):>16}")
print("\n IL NUMERO CHE DECIDE — L'HAIRCUT DI LEVA IMPLICITO NELLA LENTE:")
print(" 'a quale k close-only corrisponde il rischio di DD che la lente accoppiata")
@@ -397,6 +425,44 @@ def main() -> dict:
print(f" <= {frac:.0%} dell'equity -> costo massimo {sl*frac:.0%} dell'equity per asset a k=1,")
print(f" cioe' {sl*frac*2:.0%} se scattano entrambi; diventa il -100% a k = {1/(sl*frac*2):.1f}x.")
# ------------------------------------------------------------------ (3-bis)
sezione("(3-bis) DOVE L'ACCOPPIAMENTO MORDE DAVVERO — la regola a UN GIORNO, sui giorni STORICI")
print("\n Il finding del 25/07 ('close-only e' CIECA', 'l'indipendente RADDOPPIA i breach')")
print(" e' misurato su una regola di DAILY-LOSS: una soglia valutata sul MINIMO di UN")
print(" giorno. Il maxDD di un piano d'accumulo e' un'altra grandezza. Qui le due si")
print(" guardano fianco a fianco, sui GIORNI VERI (nessun bootstrap): quante volte in")
print(f" {len(D['R_del'])/365:.1f} anni una soglia di perdita giornaliera viene sfondata.")
Rh, Gh = D["R_del"], gap
rng2 = np.random.default_rng(909)
Gsh = Gh.copy(); rng2.shuffle(Gsh)
anni_st = len(Rh) / 365.0
for T in (0.03, 0.05, 0.10):
print(f"\n soglia daily-loss {T:.0%} (breach/anno)")
print(f" {'k':>6}{'close-only':>13}{'accoppiata':>13}{'indip. (finto)':>17}"
f"{'acc./close':>12}{'indip./acc.':>13}")
for k in (1.00, 1.25, 1.50, 2.00, 3.00):
nc = float((Rh * k <= -T).sum()) / anni_st
na = float(((Rh + Gh) * k <= -T).sum()) / anni_st
ni = float(((Rh + Gsh) * k <= -T).sum()) / anni_st
r1 = (na / nc) if nc > 0 else float("inf")
r2 = (ni / na) if na > 0 else float("nan")
print(f" {k:>6.2f}{nc:>13.2f}{na:>13.2f}{ni:>17.2f}"
f"{(f'{r1:.2f}x' if np.isfinite(r1) else 'INF'):>12}"
f"{(f'{r2:.2f}x' if np.isfinite(r2) else 'n.d.'):>13}")
print("\n 📌 IL RISULTATO TRASFERIBILE DEL FILONE: la lezione del 25/07 e' VERA e NON SI")
print(" TRASFERISCE. Su una regola a UN GIORNO l'accoppiamento cambia il conto di un")
print(" fattore (e close-only puo' dare ZERO dove il vero e' positivo: cecita' esatta);")
print(" su una statistica MULTI-GIORNO come il maxDD di un piano d'accumulo lo stesso")
print(" difetto vale un ricarico costante di pochi punti percentuali. Il gate")
print(" pre-registrato ha importato una lezione dal contesto in cui e' stata misurata")
print(" (conti FUNDED, barriera di daily-loss) a un contesto in cui la grandezza")
print(" decisiva e' un'altra — stessa forma dell'errore che il progetto ha gia'")
print(" codificato il 26/07 sulla barra parziale (SKH01 +0.38 / TP01 ±0.03: 'la")
print(" conclusione non si trasferisce fra sleeve').")
print(" ⚠️ Il che NON assolve close-only in generale: sul canale PROP/FUNDED, dove il")
print(" vincolo binding e' esattamente una regola di daily-loss per-conto, la lente")
print(" accoppiata resta obbligatoria e la leva li' e' un'altra decisione.")
# ------------------------------------------------------------------ (4)
sezione("(4) IL GRADINO ESEGUIBILE — sopravvive alla lente? (€500/mese, muro netto)")
s500 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=500, bersaglio=BERSAGLIO, seed=SEED)
@@ -443,10 +509,10 @@ def attacco2() -> None:
f" <- lo scetticato dichiara 12,14%")
print(f" -> VERIFICATO. E il numero non e' un artefatto della sua ricostruzione.")
print("\n ⚠️ MA IL MECCANISMO CHE DICHIARA E' INCOMPLETO, e la parte che manca e' la")
print(" maggiore. Lo scetticato scrive: «il target vale sulla posizione quando c'e', e")
print(" TP01 e' long-flat». Decomposizione:")
convs, vols_pred, vols_real, sers = [], [], [], {}
print("\n ⚠️ MA IL MECCANISMO CHE DICHIARA SPIEGA CIRCA META' DEL DIVARIO, e la meta'")
print(" che manca porta a una conclusione operativa diversa. Lo scetticato scrive:")
print(" «il target vale sulla posizione quando c'e', e TP01 e' long-flat». Misura:")
convs, vols_pred, vols_real, sers, flats = [], [], [], {}, []
for a in ("BTC", "ETH"):
df = resample_1d(load_data(a, "1h"))
c = df["close"].values.astype(float)
@@ -461,6 +527,7 @@ def attacco2() -> None:
sers[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
ok = np.isfinite(d) & np.isfinite(vol) & (vol > 0)
convs.append(float(np.sqrt(np.mean(d[ok] ** 2))))
flats.append(float((d[ok] == 0).mean()))
# errore del previsore di vol: vol realizzata nei 30g SEGUENTI / vol prevista
fwd = pd.Series(r).rolling(30).std().shift(-30).values * np.sqrt(365.25)
okf = ok & np.isfinite(fwd)
@@ -478,21 +545,35 @@ def attacco2() -> None:
comb = (0.5 * J["BTC"] + 0.5 * J["ETH"]).std() * 365 ** .5
div = comb / (0.5 * vb + 0.5 * ve)
conv = float(np.mean(convs))
per_leg = (0.5 * vb + 0.5 * ve) / 0.20
p_flat = float(np.mean(flats))
f_flat = float(np.sqrt(1.0 - p_flat))
f_conv = conv / f_flat if f_flat > 0 else float("nan")
print(f"\n gamba singola: BTC {vb:.2%} · ETH {ve:.2%} -> 50/50 combinato {comb:.2%}")
print(f" fattore di DIVERSIFICAZIONE BTC/ETH = {div:.3f} (corr {J['BTC'].corr(J['ETH']):.2f})")
print(f"\n CATENA: 20,00% ×{conv:.3f} (convinzione) ×{np.mean(vols_real)/np.mean(vols_pred):.3f} "
f"(previsore di vol) ×{div:.3f} (diversificazione) = "
f"{0.20*conv*(np.mean(vols_real)/np.mean(vols_pred))*div:.2%}")
print(f" misurato: {tp.std()*365**.5:.2%}")
print(f"\n 📌 IL FATTO CHE LO SCETTICATO NON DICE: il pezzo grosso NON e' lo stare flat, e'")
print(f" che `tsmom_blend` e' la MEDIA DEI SEGNI di 3 orizzonti, quindi quando TP01 e'")
print(f" 'a mercato' e' quasi sempre a 1/3 o 2/3 di convinzione. Contando lo zero, il")
print(f" fattore di convinzione da solo vale ×{conv:.2f}. Lo stare flat e' UNA PARTE di")
print(f" quel fattore, non il fattore.")
print(f" Conseguenza operativa opposta a quella suggerita: se il divario fosse solo")
print(f" 'flat', alzare target_vol non aumenterebbe il rischio nei giorni flat (dove non")
print(f" c'e' posizione). Essendo CONVINZIONE, alzare target_vol alza la posizione")
print(f" proprio nei giorni in cui il segnale e' PIU' DEBOLE (1 orizzonte su 3).")
print(f"\n SCOMPOSIZIONE ESATTA (i due fattori si moltiplicano e chiudono il conto):")
print(f" 20,00% ×{per_leg:.3f} (dentro la GAMBA) ×{div:.3f} (diversificazione BTC/ETH)"
f" = {0.20*per_leg*div:.2%}")
print(f" misurato sullo sleeve di produzione: {tp.std()*365**.5:.2%}")
print(f"\n E il ×{per_leg:.3f} della gamba si spacca a sua volta in DUE meta' quasi uguali:")
print(f" · sta FLAT nel {p_flat:.1%} delle barre (direzione 0) -> ×{f_flat:.3f}")
print(f" · quando NON e' flat sta a convinzione PARZIALE (rms {conv/f_flat:.2f}) -> ×{f_conv:.3f}")
print(f" prodotto sqrt(E[d²]) = ×{conv:.3f}; il residuo fino a ×{per_leg:.3f} e'")
print(f" la correlazione fra convinzione e vol realizzata/prevista"
f" (previsore di vol da solo: ×{np.mean(vols_real)/np.mean(vols_pred):.3f}).")
print(f"\n 📌 QUELLO CHE LO SCETTICATO NON DICE, e sono DUE cose.")
print(f" (i) «TP01 e' long-flat» spiega **circa META'** del divario (×{f_flat:.2f}), non")
print(f" il divario. L'altra meta' e' che `tsmom_blend` e' la MEDIA DEI SEGNI di 3")
print(f" orizzonti: quando TP01 e' a mercato sta quasi sempre a 1/3 o 2/3 di")
print(f" convinzione (×{f_conv:.2f}). E la differenza NON e' accademica: se il divario")
print(f" fosse solo 'flat', alzare target_vol non toccherebbe i giorni senza")
print(f" posizione; essendo CONVINZIONE, alzarlo aumenta la size proprio nei giorni")
print(f" in cui il segnale e' PIU' DEBOLE (1 orizzonte su 3 d'accordo).")
print(f" (ii) un terzo fattore non sta nemmeno nello sleeve: ×{div:.2f} viene dal")
print(f" COMBINARE BTC ed ETH a 50/50 (corr {J['BTC'].corr(J['ETH']):.2f}). Non e' un")
print(f" difetto d'etichetta: e' diversificazione, ed e' esattamente cio' che si")
print(f" vuole. Chiamare 'sovrastima del rischio' anche questo pezzo confonde una")
print(f" proprieta' desiderata con un errore di misura.")
print("\n E TP01 E' UN CASO PARTICOLARE, non la regola — gli altri sleeve vol-targeted:")
print(f"\n {'sleeve':>9}{'target dichiarato':>19}{'vol REALIZZATA':>16}{'su barre ATTIVE':>17}{'attive':>9}")
@@ -590,6 +671,59 @@ def attacco2() -> None:
print(" esattamente quella protetta dal test di guardia.")
def attacco2b() -> None:
"""Quali affermazioni pubblicate cambiano, e quali no — misurato, non argomentato."""
from src.portfolio import sleeves as SL
sezione("(6-bis) ATTACCO 2c — QUALI AFFERMAZIONI CAMBIANO (e la maggior parte NON cambia)")
tp, sk = SL._tp01_returns(), SL._skyhook_returns()
if tp.index.tz is None:
tp = tp.tz_localize("UTC")
if sk.index.tz is None:
sk = sk.tz_localize("UTC")
B = pd.concat({"TP01": tp, "SKH01": sk}, axis=1, join="inner").fillna(0.0)
w = np.array([W_TP, W_SKH])
X = B[["TP01", "SKH01"]].values
bk = X @ w
cov = np.cov(X.T)
var_b = float(bk.var())
rc = w * (cov @ w) / (cov @ w @ w)
print("\n NON cambia NIENTE di misurato. Ogni Sharpe, maxDD, muro, traiettoria e rendita")
print(" pubblicati sono calcolati sulla SERIE DI RITORNI REALIZZATA, mai sull'etichetta:")
print(" `target_vol` entra nel codice come parametro di sizing e non compare in nessuna")
print(" formula di rischio a valle. Un'etichetta che sovrastima non rende ottimista un")
print(" numero che non la usa.")
print("\n CAMBIA una classe di affermazioni sola: quelle che CONFRONTANO un parametro con")
print(" una misura. Nel progetto ce n'e' una, ed e' nel libro LIVE:")
print(f"\n {'sleeve':>8}{'peso NOZIONALE':>16}{'vol realizzata':>16}"
f"{'contributo al RISCHIO':>23}")
for i, nm in enumerate(("TP01", "SKH01")):
print(f" {nm:>8}{w[i]:>16.0%}{X[:, i].std()*365**.5:>16.2%}{rc[i]:>23.1%}")
print(f" {'libro':>8}{1.0:>16.0%}{bk.std()*365**.5:>16.2%}{sum(rc):>23.1%}")
print(f" corr TP01/SKH01 {np.corrcoef(X.T)[0,1]:+.3f} vol libro {bk.std()*365**.5:.2%}")
sig = X.std(axis=0) * 365 ** 0.5
cf = (W_TP * 0.20) ** 2 / ((W_TP * 0.20) ** 2 + (W_SKH * sig[1]) ** 2)
print("\n ❌ IPOTESI MIA, REFUTATA DAL MIO STESSO NUMERO. Mi aspettavo che l'etichetta")
print(" nascondesse una ripartizione del rischio molto diversa dai pesi di nozionale.")
print(f" Non e' cosi': {rc[0]*100:.0f}/{rc[1]*100:.0f} contro {W_TP*100:.0f}/{W_SKH*100:.0f}"
f" — coincidono entro un paio di punti, e per una")
print(" ragione che si legge in una riga: contributi di rischio ~ (w·σ)² con corr")
print(f" {np.corrcoef(X.T)[0,1]:+.2f} ~ 0, e {W_TP:.2f}×{sig[0]:.1%} = {W_TP*sig[0]:.2%} contro"
f" {W_SKH:.2f}×{sig[1]:.1%} = {W_SKH*sig[1]:.2%}")
print(" stanno gia' in rapporto ~75/25. **Coincidenza numerica, non struttura.**")
print("\n 📌 IL FATTO CHE RESTA, ed e' piu' utile dell'ipotesi che ho perso: l'errore non")
print(" lo commette chi legge i PESI, lo commette chi legge l'ETICHETTA. Un lettore che")
print(f" prendesse `target_vol=20%` per buono calcolerebbe TP01 al {cf*100:.0f}% del rischio")
print(f" di libro; il vero e' {rc[0]*100:.0f}%. L'etichetta non rende ottimista nessun numero")
print(" pubblicato (nessuno la usa) — sposta di ~16 punti l'ATTRIBUZIONE del rischio,")
print(" cioe' proprio la domanda 'quale gamba mi fa male' su cui il progetto ha gia'")
print(" tre misure indipendenti che indicano SKH01 come la fragile (LOO d'ancora,")
print(" sensibilita' alla fee 4x, vol realizzata piu' alta).")
print("\n E si precisa una frase: «SKH01 e' l'unica delle 5 a NON essere vol-targeted» e'")
print(" esatta sul CODICE e va letta insieme al fatto che e' anche quella che di vol ne")
print(" realizza di PIU' (21,0% contro 12,1%). Non e' una contraddizione: e' il motivo per")
print(" cui il peso 25% e' stato confermato tre volte all'estremo prudente.")
def guardia() -> None:
sezione("(7) IL VINCOLO DURO — cosa protegge davvero il test di guardia")
cfg = json.loads((ROOT / "config" / "live.json").read_text())
@@ -614,5 +748,6 @@ def guardia() -> None:
if __name__ == "__main__":
main()
attacco2()
attacco2b()
guardia()
print(f"\n▸ totale: consegna nel messaggio dell'agente.")
+33 -11
View File
@@ -540,8 +540,11 @@ def main() -> None:
f"-> {'INDISTINGUIBILI' if abs(dA) < 2*seA else 'differenza reale'}")
print(f" argmax J - libro LIVE : {dB:+.4f} +- {seB:.4f} "
f"-> {'INDISTINGUIBILI' if abs(dB) < 2*seB else 'differenza reale'}")
print(" La prima riga E' il risultato del filone: progettare per la barriera invece che")
print(" per lo Sharpe NON sposta i pesi. La seconda dice dove sta il guadagno vero.")
quota = abs(dA) / abs(dB) if dB != 0 else float("nan")
print(f" La prima riga E' il risultato del filone: riallocare per la BARRIERA invece che")
print(f" per lo SHARPE vale {dA:+.3f} di J, cioe' il {quota:.0%} del {dB:+.3f} che vale")
print(f" passare dal libro LIVE a un libro diversificato QUALSIASI. Il grosso del guadagno")
print(f" NON e' nella funzione obiettivo: e' nell'aggiungere lo sleeve che a $600 non entra.")
print("\n Le prime 8 celle per J:")
print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} "
@@ -688,13 +691,16 @@ def main() -> None:
for lv in LEVS:
rows.append((w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)))
w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"])
oL = max((objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx,
drift_mult=dmt) for lv in LEVS), key=lambda x: x["J"])
lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx,
drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"])
dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", ev_idx, fu_idx)
print(f" {mult:>12.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {o_['p_pass']:>8.1%} "
f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ (LIVE 75/25: J {oL['J']:.3f})")
print("\n Se anche a drift XS01 = 0 (cioe' XS01 ridotto a pura diversificazione di VOLATILITA',")
print(" senza alcun rendimento atteso) la configurazione diversificata batte il libro LIVE,")
print(" allora il risultato NON dipende dall'edge di XS01 ma dalla sua DECORRELAZIONE.")
f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ LIVE {oL['J']:.3f} "
f"delta {dd_:+.3f} +-{sd_:.3f}")
print("\n La colonna che decide e' l'ULTIMA. Se il vantaggio svanisce col drift di XS01, il")
print(" risultato e' un'ipotesi sul RENDIMENTO ATTESO di XS01 misurato sulla sua finestra di")
print(" scoperta; se resta a drift 0, il vantaggio e' pura DECORRELAZIONE e non dipende da")
print(" quell'ipotesi. Il numero, non la narrazione, dice quale delle due.")
# ---------------------------------------------------------------- F. VRP01 / GTAA01
tick("E. banda ancora")
@@ -715,6 +721,10 @@ def main() -> None:
lab = "nessuna (base)" if extra is None else f"{extra} @ {wx:.0%}"
print(f" {lab:>34} {lv:>6.3f} {o['J']:>7.3f} {o['p_pass']:>8.1%} {o['p_alive']:>8.1%} "
f"{o['sharpe']:>7.2f} {o['e_payout']:>9,.0f}$")
print("\n Le righe VRP01 e GTAA01 escono quasi IDENTICHE: non e' un errore, e' che sotto questa")
print(" funzione obiettivo i due sleeve sono lo stesso oggetto (vol ~6.2%, drift ~7.7%, corr ~0")
print(" col crypto). La barriera vede vol, drift e correlazione; la differenza fra i due — skew")
print(" -4.6 contro -0.4 — a peso 10-20% e' diluita sotto la soglia di daily-loss.")
print("\n ATTENZIONE: la riga VRP01 e' calcolata con **gap = 0** (il mark infra-settimana non")
print(" esiste nel dataset dello sleeve): e' un TETTO, non una stima. Con il mark misurato il")
print(" 30/07 (peggiore infra-settimana mediano -6.9%, minimo -81.7%) una barriera STATICA al")
@@ -815,10 +825,22 @@ def main() -> None:
seed=20260822, lens="coupled", start=START_WIN)
print(f" {fct:>9.2f} {lab:>26} {r['med']:>10.2f} {r['p_target']:>10.1%} "
f"{r['p_zero']:>8.1%}")
print("\n ⚠ Il headline ~6% del 25/07 NON e' la riga 'LIVE su tutti i conti' (quella li' era")
print(" CONC-2SL, dichiarata 'la PEGGIORE'): era la politica MISTA, sleeve diversi su conti")
print(" diversi. Replica ESATTA di quella politica, stessa macchina, de-luck 0.60, leva 0.75:")
pc.DELUCK = 0.60
cfgs25 = [(0.75, 0.25, 0.0), (0.55, 0.20, 0.25), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
r25 = pc.simulate(cfgs25, [1, 4, 1, 2, 1, 4], n_paths=1500, lev=0.75, seed=20260822,
lens="coupled", start=START_WIN)
print(f" MISTO (25/07) @0.75x, de-luck 0.60 : EUR/g med {r25['med']:.2f} | "
f"P(>=50/g) {r25['p_target']:.1%} | P(zero) {r25['p_zero']:.1%}")
print(f" -> il 25/07 pubblico' P(>=50/g) 5.6-6.5% e P(zero) 52% a 0.75x. "
f"{'REPLICA OK' if 0.03 <= r25['p_target'] <= 0.10 else 'NON REPLICA — leggere con cautela'}")
pc.DELUCK = DELUCK
print("\n La cella in alto a sinistra (0.60 + LIVE) e' la configurazione del 25/07 e va letta")
print(" come replica: se riproduce ~6% di P(>=50/g) e ~52-65% di P(zero), la macchina e' la")
print(" stessa e le altre tre celle misurano cio' che dichiarano.")
print("\n Lettura della 2x2: il de-luck e l'allocazione contribuiscono ENTRAMBI, e vanno citati")
print(" separati. Il fattore 0.60 era una stima a occhio poi RI-MISURATA troppo severa (26/07):")
print(" usarlo ancora sarebbe sbagliato, ma il salto da 1.7% a 41.7% e' il prodotto di due")
print(" correzioni, non di una scoperta.")
print("\n" + "=" * 104)
print(f" fatto in {time.time()-t0:.0f}s")
-3
View File
@@ -40,7 +40,6 @@ Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_skew.py
from __future__ import annotations
import glob
import os
import sys
import time
from pathlib import Path
@@ -57,8 +56,6 @@ import altlib as A # noqa: E402
import cblib as CB # noqa: E402
CHAIN = ROOT / "data" / "raw" / "cb_chain"
SCRATCH = Path(os.environ.get("SKEW_SCRATCH", "/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad"))
ASSETS = ("BTC", "ETH")
COLS = ["ts", "asset", "instrument_name", "option_type", "strike", "exp",
"bid", "ask", "mid", "iv", "delta", "open_interest", "quote_status"]
+509
View File
@@ -0,0 +1,509 @@
"""r0822 SLIP-AUDIT — l'attrito VERO del libro live: quanto costa davvero un fill.
DOMANDA
-------
Il progetto modella le fee con cura chirurgica (3,50 bps/lato misurati sui fill reali il 21/08)
ma ASSUME il fill al prezzo di chiusura della barra. Lo **slippage realizzato** del libro live
non e' mai stato misurato: e' dichiarato "rischio #1" per XSR01 e semplicemente ignorato per
TP01/SKH01. Ci sono 18 esecuzioni reali dal 2026-07-14. Questo script le misura.
La domanda decidibile e' UNA: i backtest girano a `fee_rt=0.001` (10 bps RT) mentre la fee vera
e' 3,50 bps/lato = 7 bps RT. C'e' **3 bps RT = 1,50 bps/LATO** di margine gia' pagato dal modello.
**Lo slippage realizzato ci sta dentro?**
TRE DIFETTI DEL DATO TROVATI PRIMA DI MISURARE (e riparati/dichiarati qui)
-------------------------------------------------------------------------
(1) **`ts_utc` nel log NON e' l'ora del fill.** `book_execute.py:213` scrive
`ts_utc=str(pd.Timestamp(r['last_data']))` = l'etichetta dell'ULTIMA BARRA DEL FEED, non
l'orologio. Tutti e 18 i record dicono `00:00:00`. L'ora vera si recupera solo dalle
intestazioni di `logs/cron_book.log` (`===== <ISO> cron_book =====`). Senza questo passo
ogni confronto "al prezzo dell'ora del fill" misurerebbe la mezzanotte.
Prova che serviva: il fill #1 e' etichettato `2026-07-08` ed e' stato eseguito il
**2026-07-14T14:00Z** sei giorni dopo (feed-freeze del 14/07, gia' noto al progetto).
(2) **Il feed certificato NON e' lo strumento che si trada.** `rebuild_history.DERIBIT_INSTR`
prende `BTC/USD:BTC` / `ETH/USD:ETH` = i perpetual **INVERSE**; `deribit.INSTRUMENT` esegue su
`BTC_USDC-PERPETUAL` / `ETH_USDC-PERPETUAL` = i **LINEARI USDC**. Il commento nel sorgente dice
"inverse ~ lineare entro 3 bps": **3 bps e' esattamente la taglia della domanda**. Misurare lo
slippage contro il feed certificato significherebbe misurare la BASE, non lo slippage.
Qui la base viene MISURATA (non assunta) e il riferimento vero e' lo strumento tradato.
Stessa forma del difetto di `fee_watch` del 21/08.
(3) **Convenzione LINEARE**: `amount` = quantita' BASE, `fee` gia' in USDC ->
`fee_bps = fee / (filled * price) * 1e4`. La convenzione inverse darebbe ~8 ordini di
grandezza di errore SENZA sollevare nulla. Il controllo e' stampato in chiaro.
TRE RIFERIMENTI, TENUTI SEPARATI (il punto 1c della missione)
------------------------------------------------------------
Il modello decide sulla barra GIORNALIERA, il cron esegue all'ORA: sono grandezze diverse.
REF-A "modello" = close della barra 1d etichettata `last_data` (= il prezzo a cui il
BACKTEST assume di riempire). Misura modello-vs-realta', NON slippage.
REF-B "5m/1h" = close dell'ultima barra CHIUSA prima del fill sullo strumento TRADATO.
Questo, e solo questo, e' slippage d'esecuzione.
REF-C "VWAP 1h" = VWAP delle 12 barre 5m dell'ora che contiene il fill (benchmark
NON causale, dichiarato tale).
Segno orientato al lato: `costo_bps = lato * (fill - ref)/ref * 1e4`, lato +1 buy / -1 sell.
Positivo = costo, negativo = guadagno.
LA STATISTICA CHE HA POTENZA (e perche' quella in bps non ne ha)
----------------------------------------------------------------
Con n=18 la via in bps e' persa in partenza: il rumore e' la deriva del prezzo fra la chiusura
della barra 5m e il fill, che vale decine di bps. La statistica giusta e' NORMALIZZATA dal range
della barra: il nostro fill e' esso stesso un print della barra 5m che lo contiene, quindi
q = (fill - low)/(high - low) per un BUY, q = 1 - quello per un SELL.
Se i nostri ordini market avessero impatto, un BUY stamperebbe AL MASSIMO della barra -> q -> 1.
Senza impatto q e' un'estrazione ~uniforme -> E[q] = 0.5. sigma_q = 1/sqrt(12) = 0.289 e' ~20x
piu' piccolo del rumore in bps.
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_slip_audit.py
(--no-net salta il fetch dello strumento lineare e lo dichiara non misurato)
"""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
SCRATCH = Path(os.environ.get("SLIP_SCRATCH", "/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad"))
EXEC_LOG = ROOT / "data" / "live" / "book_executions.jsonl"
CRON_LOG = ROOT / "logs" / "cron_book.log"
# Sensibilita' PUBBLICATA del libro (r0726_fee_sensitivity.py, CLAUDE.md 26/07).
# Unita': per bps PER LATO (verificato sulla tabella: 0->15 bps/lato = Sharpe 1.849->1.597).
DSHARPE_PER_BPS_SIDE = -0.017
DCAGR_PER_BPS_SIDE = -0.23 # punti percentuali di CAGR
# Margine gia' pagato dal modello: backtest 10 bps RT vs fee vera 7 bps RT = 3 bps RT.
MARGIN_BPS_SIDE = 1.5
FEE_TAKER_BPS_SIDE = 3.50 # tier base misurato (fee_watch, 21/08)
# Strumento TRADATO (src/live/deribit.INSTRUMENT) vs strumento del FEED certificato
# (scripts/analysis/rebuild_history.DERIBIT_INSTR). NON sono lo stesso: vedi difetto (2).
CCXT_LINEAR = {"BTC": "BTC/USDC:USDC", "ETH": "ETH/USDC:USDC"}
CCXT_INVERSE = {"BTC": "BTC/USD:BTC", "ETH": "ETH/USD:ETH"}
RNG = np.random.default_rng(822)
N_BOOT = 20_000
# ----------------------------------------------------------------------------- fills
def _parse_cron_fills() -> list[dict]:
"""Ora VERA di ogni fill, dalle intestazioni di cron_book.log. Vedi difetto (1)."""
if not CRON_LOG.exists():
raise SystemExit(f"manca {CRON_LOG}: l'ora vera del fill non e' recuperabile altrove")
head = re.compile(r"^===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====")
fill = re.compile(r"^\s+-> (BUY|SELL) ([\d.]+) @ \$([\d,.]+) fee ([\d.]+) \((\w+)\)")
out, cur = [], None
with open(CRON_LOG, errors="replace") as f:
for line in f:
m = head.match(line)
if m:
cur = pd.Timestamp(m.group(1))
continue
m = fill.match(line)
if m:
out.append(dict(fill_ts=cur, side=m.group(1).lower(),
amt=float(m.group(2)), px=float(m.group(3).replace(",", "")),
fee=float(m.group(4)), status=m.group(5)))
return out
def load_fills() -> pd.DataFrame:
"""JSONL (verita' sui numeri) + cron log (verita' sull'ora), appaiati e VERIFICATI."""
recs = [json.loads(l) for l in EXEC_LOG.read_text().splitlines() if l.strip()]
cron = _parse_cron_fills()
if len(recs) != len(cron):
raise SystemExit(f"appaiamento impossibile: {len(recs)} righe jsonl vs {len(cron)} fill "
f"nel cron log. Non invento un'ora: mi fermo.")
rows = []
for i, (r, c) in enumerate(zip(recs, cron)):
# entrambi i file sono append-only dallo stesso processo -> stesso ordine. Lo PROVO.
ok = (r["side"] == c["side"]
and abs(round(r["filled"], 4) - round(c["amt"], 4)) < 1e-9
and abs(round(r["price"], 1) - round(c["px"], 1)) < 0.051
and abs(round(r["fee"], 5) - round(c["fee"], 5)) < 1e-9)
if not ok:
raise SystemExit(f"riga {i}: jsonl e cron log NON combaciano ({r} vs {c}) -> mi fermo")
if c["status"] != "OK":
continue
rows.append(dict(
i=i, fill_ts=c["fill_ts"], model_bar=pd.Timestamp(r["ts_utc"], tz="UTC"),
asset=r["asset"], side=r["side"], lato=1.0 if r["side"] == "buy" else -1.0,
filled=float(r["filled"]), price=float(r["price"]), fee=float(r["fee"]),
notional=float(r["filled"]) * float(r["price"]),
skh=r.get("skh_sign"), tp_frac=r.get("tp_frac"),
))
return pd.DataFrame(rows)
# ----------------------------------------------------------------------------- prezzi
def _certified(asset: str, tf: str, t0, t1) -> pd.DataFrame:
"""Feed certificato su disco = strumento INVERSE. Letto a colonne e tagliato subito (RAM)."""
f = ROOT / "data" / "raw" / f"{asset.lower()}_{tf}.parquet"
d = pd.read_parquet(f, columns=["timestamp", "open", "high", "low", "close", "volume"])
d["ts"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
d = d[(d["ts"] >= t0) & (d["ts"] <= t1)].drop(columns=["timestamp"]).reset_index(drop=True)
return d
def _fetch_linear(asset: str, t0, t1) -> pd.DataFrame | None:
"""Strumento TRADATO (lineare USDC) via ccxt pubblico. Cache su scratchpad, pacing gentile.
Il progetto ha una storia di rate-limit per-IP (29-30/07): qui sono ~13 pagine per asset,
distanziate. Nessuna scrittura sotto data/."""
SCRATCH.mkdir(parents=True, exist_ok=True)
cache = SCRATCH / f"lin_{asset.lower()}_5m.parquet"
if cache.exists():
d = pd.read_parquet(cache)
if d["ts"].min() <= t0 and d["ts"].max() >= t1 - pd.Timedelta(minutes=10):
return d
try:
import ccxt, time
ex = ccxt.deribit({"enableRateLimit": True})
sym, since, rows = CCXT_LINEAR[asset], int(t0.timestamp() * 1000), []
end = int(t1.timestamp() * 1000)
while since < end:
o = ex.fetch_ohlcv(sym, "5m", since=since, limit=1000)
if not o:
break
rows += o
since = o[-1][0] + 300_000
time.sleep(0.35)
if not rows:
return None
d = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"])
d = d.drop_duplicates("timestamp").sort_values("timestamp")
d["ts"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
d = d.drop(columns=["timestamp"]).reset_index(drop=True)
d.to_parquet(cache)
return d
except Exception as e: # rete assente = si dichiara
print(f" ! fetch lineare {asset} fallito: {type(e).__name__}: {str(e)[:90]}")
return None
def _last_closed(bars: pd.DataFrame, t) -> pd.Series | None:
"""Ultima barra CHIUSA a `t`: una barra etichettata b chiude a b+durata."""
if bars is None or bars.empty:
return None
step = bars["ts"].diff().median()
ok = bars[bars["ts"] + step <= t]
return None if ok.empty else ok.iloc[-1]
def _containing(bars: pd.DataFrame, t) -> pd.Series | None:
if bars is None or bars.empty:
return None
step = bars["ts"].diff().median()
ok = bars[(bars["ts"] <= t) & (t < bars["ts"] + step)]
return None if ok.empty else ok.iloc[-1]
# ----------------------------------------------------------------------------- stats
def ci_mean(x, boot=True):
x = np.asarray(x, float)
x = x[np.isfinite(x)]
n = len(x)
if n < 2:
return dict(n=n, mean=np.nan, lo=np.nan, hi=np.nan, sd=np.nan, med=np.nan)
m, sd = x.mean(), x.std(ddof=1)
if boot:
bs = RNG.choice(x, size=(N_BOOT, n), replace=True).mean(axis=1)
lo, hi = np.percentile(bs, [2.5, 97.5])
else:
from scipy import stats
h = stats.t.ppf(0.975, n - 1) * sd / np.sqrt(n)
lo, hi = m - h, m + h
return dict(n=n, mean=m, lo=lo, hi=hi, sd=sd, med=float(np.median(x)))
def fmt(d, u="bps"):
if not np.isfinite(d["mean"]):
return "n/d"
z = "" if (d["lo"] <= 0 <= d["hi"]) else " <- ZERO ESCLUSO"
return f"{d['mean']:+7.2f} {u} IC95 [{d['lo']:+7.2f}, {d['hi']:+7.2f}] (med {d['med']:+7.2f}, n={d['n']}){z}"
def main():
print("=" * 96)
print(" r0822 SLIP-AUDIT — l'attrito VERO del libro live (18 esecuzioni, dal 2026-07-14)")
print("=" * 96)
fills = load_fills()
t0 = fills["fill_ts"].min().floor("D") - pd.Timedelta(days=3)
t1 = fills["fill_ts"].max().ceil("h") + pd.Timedelta(hours=2)
# ---------------------------------------------------------------- 0. difetti del dato
print("\n[0] DIFETTI DEL DATO — trovati PRIMA di misurare\n" + "-" * 96)
lag = (fills["fill_ts"] - fills["model_bar"]).dt.total_seconds() / 3600.0
print(f" (1) `ts_utc` nel jsonl = etichetta dell'ultima barra del feed, NON l'ora del fill.")
print(f" 18/18 record dicono 00:00:00. Ora vera recuperata da logs/cron_book.log.")
print(f" Scarto fill - etichetta: mediana {lag.median():.1f}h, max {lag.max():.1f}h "
f"({(lag > 26).sum()} fill oltre le 26h).")
if (lag > 26).any():
w = fills.loc[lag.idxmax()]
print(f" Il caso estremo e' il fill #{w['i']}: etichetta {w['model_bar'].date()}, "
f"eseguito {w['fill_ts']} = feed-freeze del 14/07 (gia' noto al progetto).")
print(f" (2) Feed certificato = INVERSE ({CCXT_INVERSE['BTC']}), libro tradato = LINEARE "
f"({CCXT_LINEAR['BTC']}).")
print(f" La base fra i due e' misurata sotto, non assunta.")
# ---------------------------------------------------------------- 1. convenzione fee
print("\n[1] CONTROLLO DI CONVENZIONE (lineare USDC) — la trappola da ~8 ordini di grandezza\n"
+ "-" * 96)
fills["fee_bps"] = fills["fee"] / fills["notional"] * 1e4
fb = ci_mean(fills["fee_bps"])
print(f" fee_bps = fee_usdc / (filled * price) * 1e4 [LINEARE: amount=base, fee gia' in USDC]")
print(f" media {fb['mean']:.3f} bps/lato · mediana {fb['med']:.3f} · "
f"min {fills['fee_bps'].min():.3f} · max {fills['fee_bps'].max():.3f}")
print(f" tier base dichiarato da fee_watch: {FEE_TAKER_BPS_SIDE:.2f} bps/lato -> "
f"scarto {fb['med'] - FEE_TAKER_BPS_SIDE:+.3f} bps")
bad = (fills["fee"] / fills["filled"] * 1e4).median()
print(f" [se avessi usato la convenzione INVERSE: {bad:,.0f} bps -> "
f"{bad / FEE_TAKER_BPS_SIDE:,.0f}x il vero, e senza sollevare nulla]")
odd = fills[(fills["fee_bps"] - FEE_TAKER_BPS_SIDE).abs() > 0.05]
if len(odd):
print(f" ⚠️ {len(odd)}/{len(fills)} fill NON al tier {FEE_TAKER_BPS_SIDE:.2f}:")
for _, o in odd.iterrows():
print(f" #{o['i']} {o['fill_ts']} {o['asset']} {o['side']} "
f"{o['filled']} @ {o['price']:.2f} = ${o['notional']:.2f} · "
f"fee {o['fee']:.8f} = {o['fee_bps']:.2f} bps")
print(f" -> nozionale ${odd['notional'].min():.2f}: sotto i ~$7 la fee sembra")
print(f" arrotondata AL RIALZO. E' un costo REALE che il modello non ha, e")
print(f" morde solo gli ordini minuscoli (dove il libro vive a $600).")
# ---------------------------------------------------------------- 2. potenza, PRIMA
print("\n[2] POTENZA DICHIARATA — calcolata PRIMA di guardare lo slippage\n" + "-" * 96)
sig5 = {}
for a in sorted(fills["asset"].unique()):
d = _certified(a, "5m", t0, t1)
r = np.log(d["close"]).diff().dropna() * 1e4
sig5[a] = float(r.std())
sig = float(np.mean(list(sig5.values())))
n = len(fills)
mde_bps = 2.80 * sig / np.sqrt(n) # 80% potenza, bilaterale alpha=0.05
print(f" Rumore strutturale: sigma dei ritorni 5m nella finestra = "
+ " · ".join(f"{a} {v:.1f} bps" for a, v in sig5.items()) + f" (media {sig:.1f})")
print(f" Il riferimento 5m dista dal fill 0-5 min -> il rumore e' di QUELL'ordine.")
print(f" MDE in bps a n={n}, potenza 80%, alpha 5%: 2.80 * {sig:.1f} / sqrt({n}) = "
f"**{mde_bps:.1f} bps/lato**")
print(f" Il margine da difendere e' {MARGIN_BPS_SIDE:.1f} bps/lato (3 bps RT).")
print(f" => VERDETTO DI POTENZA, dichiarato prima del risultato: la via in bps NON puo'")
print(f" risolvere {MARGIN_BPS_SIDE:.1f} bps ({mde_bps / MARGIN_BPS_SIDE:.0f}x troppo grossolana). "
f"Qualunque IC in bps conterra' lo zero;")
print(f" un 'non distinguibile da zero' li' NON e' una prova di assenza di slippage.")
mde_q = 2.80 * (1 / np.sqrt(12)) / np.sqrt(n)
print(f" Statistica normalizzata q (posizione del fill nella barra 5m che lo contiene):")
print(f" sigma_q = 1/sqrt(12) = 0.289 -> MDE_q = {mde_q:.3f} di range di barra.")
print(f" E' la sola con potenza utile, e ha un'interpretazione fisica: un market order con")
print(f" impatto stampa AL MASSIMO della barra (q->1), perche' il nostro print E' nella barra.")
# ---------------------------------------------------------------- 3. base inverse/lineare
print("\n[3] BASE inverse-vs-lineare — MISURATA, non assunta (difetto 2)\n" + "-" * 96)
lin, inv = {}, {}
use_net = "--no-net" not in sys.argv
for a in sorted(fills["asset"].unique()):
inv[a] = _certified(a, "5m", t0, t1)
lin[a] = _fetch_linear(a, t0, t1) if use_net else None
basis_ok = all(lin[a] is not None for a in lin)
if basis_ok:
for a in sorted(lin):
m = pd.merge(inv[a][["ts", "close"]], lin[a][["ts", "close"]], on="ts",
suffixes=("_inv", "_lin"))
b = (m["close_lin"] - m["close_inv"]) / m["close_inv"] * 1e4
print(f" {a}: n={len(m):,} barre 5m · base mediana {b.median():+.2f} bps · "
f"media {b.mean():+.2f} · IQR [{b.quantile(.25):+.2f}, {b.quantile(.75):+.2f}] · "
f"|base| p95 {b.abs().quantile(.95):.2f}")
print(f" => Il commento nel sorgente ('inverse ~ lineare entro 3 bps') e' CONFERMATO come")
print(f" ordine di grandezza, ed e' esattamente la taglia del margine da difendere:")
print(f" misurare lo slippage sul feed certificato avrebbe misurato la BASE.")
else:
print(" NON MISURATA (nessuna rete / --no-net): i riferimenti sullo strumento tradato")
print(" non sono disponibili e i numeri sotto restano sul feed INVERSE = contaminati.")
# ---------------------------------------------------------------- 4. i tre riferimenti
print("\n[4] I TRE RIFERIMENTI — segno orientato al lato (positivo = COSTO)\n" + "-" * 96)
inv1h = {a: _certified(a, "1h", t0 - pd.Timedelta(days=10), t1)
for a in sorted(fills["asset"].unique())}
daily = {}
for a, d in inv1h.items():
g = d.set_index("ts").resample("1D")
daily[a] = pd.DataFrame(dict(close=g["close"].last())).dropna()
rows = []
for _, f in fills.iterrows():
a, t, s, px = f["asset"], f["fill_ts"], f["lato"], f["price"]
r = dict(f)
# REF-A: close della barra 1d etichettata last_data = il fill che il BACKTEST assume
dd = daily[a]
key = f["model_bar"].tz_convert("UTC").normalize()
r["ref_A"] = float(dd.loc[key, "close"]) if key in dd.index else np.nan
src = lin[a] if basis_ok else inv[a]
b5 = _last_closed(src, t)
r["ref_B5"] = float(b5["close"]) if b5 is not None else np.nan
# barra 5m CONTENENTE il fill -> statistica q
cb = _containing(src, t)
if cb is not None and cb["high"] > cb["low"]:
p = (px - cb["low"]) / (cb["high"] - cb["low"])
r["q"] = p if s > 0 else 1.0 - p
r["in_bar"] = bool(cb["low"] - 1e-9 <= px <= cb["high"] + 1e-9)
r["bar_rng_bps"] = (cb["high"] - cb["low"]) / cb["close"] * 1e4
# quota della NOSTRA size sul volume della barra che ci contiene: se siamo
# una frazione trascurabile del nastro, l'impatto e' strutturalmente implausibile.
r["partecip"] = f["filled"] / cb["volume"] if cb["volume"] > 0 else np.nan
else:
r["q"], r["in_bar"], r["bar_rng_bps"], r["partecip"] = np.nan, None, np.nan, np.nan
# REF-B1h e REF-C VWAP dell'ora contenente
h0 = t.floor("h")
hb = src[(src["ts"] >= h0) & (src["ts"] < h0 + pd.Timedelta(hours=1))]
r["ref_C"] = float((hb["close"] * hb["volume"]).sum() / hb["volume"].sum()) \
if len(hb) and hb["volume"].sum() > 0 else np.nan
prev = src[src["ts"] + pd.Timedelta(minutes=5) <= h0]
r["ref_B1"] = float(prev.iloc[-1]["close"]) if len(prev) else np.nan
for k in ("A", "B5", "B1", "C"):
v = r.get(f"ref_{k}")
r[f"slip_{k}"] = s * (px - v) / v * 1e4 if v and np.isfinite(v) else np.nan
rows.append(r)
R = pd.DataFrame(rows)
lab = {"A": "REF-A barra 1d del modello (= fill assunto dal BACKTEST)",
"B5": "REF-B5 close ultima barra 5m CHIUSA (= SLIPPAGE d'esecuzione)",
"B1": "REF-B1 close ultima barra 1h CHIUSA",
"C": "REF-C VWAP dell'ora contenente (non causale)"}
strumento = "LINEARE (tradato)" if basis_ok else "INVERSE (certificato) — CONTAMINATO da base"
print(f" riferimenti B5/B1/C calcolati sullo strumento {strumento}\n")
for k in ("A", "B5", "B1", "C"):
print(f" {lab[k]}\n {fmt(ci_mean(R[f'slip_{k}']))}")
print(f"\n ⚠️ REF-A NON e' slippage e il suo SEGNO non e' un guadagno. La barra 1d etichettata")
print(f" `last_data` chiude fino a ~23h DOPO il fill: su questa finestra (BTC 66k->78k in")
print(f" 4 giorni) un trend-follower che compra alle 01:07 batte per costruzione la")
print(f" chiusura serale, ma il backtest tiene la posizione DA quella chiusura — e' una")
print(f" differenza di FASE, non un attrito. La domanda 'quanto costa la barra parziale'")
print(f" e' gia' stata misurata bene il 26/07 (r0726_tp01_partial_day, 24 ancore appaiate:")
print(f" ±0.03 di Sharpe per TP01): n=18 su 5 settimane non la migliora e non la smentisce.")
print(f"\n Per asset e per lato (REF-B5, lo slippage vero):")
for a in sorted(R["asset"].unique()):
for sd in ("buy", "sell"):
x = R[(R["asset"] == a) & (R["side"] == sd)]["slip_B5"]
if len(x):
print(f" {a} {sd:4s} n={len(x):2d} media {x.mean():+7.2f} med {x.median():+7.2f}")
# ---------------------------------------------------------------- 5. la statistica con potenza
print("\n[5] LA STATISTICA CON POTENZA — q, posizione del fill nella barra 5m\n" + "-" * 96)
q = R["q"].dropna()
dq = ci_mean(q)
print(f" q = (fill-low)/(high-low) per i BUY, 1-quello per i SELL. E[q]=0.5 senza impatto.")
print(f" media q {dq['mean']:.3f} IC95 [{dq['lo']:.3f}, {dq['hi']:.3f}] "
f"(med {dq['med']:.3f}, n={dq['n']})")
print(f" scostamento da 0.5: {dq['mean'] - 0.5:+.3f} (MDE dichiarata {mde_q:.3f})")
outside = (R["in_bar"] == False).sum()
print(f" fill FUORI dal range della propria barra: {outside}/{dq['n']} "
f"(un fill fuori sarebbe strumento o ora sbagliati)")
print(f" q=1.00 esatti (fill AL massimo = firma di impatto): {(q >= 0.999).sum()}/{dq['n']}"
f" · q=0.00 esatti: {(q <= 0.001).sum()}/{dq['n']}")
rng_med = float(R["bar_rng_bps"].median())
print(f" range mediano di barra 5m: {rng_med:.1f} bps -> lo scostamento misurato vale")
print(f" ~{(dq['mean'] - 0.5) * rng_med:+.2f} bps/lato "
f"[IC95 {(dq['lo'] - 0.5) * rng_med:+.2f}, {(dq['hi'] - 0.5) * rng_med:+.2f}]")
# -- null EMPIRICO: "0.5" e' un'assunzione (uniforme). Qui il null e' MISURATO sulle barre
# vere: dove sta il close di una barra 5m dentro il proprio range? Segno randomizzato
# per imitare l'orientamento al lato.
qc = []
for a in sorted(fills["asset"].unique()):
src = lin[a] if basis_ok else inv[a]
m = src[src["high"] > src["low"]]
qc.append(((m["close"] - m["low"]) / (m["high"] - m["low"])).to_numpy())
qc = np.concatenate(qc)
flip = RNG.random((N_BOOT, dq["n"])) < 0.5
draw = RNG.choice(qc, size=(N_BOOT, dq["n"]), replace=True)
null = np.where(flip, draw, 1.0 - draw).mean(axis=1)
pval = float((null >= dq["mean"]).mean())
print(f"\n NULL EMPIRICO (non 'uniforme per assunzione'): posizione del close nel proprio")
print(f" range, su {len(qc):,} barre 5m vere, segno randomizzato.")
print(f" null: media {null.mean():.3f} · sd {null.std():.3f} "
f"(l'uniforme darebbe sd {(1 / np.sqrt(12)) / np.sqrt(dq['n']):.3f})")
print(f" p(one-sided, impatto = q piu' alto del null) = {pval:.3f}"
f" -> {'IMPATTO RILEVATO' if pval < 0.05 else 'nessun impatto rilevabile'}")
# -- argomento FISICO, indipendente dal campione: quanto siamo grandi e quanto vale un tick
from src.live.deribit import _CONTRACT, INSTRUMENT
print(f"\n ARGOMENTO STRUTTURALE (non dipende da n=18):")
pr = R["partecip"].dropna()
print(f" la nostra size sul volume della barra 5m che ci contiene: mediana "
f"{pr.median() * 100:.2f}% · max {pr.max() * 100:.2f}%")
for a in sorted(R["asset"].unique()):
tick = _CONTRACT[INSTRUMENT[a]]["tick"]
px = float(R[R["asset"] == a]["price"].median())
print(f" {a}: tick {tick} su ~${px:,.0f} = {tick / px * 1e4:.4f} bps -> anche uno")
print(f" spread di 5 tick costerebbe {2.5 * tick / px * 1e4:.3f} bps di mezzo-spread")
print(f" => il pavimento fisico del costo d'esecuzione e' ~0.1 bps, DUE ordini di")
print(f" grandezza sotto il margine di {MARGIN_BPS_SIDE:.1f} bps. Perche' lo slippage")
print(f" sfondi il margine il libro dovrebbe CAMMINARE il book — e a <1% del nastro")
print(f" con 0/18 fill al massimo di barra non c'e' traccia che lo faccia.")
# ---------------------------------------------------------------- 6. conversione
print("\n[6] CONVERSIONE IN SHARPE/CAGR — curva PUBBLICATA, non ricalcolata\n" + "-" * 96)
print(f" r0726_fee_sensitivity: {DSHARPE_PER_BPS_SIDE:+.3f} Sharpe/bps-lato · "
f"{DCAGR_PER_BPS_SIDE:+.2f} pp CAGR/bps-lato (SKH01 ~4x piu' sensibile di TP01)")
b5 = ci_mean(R["slip_B5"])
qs_mean = (dq["mean"] - 0.5) * rng_med
qs_lo, qs_hi = (dq["lo"] - 0.5) * rng_med, (dq["hi"] - 0.5) * rng_med
for nm, m, lo, hi in (("REF-B5 (bps, senza potenza)", b5["mean"], b5["lo"], b5["hi"]),
("q normalizzata (con potenza)", qs_mean, qs_lo, qs_hi)):
print(f" {nm:<30} slip {m:+6.2f} bps -> dSharpe {DSHARPE_PER_BPS_SIDE * m:+.4f} "
f"· dCAGR {DCAGR_PER_BPS_SIDE * m:+.3f} pp "
f"[IC95 dSharpe {DSHARPE_PER_BPS_SIDE * hi:+.4f}..{DSHARPE_PER_BPS_SIDE * lo:+.4f}]")
print(f"\n Margine gia' pagato dal modello: {MARGIN_BPS_SIDE:.1f} bps/lato "
f"(backtest 10 bps RT - fee vera 7 bps RT).")
print(f" Slippage con potenza (q) dentro il margine? "
f"{'SI' if qs_hi <= MARGIN_BPS_SIDE else 'NON DIMOSTRATO'} "
f"(estremo alto IC95 {qs_hi:+.2f} vs {MARGIN_BPS_SIDE:.1f})")
# ---------------------------------------------------------------- 7. taglia e ora
print("\n[7] DIPENDENZA DA TAGLIA E ORA — il campione lo permette?\n" + "-" * 96)
from scipy import stats
sub = R.dropna(subset=["q"])
rho, p = stats.spearmanr(sub["notional"], sub["q"])
print(f" taglia ESEGUITA ${R['notional'].min():.0f}-${R['notional'].max():.0f} "
f"(mediana ${R['notional'].median():.0f}). NB: e' l'INCREMENTO mandato al venue, non il")
print(f" target di posizione (che arriva a $189): il libro riconcilia, non ri-apre.")
print(f" Spearman(notional, q) = {rho:+.3f} (p={p:.2f})")
big = sub[sub["notional"] >= sub["notional"].median()]["q"]
small = sub[sub["notional"] < sub["notional"].median()]["q"]
print(f" meta' grande n={len(big)} q={big.mean():.3f} · meta' piccola n={len(small)} "
f"q={small.mean():.3f} · differenza {big.mean() - small.mean():+.3f} "
f"(MDE su un confronto a meta' campione ~{2.80 * (1 / np.sqrt(12)) * np.sqrt(2 / (n / 2)):.3f})")
hrs = sorted(int(h) for h in R["fill_ts"].dt.hour.unique())
print(f" ore del giorno coperte: {hrs} ({len(hrs)} ore distinte su 24, "
f"{R['fill_ts'].dt.hour.value_counts().max()} fill nell'ora piu' popolata)")
print(f" => con n={n} e {len(hrs)} ore distinte, l'effetto ora NON e' stimabile: "
f"lo dichiaro non misurato, non 'assente'.")
# ---------------------------------------------------------------- 8. netting
print("\n[8] NETTING TP01/SKH01 — cosa NON e' misurabile da questi dati\n" + "-" * 96)
print(f" Il libro netta TP01 e SKH01 su UNO strumento: ogni fill e' la RISULTANTE, mai")
print(f" il trade di uno sleeve. Quindi lo slippage misurato e' quello del LIBRO, e non si")
print(f" puo' attribuire a TP01 o a SKH01 — proprio mentre SKH01 e' ~4x piu' sensibile.")
print(f" Il netting RIDUCE il turnover eseguito: {n} ordini in "
f"{(fills['fill_ts'].max() - fills['fill_ts'].min()).days} giorni, "
f"nozionale totale ${R['notional'].sum():,.0f}.")
print("\n" + "=" * 96)
out = SCRATCH / "r0822_slip_fills.csv"
SCRATCH.mkdir(parents=True, exist_ok=True)
R.to_csv(out, index=False)
print(f" dettaglio per fill: {out}")
print("=" * 96)
if __name__ == "__main__":
main()
+5 -3
View File
@@ -58,7 +58,8 @@ from r0725_statarb_basket_gate import basket_from_positions, pair_frames
from r0725_statarb_demean_skeptic import ret_from_pos # noqa: E402
ANN = np.sqrt(365.0)
DISCOVERY = pd.Timestamp("2026-07-25", tz="UTC") # ultima barra vista dalla scoperta
DISCOVERY = pd.Timestamp("2026-07-25", tz="UTC") # ultima barra vista dalla scoperta (PROVVISORIA)
CLOSED_AT_DISCOVERY = pd.Timestamp("2026-07-24", tz="UTC") # ultima barra CHIUSA quel giorno
CRONLOG = ROOT / "logs" / "cron_daily.log"
FWD = ROOT / "data" / "paper_xsr" / "returns.jsonl"
FWD_STATE = ROOT / "data" / "paper_xsr" / "state.json"
@@ -138,6 +139,8 @@ def t1_lenti():
for tag, (Pi, Si) in [
("OGGI pieno", (P, S)),
(f"tronc. {DISCOVERY.date()} (barra completa)", (_cut(P, DISCOVERY), _cut(S, DISCOVERY))),
("tronc. 2026-07-24 = ultima barra CHIUSA il 25/07",
(_cut(P, CLOSED_AT_DISCOVERY), _cut(S, CLOSED_AT_DISCOVERY))),
]:
rows.append((f"L1 gate {tag}", _stats(lens_L1(Pi, Si))))
rows.append((f"L2 scett. {tag}", _stats(lens_L2(Pi, Si))))
@@ -337,8 +340,7 @@ def t3_monitor():
print(" non entrano in nessun rendimento registrato.")
sh_a = float(a.mean() / a.std() * ANN)
sh_b = float(b.mean() / b.std() * ANN)
se = float(ANN / np.sqrt(len(a)) / np.sqrt(365) * np.sqrt(365))
se = float(np.sqrt(365.0 / len(a)))
se = float(np.sqrt(365.0 / len(a))) # SE di uno Sharpe annualizzato su n barre giornaliere
print(f"\n Sharpe della finestra forward: REGISTRATO {sh_a:+.2f} RICALCOLATO {sh_b:+.2f}")
print(f" ⚠ NESSUNO DEI DUE E' UNA LETTURA DEL GATE. Con {len(a)} barre l'errore standard di")
print(f" uno Sharpe annualizzato e' ~{se:.2f}: entrambi i numeri sono indistinguibili da 0 e")