Spaces:
Sleeping
Sleeping
File size: 7,156 Bytes
7a8d58d | 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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | """
LEAPS Scanner — S&P 100 tabanlı concurrent LEAPS anomali tarama.
Yakın zamanda yüksek miktarda LEAPS opsiyonu alınan hisseleri tespit eder.
Kaynak: yfinance (ücretsiz, ~15-20 dakika gecikmeli veri)
Strateji: S&P 100 ticker listesini concurrent tarayarak volume/OI > 2x LEAPS spike yakalar.
"""
import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Optional
# ── Constants ──────────────────────────────────────────────────────────
LEAPS_MIN_DAYS = 365 # Vade > 1 yıl = LEAPS
UNUSUAL_VOL_OI_MIN = 2.0 # volume/openInterest >= 2x = anormal
MAX_WORKERS = 10 # Concurrent thread sayısı
TIMEOUT_PER_TICKER = 15 # Ticker başına saniye timeout
# S&P 100 ticker listesi (OEX — 100 büyük hacimli hisse)
SP100_TICKERS = [
"AAPL", "MSFT", "GOOGL", "GOOG", "AMZN", "NVDA", "TSLA", "META",
"BRK-B", "JPM", "V", "JNJ", "WMT", "PG", "MA", "UNH", "HD", "DIS",
"PYPL", "BAC", "XOM", "INTC", "VZ", "ADBE", "NFLX", "CRM", "CSCO",
"PFE", "ABT", "ACN", "TMO", "AVGO", "DHR", "NKE", "TXN", "QCOM",
"COST", "NEE", "LIN", "PM", "BMY", "HON", "UNP", "LOW", "IBM",
"RTX", "SBUX", "AMD", "INTU", "GS", "CAT", "MDT", "AMGN", "BLK",
"GILD", "ADI", "ISRG", "BKNG", "T", "PLTR", "NOW", "SPGI", "MO",
"ELV", "SYK", "ZTS", "ADP", "MMC", "CB", "CI", "SO", "DUK",
"BDX", "TMUS", "SCHW", "EOG", "REGN", "LRCX", "PGR", "APD", "CL",
"SHW", "SLB", "ETN", "AON", "ITW", "BSX", "CME", "HUM", "MRNA",
"ABNB", "ORLY", "ATVI", "MCO", "KLAC", "PANW", "SNPS", "CDNS",
"FTNT", "NXPI", "ADM", "KDP", "MNST", "MCHP", "CTAS", "PAYX",
"AEP", "EXC", "AZN", "LHX", "NOC", "STZ", "KHC", "MAR", "DXCM",
]
# ── Helpers ────────────────────────────────────────────────────────────
def _safe_ratio(num, den, default=0.0):
if den == 0 or pd.isna(den):
return default
return float(num) / float(den)
def _is_leaps(expiry_date_str: str) -> bool:
"""Vade > 365 gün ise LEAPS."""
try:
dt = datetime.strptime(expiry_date_str, "%Y-%m-%d")
return (dt - datetime.today()).days >= LEAPS_MIN_DAYS
except ValueError:
return False
def scan_ticker_leaps(sym: str) -> Optional[dict]:
"""
Tek ticker tarama: LEAPS opsiyonlarda anormal hacim/OI spike ara.
Sonuç bulunamazsa None döner.
"""
try:
stock = yf.Ticker(sym)
expiry_dates = stock.options
if not expiry_dates:
return None
# LEAPS vadelerini belirle
leaps_expiries = [exp for exp in expiry_dates if _is_leaps(exp)]
if not leaps_expiries:
return None
total_leaps_call_vol = 0
total_leaps_put_vol = 0
total_leaps_call_oi = 0
total_leaps_put_oi = 0
unusual_spikes = []
for exp in leaps_expiries:
try:
chain = stock.option_chain(exp)
except Exception:
continue
for is_call, df in [(True, chain.calls), (False, chain.puts)]:
if df.empty:
continue
active = df[df["volume"] > 0].copy()
if active.empty:
continue
# Volume/OI ratio hesapla
active["vol_oi"] = active.apply(
lambda r: _safe_ratio(r.get("volume", 0), r.get("openInterest", 0)),
axis=1,
)
# Toplam hacim/OI
vol_sum = int(active["volume"].sum())
oi_sum = int(active["openInterest"].sum())
if is_call:
total_leaps_call_vol += vol_sum
total_leaps_call_oi += oi_sum
else:
total_leaps_put_vol += vol_sum
total_leaps_put_oi += oi_sum
# Anormal spike'lar: vol/OI >= threshold
spikes = active[active["vol_oi"] >= UNUSUAL_VOL_OI_MIN]
for _, row in spikes.iterrows():
unusual_spikes.append({
"type": "LEAPS CALL" if is_call else "LEAPS PUT",
"expiry": exp,
"strike": float(row.get("strike", 0)),
"volume": int(row.get("volume", 0)),
"openInterest": int(row.get("openInterest", 0)),
"vol_oi_ratio": round(float(row.get("vol_oi", 0)), 1),
"lastPrice": float(row.get("lastPrice", 0)) if pd.notna(row.get("lastPrice")) else 0,
"impliedVolatility": round(float(row.get("impliedVolatility", 0)), 4) if pd.notna(row.get("impliedVolatility")) else 0,
"signal": "BULLISH" if is_call else "BEARISH",
})
if not unusual_spikes:
return None
# Özet hesapla
total_vol = total_leaps_call_vol + total_leaps_put_vol
cp_ratio = _safe_ratio(total_leaps_call_vol, total_leaps_put_vol)
# En büyük spike'ı bul
top_spike = max(unusual_spikes, key=lambda x: x["volume"])
return {
"ticker": sym,
"total_leaps_call_vol": total_leaps_call_vol,
"total_leaps_put_vol": total_leaps_put_vol,
"total_leaps_vol": total_vol,
"call_put_ratio": round(cp_ratio, 2),
"unusual_count": len(unusual_spikes),
"top_spike": top_spike,
"all_spikes": unusual_spikes[:10], # Max 10 spike
"leaps_expiries_count": len(leaps_expiries),
"nearest_leaps_expiry": leaps_expiries[0] if leaps_expiries else None,
"scan_time": datetime.utcnow().isoformat(),
}
except Exception:
return None
def scan_all_sp100_leaps(max_tickers: int = 100) -> list[dict]:
"""
S&P 100 ticker listesini concurrent tarayarak LEAPS anomali bulur.
Sonuçları toplam LEAPS hacmine göre sıralı döndürür.
Parametre:
max_tickers: Taranacak maks ticker sayısı (varsayılan 100, S&P 100)
Dönüş:
list[dict]: LEAPS anomali bulunan ticker'lar, hacme göre sıralı
"""
tickers = SP100_TICKERS[:max_tickers]
results = []
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_to_sym = {
executor.submit(scan_ticker_leaps, sym): sym
for sym in tickers
}
for future in as_completed(future_to_sym, timeout=120):
try:
result = future.result(timeout=TIMEOUT_PER_TICKER)
if result is not None:
results.append(result)
except Exception:
continue
# Toplam LEAPS hacmine göre sırala
results.sort(key=lambda x: x["total_leaps_vol"], reverse=True)
return results
|