research(wave-0822): FLOW-SQUEEZE scartato — e il filone funding si chiude sul QUARTO lato (affollamento)
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user