chart/app/market/schwab_quotes.py

80 lines
2.1 KiB
Python

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