research(wave-0822): FLOW-SQUEEZE scartato — e il filone funding si chiude sul QUARTO lato (affollamento)

This commit is contained in:
Adriano Dal Pastro
2026-08-22 17:12:42 +00:00
parent bd5b634209
commit f4b14f305b
2 changed files with 67 additions and 32 deletions
+26
View File
@@ -10,6 +10,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato.
| 2 | GROWTH-POLICY | **LEAD** (condizione, non data) | il libro gira al **7% di Kelly** e raccoglie il **15%** della crescita massima in log; il gradino eseguibile 1.00x->1.25-1.50x vale 14.7a -> 12.9-11.6a al muro. Ma un solo giorno -10% all'anno porta k* da ~10x a **2x**: non si decide su un conto vero con la lente close-only | | 2 | GROWTH-POLICY | **LEAD** (condizione, non data) | il libro gira al **7% di Kelly** e raccoglie il **15%** della crescita massima in log; il gradino eseguibile 1.00x->1.25-1.50x vale 14.7a -> 12.9-11.6a al muro. Ma un solo giorno -10% all'anno porta k* da ~10x a **2x**: non si decide su un conto vero con la lente close-only |
| 12 | ORTHO-SCREEN | **SCARTATO 7/7** | nessuna famiglia arriva a ADDS+DSR; e il motivo e' **aritmetico**: su 168 trial il massimo atteso dal rumore e' Sharpe **1.572**, SOPRA il soffitto direzionale misurato (~1.3) -> uno screen largo su BTC/ETH direzionale **non puo'** passare un DSR, per costruzione | | 12 | ORTHO-SCREEN | **SCARTATO 7/7** | nessuna famiglia arriva a ADDS+DSR; e il motivo e' **aritmetico**: su 168 trial il massimo atteso dal rumore e' Sharpe **1.572**, SOPRA il soffitto direzionale misurato (~1.3) -> uno screen largo su BTC/ETH direzionale **non puo'** passare un DSR, per costruzione |
| 3 | XS-LITE | **SCARTATO** (come sleeve) + **1 soglia pubblicata falsificata** | concentrare XS01 non crea uno sleeve nuovo (corr 0.81-0.96 col canonico, DSR FAIL, de-levering non superato) — ma il muro *"XS01 serve ~$20k"* e' misurato su una diagnostica di TURNOVER: il ribilancio vero smette di passare sotto **~$109 di sleeve (~$730 di book), 27x piu' in basso** | | 3 | XS-LITE | **SCARTATO** (come sleeve) + **1 soglia pubblicata falsificata** | concentrare XS01 non crea uno sleeve nuovo (corr 0.81-0.96 col canonico, DSR FAIL, de-levering non superato) — ma il muro *"XS01 serve ~$20k"* e' misurato su una diagnostica di TURNOVER: il ribilancio vero smette di passare sotto **~$109 di sleeve (~$730 di book), 27x piu' in basso** |
| 10 | FLOW-SQUEEZE | **SCARTATO** (2 motivi indipendenti) | meta' dell'ipotesi **non esiste nel dato** (`liquidation_*_risk` = una sola categoria in 17.229/17.229 righe); l'altra meta' e' sotto la propria soglia di potenza **dichiarata prima** (MDE 1,056% vs effetto 0,402%) ed e' comunque **morta alla nascita** (la colonna non e' ricostruibile) |
## Note che sopravvivono ai singoli filoni ## Note che sopravvivono ai singoli filoni
@@ -118,3 +119,28 @@ sta verificando i minimi veri del venue.
`paper_xsr` da' **2.23**; 1.82 non e' nessuna delle due. Spiegazione piu' probabile: `data/raw` e' `paper_xsr` da' **2.23**; 1.82 non e' nessuna delle due. Spiegazione piu' probabile: `data/raw` e'
gitignored e il cron riscrive i parquet HL ogni notte -> **stesso codice, dati diversi** (identico gitignored e il cron riscrive i parquet HL ogni notte -> **stesso codice, dati diversi** (identico
allo scoperto del 07/08 su GTAA/TLT). **La decisione del 23/10 poggia su quel numero.** allo scoperto del 07/08 su GTAA/TLT). **La decisione del 23/10 poggia su quel numero.**
### 10 — FLOW-SQUEEZE (r0822_flow_squeeze.py, 118 trial dichiarati, griglia RIDOTTA per budget)
📌 **Sottoprodotto con potenza vera, e chiude un filone: il funding non predice ne' direzione ne'
volatilita'** — misurato su **53.430 ore / 3 anni** di funding Hyperliquid (li' la potenza c'e'):
eccesso |<=0,35%| a 24h con **segno instabile fra anni**, |ritorno| futuro piatto e non monotono.
**Il filone funding, gia' chiuso su 3 lati come livello di carry, si puo' dichiarare chiuso anche
come proxy di AFFOLLAMENTO. Quarto lato.**
- Il "segnale" migliore (Sharpe 3,25 su 90 giorni) e' **il massimo di 18 celle**: DSR **0,103**
(0,004 su 118 trial), `implausible_sharpe=True`, e su 114 giorni **il massimo atteso per caso e'
Sharpe 5,40** -> il candidato sta **sotto il proprio null**. Il 71% del P&L e' in giugno, il 60%
in **3 giorni su 114**.
- Il null "statica travestita" e' superato (sempre-short = 0,67), ma quello di **timing casuale
location-matched** e' al 99,7° pctl **che pero' e' il massimo di 18** -> P(per caso) = 0,06:
**non superato dopo il conto dei trial**. Esempio pulito di come si legge un percentile alto.
- ⚠️ **Ricostruibilita' MISURATA, non assunta:** l'OI della catena (opzioni) contro la colonna dello
snapshot (perpetual) da' **Pearson +0,03 BTC / +0,21 ETH** — non e' la stessa grandezza.
- 💡 **Azione possibile, costo quasi nullo (NON eseguita, tocca il cron di produzione):** accendere
un collettore orario dell'**OI dei perpetual Deribit** (una GET/ora, API pubblica). Non ripara
questo filone, ma da quel giorno la storia comincia; oggi ripartirebbe da zero. Il criterio di
falsificazione e' gia' scritto dall'agente: >=12 mesi, eccesso oltre la MDE (~0,3%) e presente in
>=3 trimestri su 4.
- ⚠️ **Distinzione metodologica utile** (nata contraddicendo il suggerimento del coordinatore): qui
il vincolo binding NON e' la concentrazione degli episodi (86-87 episodi corti, i primi 8 coprono
solo il 20-25% delle ore) ma la **lunghezza del calendario** (90 giorni). *Ampiezza scarsa* e
*calendario corto* sono due diagnosi diverse e si curano in modi diversi: piu' episodi vs piu' tempo.
+41 -32
View File
@@ -68,6 +68,12 @@ def tick_of(prezzo: np.ndarray) -> np.ndarray:
return np.where(np.asarray(prezzo, float) < 0.005, 0.0001, 0.0005) return np.where(np.asarray(prezzo, float) < 0.005, 0.0001, 0.0005)
def _ms(ix: pd.DatetimeIndex) -> np.ndarray:
"""Epoca esplicita in millisecondi. Lezione 01/07: `.view("int64")` (e `.astype`) su un
DatetimeIndex tz-aware non-ns sbaglia scala o solleva -> merge_asof broadcasta in silenzio."""
return pd.DatetimeIndex(ix).tz_convert("UTC").tz_localize(None).astype("datetime64[ms]").astype("int64").to_numpy()
def rule(t: str) -> None: def rule(t: str) -> None:
print("\n" + "=" * 78) print("\n" + "=" * 78)
print(t) print(t)
@@ -142,38 +148,41 @@ def _iv_at(ad: np.ndarray, iv: np.ndarray, tgt: float) -> float:
def build_smile(q: pd.DataFrame) -> pd.DataFrame: def build_smile(q: pd.DataFrame) -> pd.DataFrame:
"""Per (asset, ora, scadenza): IV a |delta|=0.25 su entrambe le ali e ATM a |delta|=0.50.""" """Per (asset, ora, scadenza): IV a |delta|=0.25 su entrambe le ali e ATM a |delta|=0.50.
cache = SCRATCH / "r0822_smile.parquet"
if cache.exists(): Vettorizzato per bracketing (nessun ciclo per gruppo): per ogni chiave si prende la riga con
try: |delta| massimo SOTTO il bersaglio e quella con |delta| minimo SOPRA, poi si interpola fra
return pd.read_parquet(cache) le due. Se una delle due manca, la cella si scarta -> non si estrapola MAI (una IV a 25 delta
except Exception: ricavata estrapolando dall'ala e' un numero inventato, e sarebbe proprio dove il segnale
pass sembrerebbe piu' forte)."""
rows = [] KEY = ["asset", "hr", "exp", "option_type"]
for (asset, hr, exp), g in q.groupby(["asset", "hr", "exp"], sort=False):
c = g[g["option_type"] == "C"] def bracket(d: pd.DataFrame, tgt: float) -> pd.DataFrame:
p = g[g["option_type"] == "P"] d = d[["asset", "hr", "exp", "option_type", "ad", "iv"]]
if len(c) < 2 or len(p) < 2: lo = d[d["ad"] <= tgt].sort_values("ad").groupby(KEY, sort=False, observed=True).tail(1)
continue hi = d[d["ad"] >= tgt].sort_values("ad").groupby(KEY, sort=False, observed=True).head(1)
c25 = _iv_at(c["ad"].to_numpy(), c["iv"].to_numpy(), 0.25) m = lo.merge(hi, on=KEY, suffixes=("_lo", "_hi"))
p25 = _iv_at(p["ad"].to_numpy(), p["iv"].to_numpy(), 0.25) span = (m["ad_hi"] - m["ad_lo"]).to_numpy()
if not (np.isfinite(c25) and np.isfinite(p25)): w = np.where(span > 1e-12, (tgt - m["ad_lo"].to_numpy()) / np.where(span > 1e-12, span, 1.0), 0.0)
continue m["iv_t"] = m["iv_lo"].to_numpy() * (1 - w) + m["iv_hi"].to_numpy() * w
c50 = _iv_at(c["ad"].to_numpy(), c["iv"].to_numpy(), 0.50) return m[KEY + ["iv_t"]]
p50 = _iv_at(p["ad"].to_numpy(), p["iv"].to_numpy(), 0.50)
atm = np.nanmean([x for x in (c50, p50) if np.isfinite(x)]) if ( # bastano le scadenze che possono ABBRACCIARE 7 o 30 giorni
np.isfinite(c50) or np.isfinite(p50)) else np.nan d = q[q["dte"] <= 70.0]
rows.append((asset, hr, exp, float(g["dte"].iloc[0]), c25, p25, atm, b25 = bracket(d, 0.25).pivot_table(index=["asset", "hr", "exp"], columns="option_type",
g["ts"].max(), int(len(g)))) values="iv_t", observed=True)
R = pd.DataFrame(rows, columns=["asset", "hr", "exp", "dte", "ivc25", "ivp25", b50 = bracket(d, 0.50).pivot_table(index=["asset", "hr", "exp"], columns="option_type",
"ivatm", "ts_max", "n_gambe"]) values="iv_t", observed=True)
R = pd.DataFrame(index=b25.index)
R["ivc25"] = b25.get("C")
R["ivp25"] = b25.get("P")
R = R.dropna(subset=["ivc25", "ivp25"]) # servono ENTRAMBE le ali, sempre
R["ivatm"] = b50.reindex(R.index)[[c for c in ("C", "P") if c in b50.columns]].mean(axis=1)
agg = d.groupby(["asset", "hr", "exp"], sort=False, observed=True).agg(
dte=("dte", "first"), ts_max=("ts", "max"), n_gambe=("iv", "size"))
R = R.join(agg, how="left").reset_index()
R["rr"] = R["ivc25"] - R["ivp25"] R["rr"] = R["ivc25"] - R["ivp25"]
R["bf"] = 0.5 * (R["ivc25"] + R["ivp25"]) - R["ivatm"] R["bf"] = 0.5 * (R["ivc25"] + R["ivp25"]) - R["ivatm"]
try:
SCRATCH.mkdir(parents=True, exist_ok=True)
R.to_parquet(cache)
except Exception:
pass
return R return R
@@ -261,8 +270,8 @@ def daily_signal(S: pd.DataFrame, df: pd.DataFrame) -> pd.DataFrame:
bpd = A.bars_per_day(df) bpd = A.bars_per_day(df)
chiusura = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) chiusura = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
chiusura = chiusura + (pd.Timedelta("24h") - pd.Timedelta("1h") if bpd == 1 else pd.Timedelta(0)) chiusura = chiusura + (pd.Timedelta("24h") - pd.Timedelta("1h") if bpd == 1 else pd.Timedelta(0))
left = pd.DataFrame({"t_ms": chiusura.astype("datetime64[ms]").astype("int64")}) left = pd.DataFrame({"t_ms": _ms(chiusura)})
right = pd.DataFrame({"t_ms": pd.DatetimeIndex(S["ts_max"]).astype("datetime64[ms]").astype("int64"), right = pd.DataFrame({"t_ms": _ms(pd.DatetimeIndex(S["ts_max"])),
"rr": S["rr"].to_numpy(), "rr_n": S["rr_n"].to_numpy(), "rr": S["rr"].to_numpy(), "rr_n": S["rr_n"].to_numpy(),
"bf": S["bf"].to_numpy(), "atm": S["atm"].to_numpy()}).sort_values("t_ms") "bf": S["bf"].to_numpy(), "atm": S["atm"].to_numpy()}).sort_values("t_ms")
m = pd.merge_asof(left.sort_values("t_ms"), right, on="t_ms", direction="backward", m = pd.merge_asof(left.sort_values("t_ms"), right, on="t_ms", direction="backward",