from __future__ import annotations from datetime import date, datetime from app.market.es_options import ( QUOTE_BATCH, candidate_symbols, search_from_quotes, years_to_expiry, ) def schwab_rest_client(settings): from schwab.auth import client_from_token_file if not settings.schwab_token_path.exists(): raise FileNotFoundError(f"No Schwab token at {settings.schwab_token_path}") return client_from_token_file( str(settings.schwab_token_path), settings.schwab_api_key, settings.schwab_app_secret, ) def fetch_quotes(client, symbols: list[str]) -> dict[str, dict]: quotes: dict[str, dict] = {} for index in range(0, len(symbols), QUOTE_BATCH): payload = client.get_quotes(symbols[index : index + QUOTE_BATCH]).json() for symbol, body in payload.items(): if symbol != "errors": quotes[symbol] = body return quotes def underlying_price(client, symbol: str = "/ES") -> tuple[str, float]: payload = client.get_quotes([symbol]).json() for returned, body in payload.items(): if returned == "errors": continue if body.get("assetMainType") != "FUTURE": continue fields = body.get("quote") or {} price = fields.get("mark") if price is None: price = fields.get("lastPrice") if price is None: continue return returned, float(price) raise ValueError("No /ES futures quote") def run_search( settings, *, day: date, root: str, side: str, mode: str, low: float, high: float, now: datetime | None = None, client=None, ) -> dict: rest = client or schwab_rest_client(settings) contract, forward = underlying_price(rest) symbols = candidate_symbols(root, side, forward) quotes = fetch_quotes(rest, symbols) result = search_from_quotes( day=day, side=side, mode=mode, low=low, high=high, forward=forward, quotes=quotes, now=now, ) result["underlying"] = contract result["years"] = years_to_expiry(day, now) return result