from typing import Any, Dict, List, Optional from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel from services import saxo_auth from services.saxo_scheduler import get_watchlist, set_watchlist from services.database import ( get_saxo_snapshots, get_saxo_snapshot_symbols, get_saxo_catalog, get_saxo_catalog_summary, upsert_saxo_catalog_rows, ) router = APIRouter(prefix="/api/saxo", tags=["saxo"]) class WatchlistRequest(BaseModel): symbols: List[str] @router.get("/status") def status(): return saxo_auth.get_status() @router.post("/disconnect") def disconnect(): saxo_auth.disconnect() return {"disconnected": True} @router.get("/watchlist") def watchlist(): return {"symbols": get_watchlist()} @router.put("/watchlist") def update_watchlist(req: WatchlistRequest): set_watchlist(req.symbols) return {"symbols": get_watchlist()} def _validate_symbol(symbol: str) -> dict: from services.saxo_client import resolve_instrument, SaxoNotConnected try: info = resolve_instrument(symbol) return { "symbol": symbol.upper(), "valid": True, "error": None, "uic": info["uic"], "matched_symbol": info["symbol"], "description": info["description"], "asset_type": info["asset_type"], } except SaxoNotConnected as e: return {"symbol": symbol.upper(), "valid": False, "uic": None, "error": str(e)} except Exception as e: return {"symbol": symbol.upper(), "valid": False, "uic": None, "error": str(e)} @router.get("/validate") def validate_watchlist(): """Checks each watchlist symbol actually resolves to a real Saxo option-root instrument.""" return [_validate_symbol(s) for s in get_watchlist()] @router.get("/validate/{symbol}") def validate_symbol(symbol: str): return _validate_symbol(symbol) @router.get("/symbols") def symbols_with_history(): """Distinct symbols that already have recorded snapshot history (may differ from the current watchlist — includes ad-hoc 'snapshot now' calls and previously-tracked symbols).""" return get_saxo_snapshot_symbols() @router.post("/snapshot-now/{symbol}") def snapshot_now(symbol: str): from services.saxo_client import snapshot_options_chain, SaxoNotConnected, SaxoApiError from services.database import save_saxo_snapshot_rows try: rows = snapshot_options_chain(symbol) except SaxoNotConnected as e: raise HTTPException(status_code=401, detail=str(e)) except (ValueError, SaxoApiError) as e: raise HTTPException(status_code=502, detail=str(e)) save_saxo_snapshot_rows(rows) return {"symbol": symbol.upper(), "rows_saved": len(rows)} @router.get("/history") def history( symbol: Optional[str] = Query(None), date_from: Optional[str] = Query(None), date_to: Optional[str] = Query(None), ): return get_saxo_snapshots(symbol, date_from, date_to) class CatalogRefreshRequest(BaseModel): asset_types: Optional[List[str]] = None # default: CATALOG_ASSET_TYPES (FuturesOption, FxVanillaOption) @router.post("/catalog/refresh") def refresh_catalog(req: CatalogRefreshRequest): from services.saxo_client import list_instruments, CATALOG_ASSET_TYPES, SaxoNotConnected, SaxoApiError asset_types = req.asset_types or CATALOG_ASSET_TYPES counts: Dict[str, int] = {} try: for asset_type in asset_types: items = list_instruments(asset_type) rows = [{ "uic": item.get("Identifier") or item.get("Uic"), "symbol": item.get("Symbol"), "asset_type": asset_type, "description": item.get("Description"), } for item in items if item.get("Identifier") or item.get("Uic")] upsert_saxo_catalog_rows(rows) counts[asset_type] = len(rows) except SaxoNotConnected as e: raise HTTPException(status_code=401, detail=str(e)) except SaxoApiError as e: raise HTTPException(status_code=502, detail=str(e)) return {"refreshed": counts} @router.get("/catalog") def catalog( asset_type: Optional[str] = Query(None), q: Optional[str] = Query(None), limit: int = Query(200), ): return get_saxo_catalog(asset_type, q, limit) @router.get("/catalog/summary") def catalog_summary(): return get_saxo_catalog_summary()