File size: 2,323 Bytes
6b66ac0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | """Unusual options activity factor.
Methodology:
For each (kind, moneyness_bucket) over the last ``LOOKBACK_DAYS``:
vol_oi = volume / open_interest
z_vol_oi = (today's vol_oi - mean) / std (over lookback window)
Final score is the vol-weighted sum of z-scores, with far-OTM calls
weighted heaviest (because they're the "lottery ticket" signal most
associated with informed buying):
OPT = + z_call_otm * 1.0
+ z_call_atm * 0.6
+ z_call_itm * 0.3
- z_put_otm * 1.0
- z_put_atm * 0.6
- z_put_itm * 0.3
Output is a single float.
"""
from __future__ import annotations
from typing import Optional
import numpy as np
import pandas as pd
from .factor_sources import get_data_source
LOOKBACK_DAYS = 20
WEIGHTS = {
("call", "otm"): +1.0,
("call", "atm"): +0.6,
("call", "itm"): +0.3,
("put", "otm"): -1.0,
("put", "atm"): -0.6,
("put", "itm"): -0.3,
}
def _zscore(series: pd.Series) -> float:
"""Z-score of the last value against the rest. Returns 0 if degenerate."""
if len(series) < 3:
return 0.0
s = series.astype(float)
if s.std() == 0 or not np.isfinite(s.std()):
return 0.0
return float((s.iloc[-1] - s.mean()) / s.std())
def compute_options_factor(
ticker: str,
source=None,
lookback_days: int = LOOKBACK_DAYS,
) -> float:
"""Compute the unusual options activity factor for ``ticker``."""
if source is None:
source = get_data_source()
df = source.get_options_history(ticker, lookback_days=lookback_days)
if df is None or df.empty:
return 0.0
# vol_oi per (date, kind, moneyness)
df = df.copy()
df["vol_oi"] = df["volume"] / df["oi"].replace(0, np.nan)
df = df.dropna(subset=["vol_oi"])
score = 0.0
for (kind, bucket), w in WEIGHTS.items():
sub = df[(df["kind"] == kind) & (df["moneyness"] == bucket)]
if sub.empty:
continue
sub = sub.sort_values("date")
z = _zscore(sub["vol_oi"])
score += w * z
return float(np.clip(score, -5.0, 5.0))
def compute_options_factors(
tickers: list[str],
source=None,
) -> dict[str, float]:
if source is None:
source = get_data_source()
return {t: compute_options_factor(t, source) for t in tickers}
|