From 7267443d5b0ab8385312a9957b67c1a23268a40f Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Thu, 23 Jul 2026 10:46:24 +0200 Subject: [PATCH] feat: saxo price --- backend/routers/saxo.py | 2 +- backend/services/database.py | 7 +++++-- backend/services/saxo_client.py | 12 +++++++++++- frontend/src/pages/Config.tsx | 17 ++++++++++++++--- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/backend/routers/saxo.py b/backend/routers/saxo.py index 8dbe11e..1498232 100644 --- a/backend/routers/saxo.py +++ b/backend/routers/saxo.py @@ -187,7 +187,7 @@ def delete_history(symbol: str): class CatalogRefreshRequest(BaseModel): - asset_types: Optional[List[str]] = None # default: CATALOG_ASSET_TYPES (FuturesOption, FxVanillaOption) + asset_types: Optional[List[str]] = None # default: CATALOG_ASSET_TYPES (FuturesOption, FxVanillaOption, ContractFutures, FxSpot) @router.post("/catalog/refresh") diff --git a/backend/services/database.py b/backend/services/database.py index 5245e55..941e51f 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -6387,12 +6387,15 @@ def upsert_saxo_catalog_rows(rows: List[Dict[str, Any]]): def get_saxo_catalog(asset_type: Optional[str] = None, q: Optional[str] = None, limit: int = 200) -> List[Dict[str, Any]]: + """asset_type accepts a single value or a comma-separated list (e.g. the Instruments + Watchlist "Option" picker passes all 4 option AssetTypes at once).""" conn = get_conn() query = "SELECT * FROM saxo_instrument_catalog WHERE 1=1" params: List[Any] = [] if asset_type: - query += " AND asset_type=?" - params.append(asset_type) + types = [t.strip() for t in asset_type.split(",") if t.strip()] + query += f" AND asset_type IN ({','.join('?' * len(types))})" + params.extend(types) if q: query += " AND (symbol LIKE ? OR description LIKE ?)" params.extend([f"%{q}%", f"%{q}%"]) diff --git a/backend/services/saxo_client.py b/backend/services/saxo_client.py index 8d6d7e4..5c80236 100644 --- a/backend/services/saxo_client.py +++ b/backend/services/saxo_client.py @@ -60,7 +60,17 @@ def _synthesize_quote( # Bounded, stable catalogs worth fully caching in our own DB (StockOption/StockIndexOption # are far too large to bulk-fetch — those stay resolved on demand via Keywords search). -CATALOG_ASSET_TYPES = ["FuturesOption", "FxVanillaOption"] +# ContractFutures (outright futures — Brent, WTI, Gold, Copper, index e-minis) and FxSpot +# (FX pairs) are the underlying-side equivalents of the option types above — added so the +# Instruments Watchlist "Quote" link (services.database.set_instrument_watchlist_saxo_quote_symbol) +# has a real catalog to search instead of only ever finding option instruments. +CATALOG_ASSET_TYPES = ["FuturesOption", "FxVanillaOption", "ContractFutures", "FxSpot"] + +# Which of CATALOG_ASSET_TYPES are options (Instruments Watchlist "Option" picker) vs. +# underlyings (the "Quote" picker) — see routers/instruments_watchlist.py's saxo-*-link +# endpoints and frontend/src/pages/Config.tsx's SaxoLinkPicker. +OPTION_ASSET_TYPES = ["FuturesOption", "FxVanillaOption", "StockOption", "StockIndexOption"] +UNDERLYING_ASSET_TYPES = ["ContractFutures", "FxSpot", "StockIndex"] # symbol -> resolved instrument details, cheap in-process cache (roots don't change within a session) _root_uic_cache: Dict[str, Dict[str, Any]] = {} diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index eee32e2..a31c47f 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -338,9 +338,20 @@ function RiskProfilesCard() { // scheduler's watchlist (Config -> Saxo below) so it gets snapshotted. // 'quote' — which Saxo spot/futures symbol prices this instrument in the Cockpit, // replacing yfinance's unadjusted continuous-futures tickers. +// Asset-type filters matching backend services.saxo_client.OPTION_ASSET_TYPES / +// UNDERLYING_ASSET_TYPES — keeps each picker's search scoped to the catalog rows that +// are actually relevant, instead of both searching the same undifferentiated list. const SAXO_LINK_KIND_META = { - option: { label: 'Option', placeholder: 'Search Saxo option symbol…', activeCls: 'text-emerald-400 hover:text-emerald-300 bg-emerald-900/20 border-emerald-700/30' }, - quote: { label: 'Quote', placeholder: 'Search Saxo spot/futures symbol…', activeCls: 'text-sky-400 hover:text-sky-300 bg-sky-900/20 border-sky-700/30' }, + option: { + label: 'Option', placeholder: 'Search Saxo option symbol…', + activeCls: 'text-emerald-400 hover:text-emerald-300 bg-emerald-900/20 border-emerald-700/30', + assetTypes: 'FuturesOption,FxVanillaOption,StockOption,StockIndexOption', + }, + quote: { + label: 'Quote', placeholder: 'Search Saxo spot/futures symbol…', + activeCls: 'text-sky-400 hover:text-sky-300 bg-sky-900/20 border-sky-700/30', + assetTypes: 'ContractFutures,FxSpot,StockIndex', + }, } as const function SaxoLinkPicker({ ticker, kind, saxoSymbol }: { ticker: string; kind: keyof typeof SAXO_LINK_KIND_META; saxoSymbol: string | null }) { @@ -350,8 +361,8 @@ function SaxoLinkPicker({ ticker, kind, saxoSymbol }: { ticker: string; kind: ke const optionLink = useSetWatchlistSaxoOptionLink() const quoteLink = useSetWatchlistSaxoQuoteLink() const { mutate: setLink, isPending } = kind === 'option' ? optionLink : quoteLink - const { data: catalogMatches } = useSaxoCatalog(undefined, value.length >= 2 ? value : undefined) const meta = SAXO_LINK_KIND_META[kind] + const { data: catalogMatches } = useSaxoCatalog(meta.assetTypes, value.length >= 2 ? value : undefined) const save = (sym?: string) => { const resolved = (sym ?? value).trim().toUpperCase() || null