From e7d841e3aa52634f84f860eaddf49793763fffcc Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Thu, 23 Jul 2026 17:19:23 +0200 Subject: [PATCH] feat: saxo price --- backend/routers/instruments_watchlist.py | 30 ++++++++++++++++-------- frontend/src/pages/Config.tsx | 18 ++++++++------ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/backend/routers/instruments_watchlist.py b/backend/routers/instruments_watchlist.py index 1012520..ca92c2d 100644 --- a/backend/routers/instruments_watchlist.py +++ b/backend/routers/instruments_watchlist.py @@ -66,26 +66,36 @@ def watchlist_quotes(): @router.post("/{ticker}") def add_ticker(ticker: str): + """Adds a tracked instrument. yfinance validation is best-effort, not a gate — an + instrument can be entirely Saxo-sourced (both saxo_option_symbol and + saxo_quote_symbol linked, see the two PUT .../saxo-*-link endpoints below) with no + yfinance equivalent at all, so a ticker yfinance doesn't recognize is still added, + just without a name/asset_class lookup or an initial price to report back.""" from services.data_fetcher import get_quote from services.database import get_instruments_watchlist, add_instrument_watchlist import yfinance as yf ticker = ticker.strip().upper() if any(row["ticker"] == ticker for row in get_instruments_watchlist()): raise HTTPException(409, f"'{ticker}' is already in the watchlist") + q = get_quote(ticker) - if not q or not q.get("price"): - raise HTTPException(400, f"Ticker '{ticker}' not found on yfinance") name = ticker asset_class = "unknown" - try: - info = yf.Ticker(ticker).fast_info - name = getattr(info, "long_name", None) or getattr(info, "short_name", None) or ticker - quote_type = getattr(info, "quote_type", "") or "" - asset_class = _QUOTE_TYPE_TO_ASSET_CLASS.get(quote_type.upper(), "unknown") - except Exception: - pass + if q and q.get("price"): + try: + info = yf.Ticker(ticker).fast_info + name = getattr(info, "long_name", None) or getattr(info, "short_name", None) or ticker + quote_type = getattr(info, "quote_type", "") or "" + asset_class = _QUOTE_TYPE_TO_ASSET_CLASS.get(quote_type.upper(), "unknown") + except Exception: + pass + add_instrument_watchlist(ticker, name, asset_class) - return {"ticker": ticker, "name": name, "asset_class": asset_class, "price": q["price"]} + return { + "ticker": ticker, "name": name, "asset_class": asset_class, + "price": q.get("price") if q else None, + "yfinance_recognized": bool(q and q.get("price")), + } @router.delete("/{ticker}") diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index 8664e0b..305726a 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' import { Link } from 'react-router-dom' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useCycleStepCatalog, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate, useTechIndicatorsConfig, useSaveTechIndicatorsConfig, useInstrumentsWatchlist, useAddWatchlistInstrument, useRemoveWatchlistInstrument, useSetWatchlistSaxoOptionLink, useSetWatchlistSaxoQuoteLink, validateTicker, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, useExpandSaxoWatchlist, type CycleStepDef } from '../hooks/useApi' +import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useCycleStepCatalog, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate, useTechIndicatorsConfig, useSaveTechIndicatorsConfig, useInstrumentsWatchlist, useAddWatchlistInstrument, useRemoveWatchlistInstrument, useSetWatchlistSaxoOptionLink, useSetWatchlistSaxoQuoteLink, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, useExpandSaxoWatchlist, type CycleStepDef } from '../hooks/useApi' import { Settings, Key, Globe, CheckCircle, XCircle, AlertCircle, Save, Eye, EyeOff, Brain, SlidersHorizontal, RefreshCw, Zap, Plus, Trash2, Pencil, X, Lock, Gauge, DollarSign, TrendingUp, ShieldAlert, DatabaseBackup, Radar, Link2, Unlink, Camera, ShieldCheck, ExternalLink } from 'lucide-react' import clsx from 'clsx' @@ -439,18 +439,21 @@ function WatchlistCard() { const { mutate: removeTicker } = useRemoveWatchlistInstrument() const [input, setInput] = useState('') const [error, setError] = useState('') + const [note, setNote] = useState('') const handleAdd = async () => { const sym = input.trim().toUpperCase() if (!sym) return setError('') - const check = await validateTicker(sym) - if (!check.valid) { - setError(check.reason ?? 'Ticker not found') - return - } + setNote('') + // yfinance recognition is informational only, not a gate — an instrument can be + // entirely Saxo-sourced (link its Option/Quote symbols below) with no yfinance + // equivalent at all. try { - await addTicker(sym) + const result = await addTicker(sym) + if (!(result as any)?.yfinance_recognized) { + setNote(`'${sym}' added, but yfinance doesn't recognize it — no fallback price until you link it to Saxo (Option/Quote below).`) + } setInput('') } catch (e: unknown) { const msg = (e as any)?.response?.data?.detail ?? (e as { message?: string })?.message ?? 'Could not add ticker' @@ -487,6 +490,7 @@ function WatchlistCard() { {error &&
{error}
} + {note &&
{note}
} {isLoading ? (
Loading…