TradeTerminal / leaps_scanner.py
ArifKemall's picture
deploy: backend to HF Space (with README metadata)
85416dd
Raw
History Blame Contribute Delete
7.16 kB
"""
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