"""Tradier vendor module: typed dataclasses and options chain retrieval with Greeks and IV. Provides OptionsContract and OptionsChain dataclasses as the canonical typed structures for options data throughout the system. Retrieves options chains from the Tradier API with 1st-order Greeks (via ORATS), IV fields, DTE filtering, and session caching. """ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime, date import pandas as pd from .tradier_common import make_tradier_request_with_retry, TradierRateLimitError # --------------------------------------------------------------------------- # Typed dataclasses # --------------------------------------------------------------------------- @dataclass class OptionsContract: """A single options contract with Greeks and IV from Tradier/ORATS. Fields map directly to the Tradier ``/v1/markets/options/chains`` response with ``greeks=true``. """ symbol: str # OCC symbol e.g. AAPL220617C00270000 underlying: str # e.g. AAPL option_type: str # "call" or "put" strike: float expiration_date: str # YYYY-MM-DD bid: float ask: float last: float volume: int open_interest: int # Greeks (from ORATS via Tradier) delta: float | None = None gamma: float | None = None theta: float | None = None vega: float | None = None rho: float | None = None phi: float | None = None # IV bid_iv: float | None = None mid_iv: float | None = None ask_iv: float | None = None smv_vol: float | None = None greeks_updated_at: str | None = None @dataclass class OptionsChain: """A collection of options contracts for a single underlying. Provides convenience methods for DataFrame conversion and DTE-based filtering. """ underlying: str fetch_timestamp: str expirations: list[str] contracts: list[OptionsContract] = field(default_factory=list) def to_dataframe(self) -> pd.DataFrame: """Convert all contracts to a pandas DataFrame.""" return pd.DataFrame([vars(c) for c in self.contracts]) def filter_by_dte(self, min_dte: int = 0, max_dte: int = 50) -> OptionsChain: """Return a new OptionsChain containing only contracts within the DTE range. Args: min_dte: Minimum days to expiration (inclusive). max_dte: Maximum days to expiration (inclusive). Returns: A new OptionsChain with filtered contracts and updated expirations list. """ today = date.today() filtered: list[OptionsContract] = [] for contract in self.contracts: exp = datetime.strptime(contract.expiration_date, "%Y-%m-%d").date() dte = (exp - today).days if min_dte <= dte <= max_dte: filtered.append(contract) # Derive unique sorted expirations from filtered contracts unique_exps = sorted({c.expiration_date for c in filtered}) return OptionsChain( underlying=self.underlying, fetch_timestamp=self.fetch_timestamp, expirations=unique_exps, contracts=filtered, ) # --------------------------------------------------------------------------- # Response parsing # --------------------------------------------------------------------------- def _parse_contract(raw: dict) -> OptionsContract: """Parse a single contract dict from the Tradier API response. Handles missing Greeks gracefully (sandbox has no Greeks -- Pitfall 1). """ greeks = raw.get("greeks") or {} return OptionsContract( symbol=raw.get("symbol", ""), underlying=raw.get("underlying", ""), option_type=raw.get("option_type", ""), strike=float(raw.get("strike", 0) or 0), expiration_date=raw.get("expiration_date", ""), bid=float(raw.get("bid", 0) or 0), ask=float(raw.get("ask", 0) or 0), last=float(raw.get("last", 0) or 0), volume=int(raw.get("volume", 0) or 0), open_interest=int(raw.get("open_interest", 0) or 0), delta=greeks.get("delta"), gamma=greeks.get("gamma"), theta=greeks.get("theta"), vega=greeks.get("vega"), rho=greeks.get("rho"), phi=greeks.get("phi"), bid_iv=greeks.get("bid_iv"), mid_iv=greeks.get("mid_iv"), ask_iv=greeks.get("ask_iv"), smv_vol=greeks.get("smv_vol"), greeks_updated_at=greeks.get("updated_at"), ) # --------------------------------------------------------------------------- # API retrieval functions # --------------------------------------------------------------------------- def get_options_expirations( symbol: str, min_dte: int = 0, max_dte: int = 50 ) -> list[str]: """Retrieve available option expiration dates for a symbol, filtered by DTE range. Args: symbol: Ticker symbol (e.g. 'AAPL'). min_dte: Minimum days to expiration (inclusive). max_dte: Maximum days to expiration (inclusive). Returns: Sorted list of expiration date strings in YYYY-MM-DD format. """ data = make_tradier_request_with_retry( "/v1/markets/options/expirations", { "symbol": symbol.upper(), "includeAllRoots": "false", "strikes": "false", }, ) dates = data.get("expirations", {}).get("date", []) # Pitfall 5: single-item response comes as a string, not a list if isinstance(dates, str): dates = [dates] today = date.today() qualifying: list[str] = [] for d in dates: exp = datetime.strptime(d, "%Y-%m-%d").date() dte = (exp - today).days if min_dte <= dte <= max_dte: qualifying.append(d) return sorted(qualifying) def _fetch_chain_for_expiration( symbol: str, expiration: str ) -> list[OptionsContract]: """Fetch the options chain for a single expiration date. Args: symbol: Ticker symbol. expiration: Expiration date in YYYY-MM-DD format. Returns: List of parsed OptionsContract instances. """ data = make_tradier_request_with_retry( "/v1/markets/options/chains", { "symbol": symbol.upper(), "expiration": expiration, "greeks": "true", }, ) options = data.get("options", {}).get("option", []) # Pitfall 2: single contract comes as a dict, not a list if isinstance(options, dict): options = [options] return [_parse_contract(opt) for opt in options] # --------------------------------------------------------------------------- # Session cache # --------------------------------------------------------------------------- _options_cache: dict[str, OptionsChain] = {} def clear_options_cache() -> None: """Clear the in-memory options chain cache.""" _options_cache.clear() # --------------------------------------------------------------------------- # Public retrieval functions # --------------------------------------------------------------------------- def get_options_chain(symbol: str, min_dte: int = 0, max_dte: int = 50) -> str: """Retrieve the full options chain as a string (for LLM tool consumption). Pre-fetches all expirations within the DTE range (D-03), always requests greeks=true (D-05). Results are cached per (symbol, min_dte, max_dte) key. Args: symbol: Ticker symbol (e.g. 'AAPL'). min_dte: Minimum days to expiration (inclusive). max_dte: Maximum days to expiration (inclusive). Returns: String representation of the options chain DataFrame. """ cache_key = f"{symbol.upper()}:{min_dte}:{max_dte}" if cache_key in _options_cache: return _options_cache[cache_key].to_dataframe().to_string() expirations = get_options_expirations(symbol, min_dte, max_dte) all_contracts: list[OptionsContract] = [] for exp in expirations: all_contracts.extend(_fetch_chain_for_expiration(symbol, exp)) chain = OptionsChain( underlying=symbol.upper(), fetch_timestamp=datetime.now().isoformat(), expirations=expirations, contracts=all_contracts, ) _options_cache[cache_key] = chain return chain.to_dataframe().to_string() def get_options_chain_structured( symbol: str, min_dte: int = 0, max_dte: int = 50 ) -> OptionsChain: """Retrieve the full options chain as a typed OptionsChain dataclass. Same behavior as get_options_chain() but returns the OptionsChain directly for programmatic access by downstream computation modules. Args: symbol: Ticker symbol (e.g. 'AAPL'). min_dte: Minimum days to expiration (inclusive). max_dte: Maximum days to expiration (inclusive). Returns: OptionsChain dataclass with all contracts, Greeks, and IV. """ cache_key = f"{symbol.upper()}:{min_dte}:{max_dte}" if cache_key in _options_cache: return _options_cache[cache_key] expirations = get_options_expirations(symbol, min_dte, max_dte) all_contracts: list[OptionsContract] = [] for exp in expirations: all_contracts.extend(_fetch_chain_for_expiration(symbol, exp)) chain = OptionsChain( underlying=symbol.upper(), fetch_timestamp=datetime.now().isoformat(), expirations=expirations, contracts=all_contracts, ) _options_cache[cache_key] = chain return chain