from __future__ import annotations from dataclasses import dataclass from datetime import date, datetime, timedelta from math import erf, log, sqrt from zoneinfo import ZoneInfo EASTERN = ZoneInfo("America/New_York") MONTH_CODES = { 1: "F", 2: "G", 3: "H", 4: "J", 5: "K", 6: "M", 7: "N", 8: "Q", 9: "U", 10: "V", 11: "X", 12: "Z", } WEEKDAY_LETTER = {0: "A", 1: "B", 2: "C", 3: "D"} QUARTERLY_MONTHS = {3, 6, 9, 12} TOS_SUFFIX = ":XCME" QUOTE_BATCH = 50 @dataclass(frozen=True) class Expiration: id: str kind: str date: date root: str label: str def to_dict(self) -> dict: return { "id": self.id, "kind": self.kind, "date": self.date.isoformat(), "root": self.root, "label": self.label, } def week_of_month(day: date) -> int: return (day.day - 1) // 7 + 1 def daily_root(day: date) -> str: month = MONTH_CODES[day.month] year = day.year % 100 nth = week_of_month(day) if day.weekday() == 4: return f"EW{nth}{month}{year:02d}" return f"E{nth}{WEEKDAY_LETTER[day.weekday()]}{month}{year:02d}" def monthly_root(day: date) -> str: if day.month in QUARTERLY_MONTHS: return f"ES{MONTH_CODES[day.month]}{day.year % 100:02d}" return daily_root(day) def api_symbol(root: str, side: str, strike: float | int) -> str: return f"./{root}{side}{int(strike)}" def tos_symbol(symbol: str) -> str: if symbol.endswith(TOS_SUFFIX): return symbol return f"{symbol}{TOS_SUFFIX}" def from_tos_symbol(symbol: str) -> str: if symbol.endswith(TOS_SUFFIX): return symbol[: -len(TOS_SUFFIX)] return symbol def next_weekdays(today: date, count: int) -> list[date]: days: list[date] = [] cursor = today while len(days) < count: if cursor.weekday() < 5: days.append(cursor) cursor += timedelta(days=1) return days def next_friday(today: date) -> date: return today + timedelta(days=(4 - today.weekday()) % 7) def third_friday(year: int, month: int) -> date: first = date(year, month, 1) first_friday = first + timedelta(days=(4 - first.weekday()) % 7) return first_friday + timedelta(days=14) def next_third_friday(today: date) -> date: candidate = third_friday(today.year, today.month) if candidate >= today: return candidate if today.month == 12: return third_friday(today.year + 1, 1) return third_friday(today.year, today.month + 1) def _label(day: date, kind: str) -> str: return f"{day.strftime('%a %b')} {day.day} {kind}" def nearby_expirations(today: date | None = None) -> list[Expiration]: if today is None: today = datetime.now(EASTERN).date() expirations: list[Expiration] = [] for day in next_weekdays(today, 3): expirations.append( Expiration( id=f"daily-{day.isoformat()}", kind="daily", date=day, root=daily_root(day), label=_label(day, "daily"), ) ) weekly = next_friday(today) expirations.append( Expiration( id=f"weekly-{weekly.isoformat()}", kind="weekly", date=weekly, root=daily_root(weekly), label=_label(weekly, "weekly"), ) ) monthly = next_third_friday(today) expirations.append( Expiration( id=f"monthly-{monthly.isoformat()}", kind="monthly", date=monthly, root=monthly_root(monthly), label=_label(monthly, "monthly"), ) ) return expirations def strike_step(root: str) -> int: return 25 if root.startswith("ES") else 5 def strike_span(root: str) -> int: return 600 if root.startswith("ES") else 200 def strike_grid(price: float, root: str) -> list[int]: step = strike_step(root) span = strike_span(root) center = int(round(price / step) * step) return list(range(center - span, center + span + step, step)) def candidate_symbols(root: str, side: str, price: float) -> list[str]: return [api_symbol(root, side, strike) for strike in strike_grid(price, root)] def _norm_cdf(value: float) -> float: return 0.5 * (1.0 + erf(value / sqrt(2.0))) def years_to_expiry(day: date, now: datetime | None = None) -> float: if now is None: now = datetime.now(EASTERN) if now.tzinfo is None: now = now.replace(tzinfo=EASTERN) expiry = datetime(day.year, day.month, day.day, 16, 0, tzinfo=EASTERN) seconds = (expiry - now.astimezone(EASTERN)).total_seconds() return max(seconds / (365.25 * 24 * 3600), 1 / 365.25) def black76_price(forward: float, strike: float, years: float, sigma: float, side: str) -> float: d1 = (log(forward / strike) + 0.5 * sigma * sigma * years) / (sigma * sqrt(years)) d2 = d1 - sigma * sqrt(years) if side == "C": return forward * _norm_cdf(d1) - strike * _norm_cdf(d2) return strike * _norm_cdf(-d2) - forward * _norm_cdf(-d1) def black76_delta(forward: float, strike: float, years: float, sigma: float, side: str) -> float | None: if years <= 0 or sigma <= 0 or forward <= 0 or strike <= 0: return None d1 = (log(forward / strike) + 0.5 * sigma * sigma * years) / (sigma * sqrt(years)) if side == "C": return _norm_cdf(d1) return -_norm_cdf(-d1) def implied_vol(forward: float, strike: float, years: float, price: float, side: str) -> float | None: if price <= 0 or years <= 0 or forward <= 0 or strike <= 0: return None low, high = 0.01, 3.0 for _ in range(40): mid = (low + high) / 2 model = black76_price(forward, strike, years, mid, side) if model > price: high = mid else: low = mid return (low + high) / 2 def parse_option_quote(symbol: str, payload: dict) -> dict | None: if payload.get("assetMainType") != "FUTURE_OPTION": return None fields = payload.get("quote") or {} reference = payload.get("reference") or {} mark = fields.get("mark") strike = reference.get("strikePrice") if mark is None or strike is None: return None description = reference.get("description") or tos_symbol(symbol) return { "symbol": symbol, "tos": description, "strike": float(strike), "side": reference.get("contractType") or "", "bid": fields.get("bidPrice"), "ask": fields.get("askPrice"), "mark": float(mark), "last": fields.get("lastPrice"), "volume": fields.get("totalVolume") or 0, "open_interest": fields.get("openInterest") or 0, "quote_time": fields.get("quoteTime"), "realtime": payload.get("realtime"), } def atm_implied_vol(contracts: list[dict], forward: float, years: float, side: str) -> float | None: if not contracts: return None atm = min(contracts, key=lambda row: abs(row["strike"] - forward)) return implied_vol(forward, atm["strike"], years, atm["mark"], side) def attach_deltas(contracts: list[dict], forward: float, years: float, side: str, sigma: float | None) -> list[dict]: vol = sigma or 0.15 rows = [] for contract in contracts: delta = black76_delta(forward, contract["strike"], years, vol, side) rows.append({**contract, "delta": delta, "abs_delta": None if delta is None else abs(delta)}) return rows def filter_contracts(contracts: list[dict], mode: str, low: float, high: float) -> list[dict]: if low > high: low, high = high, low kept = [] for contract in contracts: value = contract["mark"] if mode == "price" else contract.get("abs_delta") if value is None: continue if low <= value <= high: kept.append(contract) kept.sort(key=lambda row: row["strike"]) return kept def search_from_quotes( *, day: date, side: str, mode: str, low: float, high: float, forward: float, quotes: dict[str, dict], now: datetime | None = None, ) -> dict: contracts = [] for symbol, payload in quotes.items(): parsed = parse_option_quote(symbol, payload) if parsed is None: continue contracts.append(parsed) years = years_to_expiry(day, now) sigma = atm_implied_vol(contracts, forward, years, side) ranked = attach_deltas(contracts, forward, years, side, sigma) matches = filter_contracts(ranked, mode, low, high) return { "underlying_price": forward, "iv": sigma, "delta_approx": True, "contracts": matches, }