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
+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)
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:
print("\n" + "=" * 78)
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:
"""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():
try:
return pd.read_parquet(cache)
except Exception:
pass
rows = []
for (asset, hr, exp), g in q.groupby(["asset", "hr", "exp"], sort=False):
c = g[g["option_type"] == "C"]
p = g[g["option_type"] == "P"]
if len(c) < 2 or len(p) < 2:
continue
c25 = _iv_at(c["ad"].to_numpy(), c["iv"].to_numpy(), 0.25)
p25 = _iv_at(p["ad"].to_numpy(), p["iv"].to_numpy(), 0.25)
if not (np.isfinite(c25) and np.isfinite(p25)):
continue
c50 = _iv_at(c["ad"].to_numpy(), c["iv"].to_numpy(), 0.50)
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 (
np.isfinite(c50) or np.isfinite(p50)) else np.nan
rows.append((asset, hr, exp, float(g["dte"].iloc[0]), c25, p25, atm,
g["ts"].max(), int(len(g))))
R = pd.DataFrame(rows, columns=["asset", "hr", "exp", "dte", "ivc25", "ivp25",
"ivatm", "ts_max", "n_gambe"])
"""Per (asset, ora, scadenza): IV a |delta|=0.25 su entrambe le ali e ATM a |delta|=0.50.
Vettorizzato per bracketing (nessun ciclo per gruppo): per ogni chiave si prende la riga con
|delta| massimo SOTTO il bersaglio e quella con |delta| minimo SOPRA, poi si interpola fra
le due. Se una delle due manca, la cella si scarta -> non si estrapola MAI (una IV a 25 delta
ricavata estrapolando dall'ala e' un numero inventato, e sarebbe proprio dove il segnale
sembrerebbe piu' forte)."""
KEY = ["asset", "hr", "exp", "option_type"]
def bracket(d: pd.DataFrame, tgt: float) -> pd.DataFrame:
d = d[["asset", "hr", "exp", "option_type", "ad", "iv"]]
lo = d[d["ad"] <= tgt].sort_values("ad").groupby(KEY, sort=False, observed=True).tail(1)
hi = d[d["ad"] >= tgt].sort_values("ad").groupby(KEY, sort=False, observed=True).head(1)
m = lo.merge(hi, on=KEY, suffixes=("_lo", "_hi"))
span = (m["ad_hi"] - m["ad_lo"]).to_numpy()
w = np.where(span > 1e-12, (tgt - m["ad_lo"].to_numpy()) / np.where(span > 1e-12, span, 1.0), 0.0)
m["iv_t"] = m["iv_lo"].to_numpy() * (1 - w) + m["iv_hi"].to_numpy() * w
return m[KEY + ["iv_t"]]
# bastano le scadenze che possono ABBRACCIARE 7 o 30 giorni
d = q[q["dte"] <= 70.0]
b25 = bracket(d, 0.25).pivot_table(index=["asset", "hr", "exp"], columns="option_type",
values="iv_t", observed=True)
b50 = bracket(d, 0.50).pivot_table(index=["asset", "hr", "exp"], columns="option_type",
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["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
@@ -261,8 +270,8 @@ def daily_signal(S: pd.DataFrame, df: pd.DataFrame) -> pd.DataFrame:
bpd = A.bars_per_day(df)
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))
left = pd.DataFrame({"t_ms": chiusura.astype("datetime64[ms]").astype("int64")})
right = pd.DataFrame({"t_ms": pd.DatetimeIndex(S["ts_max"]).astype("datetime64[ms]").astype("int64"),
left = pd.DataFrame({"t_ms": _ms(chiusura)})
right = pd.DataFrame({"t_ms": _ms(pd.DatetimeIndex(S["ts_max"])),
"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")
m = pd.merge_asof(left.sort_values("t_ms"), right, on="t_ms", direction="backward",