"""Options data tools (Phase 6a — single-leg long calls/puts). get_option_chain lists near-the-money contracts near a target DTE with live premiums; the agent copies a contract_symbol into its pick. get_option_quote prices one contract. """ from __future__ import annotations from datetime import date, timedelta from functools import lru_cache from langchain_core.tools import tool from config.settings import get_settings from tools.market_data import latest_price @lru_cache def _opt_data(): from alpaca.data.historical.option import OptionHistoricalDataClient key, secret = get_settings().alpaca_data_creds() return OptionHistoricalDataClient(key, secret) @lru_cache def _opt_trading(): from alpaca.trading.client import TradingClient s = get_settings() key, secret = s.alpaca_data_creds() return TradingClient(key, secret, paper=s.alpaca_paper) def option_premium(contract_symbol: str) -> float | None: """Mid premium for an option contract (per share; ×100 = contract cost). None if unavailable.""" from alpaca.data.requests import OptionLatestQuoteRequest try: q = _opt_data().get_option_latest_quote( OptionLatestQuoteRequest(symbol_or_symbols=contract_symbol) ) c = q[contract_symbol] bid, ask = float(c.bid_price or 0), float(c.ask_price or 0) if bid and ask: return round((bid + ask) / 2, 2) return round(ask or bid, 2) or None except Exception: return None @tool def get_option_chain(underlying: str, right: str = "call", target_dte: int = 35) -> str: """List near-the-money option contracts for an underlying stock. right = 'call' (bullish) or 'put' (bearish). target_dte = desired days to expiration (~30-45 is typical). Returns contract symbols + strike + expiry + mid premium. Copy a contract_symbol into your pick to trade it. """ from alpaca.trading.enums import AssetStatus, ContractType from alpaca.trading.requests import GetOptionContractsRequest underlying = underlying.strip().upper() px = latest_price(underlying) if not px: return f"ERROR: no price for {underlying}" ct = ContractType.CALL if right.lower().startswith("c") else ContractType.PUT today = date.today() try: req = GetOptionContractsRequest( underlying_symbols=[underlying], type=ct, status=AssetStatus.ACTIVE, expiration_date_gte=today + timedelta(days=max(target_dte - 15, 1)), expiration_date_lte=today + timedelta(days=target_dte + 20), strike_price_gte=str(round(px * 0.85, 2)), strike_price_lte=str(round(px * 1.15, 2)), limit=60, ) contracts = _opt_trading().get_option_contracts(req).option_contracts except Exception as e: return f"ERROR: chain fetch failed: {e}" if not contracts: return f"No {right} contracts for {underlying} near {target_dte} DTE." # nearest expiry first, then nearest strike to spot contracts.sort(key=lambda c: (c.expiration_date, abs(float(c.strike_price) - px))) out = [f"{underlying} ~${px:.2f}: {right}s ~{target_dte}DTE (copy a contract_symbol):"] for c in contracts[:10]: prem = option_premium(c.symbol) prem_s = f"${prem:.2f}" if prem else "n/a" out.append( f" {c.symbol} strike ${float(c.strike_price):.1f} exp {c.expiration_date} mid {prem_s}" ) return "\n".join(out) @tool def get_option_quote(contract_symbol: str) -> str: """Latest mid premium for an option contract symbol (OCC format, from get_option_chain).""" prem = option_premium(contract_symbol.strip().upper()) return f"{contract_symbol}: mid ${prem:.2f} (x100 = ${prem * 100:.0f}/contract)" if prem else ( f"ERROR: no quote for {contract_symbol}" )