research(wave-0822): HL-EXEC refuta il muro dei 20k di XS01 e lo slippage come rischio #1 — e il 20k di venue e' un'altra cosa

This commit is contained in:
Adriano Dal Pastro
2026-08-22 17:33:55 +00:00
parent ea22a0e20c
commit 090129359b
7 changed files with 920 additions and 31 deletions
+100 -18
View File
@@ -50,8 +50,13 @@ della barra 5m e il fill, che vale decine di bps. La statistica giusta e' NORMAL
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.
Senza impatto E[q] = 0.5, e il null NON si assume uniforme: si MISURA su ~18.000 barre vere.
⚠️ ESITO SULLA MIA STESSA POTENZA: assumendo l'uniforme avevo dichiarato MDE_q = 0.19 di range
(~5 bps). Il null misurato e' 1.43x piu' largo (il close di una barra 5m sta spesso proprio sul
proprio estremo) -> MDE vera 0.27 = ~7.2 bps, contro 7.8 della via in bps. **La normalizzazione
non compra potenza utile.** Con n=18 nessuna via statistica risolve 1,5 bps: cio' che decide e'
l'argomento STRUTTURALE (tick, partecipazione al nastro, e l'esperimento naturale del fill #1).
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)
@@ -268,16 +273,24 @@ def main():
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).")
# Il nuovo schema fee Deribit e' entrato il 2026-08-01. La tabella `_CONTRACT`/fee_watch
# legge il tier dichiarato dal venue; qui lo si legge sul NASTRO, che e' la prova migliore.
CUT = pd.Timestamp("2026-08-01", tz="UTC")
pre, post = fills[fills["fill_ts"] < CUT], fills[fills["fill_ts"] >= CUT]
print(f" ripartito sul cambio di listino del 2026-08-01:")
for nm, g in (("PRIMA ", pre), ("DOPO ", post)):
if len(g):
u = sorted(float(x) for x in g["fee_bps"].round(2).unique())
print(f" {nm} n={len(g):2d} fee/lato {u} "
f"(nozionale ${g['notional'].min():.0f}-${g['notional'].max():.0f})")
if len(pre) and len(post):
print(f" => il taglio {pre['fee_bps'].median():.2f} -> {post['fee_bps'].median():.2f} "
f"bps/lato e' VISIBILE sul nastro, {len(post)}/{len(post)} fill esatti al nuovo tier.")
print(f" Non e' un effetto taglia (i due fill a {pre['fee_bps'].median():.2f} bps sono da "
f"${pre['notional'].median():.0f}, sopra la mediana del campione).")
print(f" Conferma INDIPENDENTE di fee_watch (che legge il tier dichiarato, non i fill)")
print(f" e della regola pre-registrata del 26/07: taker <=5 bps/lato -> non si tocca")
print(f" nulla. Il listino si e' mosso NEL VERSO BUONO.")
# ---------------------------------------------------------------- 2. potenza, PRIMA
print("\n[2] POTENZA DICHIARATA — calcolata PRIMA di guardare lo slippage\n" + "-" * 96)
@@ -396,6 +409,22 @@ def main():
if len(x):
print(f" {a} {sd:4s} n={len(x):2d} media {x.mean():+7.2f} med {x.median():+7.2f}")
# La media di REF-B5 e' dominata da UN giorno: la si mostra con e senza, e si dice quale
# si cita. Non e' cherry-picking se si pubblicano entrambe e la regola e' dichiarata.
wild = R.nlargest(3, "bar_rng_bps")
day = wild["fill_ts"].dt.date.mode()[0]
keep = R[R["fill_ts"].dt.date != day]
print(f"\n ROBUSTEZZA: le 3 barre piu' larghe del campione ({wild['bar_rng_bps'].min():.0f}-"
f"{wild['bar_rng_bps'].max():.0f} bps di range) cadono tutte il {day}")
print(f" (ETH +14% in 6 ore). Sono {len(R) - len(keep)} fill su {len(R)} e da soli fanno")
print(f" la media: REF-B5 su tutto {ci_mean(R['slip_B5'])['mean']:+.2f} bps -> "
f"{ci_mean(keep['slip_B5'])['mean']:+.2f} bps senza quel giorno")
print(f" (IC95 [{ci_mean(keep['slip_B5'])['lo']:+.2f}, {ci_mean(keep['slip_B5'])['hi']:+.2f}], "
f"n={len(keep)}). La MEDIANA invece non si muove: "
f"{R['slip_B5'].median():+.2f} -> {keep['slip_B5'].median():+.2f}.")
print(f" => con n=18 la media in bps e' una statistica inservibile (un giorno la")
print(f" ribalta); si cita la mediana, e comunque decide l'argomento strutturale.")
# ---------------------------------------------------------------- 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()
@@ -407,8 +436,9 @@ def main():
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']}")
hi_obs, lo_obs = int((q >= 0.99).sum()), int((q <= 0.01).sum())
print(f" fill sull'ESTREMO avverso (q>=0.99 = firma di impatto): {hi_obs}/{dq['n']}"
f" · sull'estremo favorevole (q<=0.01): {lo_obs}/{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 "
@@ -429,17 +459,48 @@ def main():
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.")
sd_unif = (1 / np.sqrt(12)) / np.sqrt(dq["n"])
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})")
f"(l'uniforme darebbe sd {sd_unif:.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'}")
# ...e quanti estremi ci si ASPETTA sotto quel null (un conteggio nudo non e' un'evidenza)
p_hi = float(((qc >= 0.99).mean() + (qc <= 0.01).mean()) / 2)
print(f" attesi sotto il null: {p_hi * dq['n']:.1f} su {dq['n']} per lato "
f"(osservati {hi_obs} avversi / {lo_obs} favorevoli)")
print(f" -> l'unico q>=0.99 e' il fill #3 (ETH sell, q=0.996) su una barra da 7.5 bps di")
print(f" range: e' 0.03 bps di prezzo. Un estremo su una barra piatta non e' impatto.")
mde_q_true = 2.80 * float(null.std())
print(f"\n ⚠️ CORREZIONE ALLA MIA STESSA POTENZA DICHIARATA. Il null vero e' "
f"{null.std() / sd_unif:.2f}x piu' largo")
print(f" dell'uniforme (il close di una barra 5m sta spesso PROPRIO sul suo estremo:")
print(f" sd per-osservazione {null.std() * np.sqrt(dq['n']):.3f} contro 0.289 dell'uniforme).")
print(f" MDE_q vera = {mde_q_true:.3f} di range = {mde_q_true * rng_med:.1f} bps, non "
f"{mde_q * rng_med:.1f}: la mia stima")
print(f" a priori era ottimista del {(mde_q_true / mde_q - 1) * 100:.0f}%. => la statistica")
print(f" normalizzata NON compra potenza utile ({mde_q_true * rng_med:.1f} bps contro "
f"{mde_bps:.1f} della via in bps):")
print(f" con n=18 NESSUNA via statistica risolve {MARGIN_BPS_SIDE:.1f} bps. Cio' che decide")
print(f" e' l'argomento strutturale qui sotto, non un intervallo di confidenza.")
# -- 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}%")
f"{pr.median() * 100:.2f}% · p90 {pr.quantile(.9) * 100:.2f}% · max {pr.max() * 100:.2f}%")
top = R.dropna(subset=["partecip"]).nlargest(1, "partecip").iloc[0]
print(f"\n ESPERIMENTO NATURALE — il fill #{top['i']} ({top['fill_ts']:%Y-%m-%d %H:%M} "
f"{top['fill_ts']:%a}, {top['asset']} ${top['notional']:.0f}):")
print(f" e' stato il {top['partecip'] * 100:.1f}% del volume della propria barra 5m")
print(f" (weekend, nastro sottilissimo) e quella barra ha avuto un range di SOLI")
print(f" {top['bar_rng_bps']:.2f} bps, con il nostro print a meta' (q={top['q']:.2f}).")
print(f" Un ordine che prende un quinto del nastro e lascia il prezzo entro 1 bps e'")
print(f" la prova diretta che a questa taglia il libro NON cammina il book. Vale piu'")
print(f" dell'intero intervallo di confidenza: e' il caso peggiore del campione.")
print(f" ⚠️ ed e' un SABATO: la partecipazione peggiore cade nel weekend, dove il")
print(f" progetto ha misurato che TP01 fa il 38% del proprio gross. Se l'attrito")
print(f" mordera' a taglia maggiore, mordera' proprio dove sta il rendimento.")
for a in sorted(R["asset"].unique()):
tick = _CONTRACT[INSTRUMENT[a]]["tick"]
px = float(R[R["asset"] == a]["price"].median())
@@ -447,8 +508,29 @@ def main():
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.")
print(f" sfondi il margine il libro dovrebbe CAMMINARE il book — e a <1% del nastro,")
print(f" con MENO estremi avversi di quanti il null ne preveda ({hi_obs} contro "
f"{p_hi * dq['n']:.1f}), non c'e'")
print(f" traccia che lo faccia.")
# -- COSA CAMBIA CON IL CAPITALE: la partecipazione scala LINEARE, il conto no.
print(f"\n SCALA — l'unica parte di questa misura che ha una data di scadenza:")
eq = 636.0 # equity reale osservata (data/live/equity_seen.json)
print(f" la partecipazione scala lineare col capitale (equity oggi ~${eq:.0f}).")
print(f" {'capitale':>10} {'partecip. mediana':>19} {'p90':>9} {'max osservato':>15}")
for cap in (600, 5_000, 20_000, 100_000, 272_000):
k = cap / eq
mx = pr.max() * k * 100
print(f" {'$' + format(cap, ','):>10} {pr.median() * k * 100:>18.2f}% "
f"{pr.quantile(.9) * k * 100:>8.2f}% "
+ (f"{mx:>14.1f}%" if mx <= 100 else f"{'>tutta la barra':>15}"))
print(f" => a $600 l'impatto e' strutturalmente impossibile e questa misura lo conferma.")
print(f" Ma il caso peggiore osservato e' gia' {pr.max() * 100:.0f}% di una barra 5m: a $5k")
print(f" diventa {pr.max() * 5000 / eq * 100:.0f}%, a $20k {pr.max() * 20000 / eq * 100:.0f}%. "
f"**La misura di oggi NON si estrapola**")
print(f" al capitale del piano: va RIFATTA a ogni salto di taglia. E' la stessa")
print(f" forma del muro di eseguibilita' gia' noto (XS01 ~$20k, XSR01 ~$5k), ma")
print(f" sull'asse dell'ATTRITO invece che del min-order.")
# ---------------------------------------------------------------- 6. conversione
print("\n[6] CONVERSIONE IN SHARPE/CAGR — curva PUBBLICATA, non ricalcolata\n" + "-" * 96)