diff --git a/backend/routers/instruments_watchlist.py b/backend/routers/instruments_watchlist.py index 8476f15..5bdea6f 100644 --- a/backend/routers/instruments_watchlist.py +++ b/backend/routers/instruments_watchlist.py @@ -107,6 +107,23 @@ def remove_ticker(ticker: str): return {"removed": ticker.strip().upper()} +class RenameBody(BaseModel): + name: str + + +@router.put("/{ticker}/name") +def rename_ticker(ticker: str, body: RenameBody): + """Free-form display name — no longer tied to yfinance's long_name lookup, since an + instrument can now be entirely Saxo-sourced.""" + from services.database import rename_instrument_watchlist + if not body.name.strip(): + raise HTTPException(400, "Name cannot be empty") + ok = rename_instrument_watchlist(ticker, body.name) + if not ok: + raise HTTPException(404, f"'{ticker}' is not in the instruments watchlist") + return {"ticker": ticker.strip().upper(), "name": body.name.strip()} + + class ReorderBody(BaseModel): tickers: List[str] diff --git a/backend/services/database.py b/backend/services/database.py index 941e51f..8b53d13 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -3375,6 +3375,19 @@ def reorder_instruments_watchlist(tickers: List[str]) -> None: conn.close() +def rename_instrument_watchlist(ticker: str, name: str) -> bool: + """Set a free-form display name — the ticker (primary key) no longer has to be a + yfinance-recognized code now that an instrument can be entirely Saxo-sourced + (saxo_option_symbol + saxo_quote_symbol), so the auto-derived yfinance long_name is + just a starting point, not the only option.""" + conn = get_conn() + conn.execute("UPDATE instruments_watchlist SET name=? WHERE ticker=?", (name.strip(), ticker.upper())) + changed = conn.total_changes > 0 + conn.commit() + conn.close() + return changed + + # ── Wavelets — saved simulation/optimization runs ───────────────────────────── def save_wavelet_simulation(name: str, form: Dict, results: Optional[List[Dict]] = None, diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index a609aa9..a1efe72 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -182,6 +182,20 @@ export const useSetWatchlistSaxoQuoteLink = () => { }) } +// Free-form display name — no longer tied to yfinance's long_name lookup, since an +// instrument can now be entirely Saxo-sourced. +export const useRenameWatchlistInstrument = () => { + const qc = useQueryClient() + return useMutation({ + mutationFn: ({ ticker, name }: { ticker: string; name: string }) => + api.put(`/watchlist/${encodeURIComponent(ticker)}/name`, { name }).then(r => r.data), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['instruments-watchlist'] }) + qc.invalidateQueries({ queryKey: ['instruments-watchlist-quotes'] }) + }, + }) +} + // Latest cycle report — shares the 'cycle-report-latest' queryKey with RapportIA.tsx export const useLatestCycleReport = () => useQuery({ diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index 305726a..9f43db9 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, 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, useRenameWatchlistInstrument, 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' @@ -352,7 +352,7 @@ const YFINANCE_SEARCH_HINTS: Record = { 'BZ=F': 'Brent', 'CL=F': 'WTI Crude', 'NG=F': 'Natural Gas', 'GC=F': 'Gold', 'SI=F': 'Silver', 'HG=F': 'Copper', 'PL=F': 'Platinum', '^GSPC': 'S&P 500', '^NDX': 'Nasdaq 100', '^DJI': 'Dow Jones', '^RUT': 'Russell 2000', - '^VIX': 'VIX', + '^VIX': 'VIX', 'IEF': 'Treasury', } function guessSaxoSearchHint(ticker: string): string { @@ -433,6 +433,43 @@ function SaxoLinkPicker({ ticker, kind, saxoSymbol }: { ticker: string; kind: ke ) } +// Inline-editable display name — the ticker (primary key) no longer has to be a +// yfinance-recognized code now that an instrument can be entirely Saxo-sourced, so the +// name shouldn't be stuck at whatever yfinance's long_name lookup produced (often just +// the ticker itself, e.g. "BZ=F") or the user's raw typed input (e.g. "BRENT"). +function EditableName({ ticker, name }: { ticker: string; name: string }) { + const [editing, setEditing] = useState(false) + const [value, setValue] = useState(name) + const { mutate: rename, isPending } = useRenameWatchlistInstrument() + + const save = () => { + const trimmed = value.trim() + if (!trimmed || trimmed === name) { setEditing(false); setValue(name); return } + rename({ ticker, name: trimmed }, { onSuccess: () => setEditing(false) }) + } + + if (!editing) { + return ( + + ) + } + + return ( + setValue(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') { setEditing(false); setValue(name) } }} + onBlur={save} + disabled={isPending} + className="bg-dark-800 border border-slate-700/40 rounded px-1.5 py-0.5 text-[10px] text-white w-32 focus:outline-none focus:border-blue-500/50" + /> + ) +} + function WatchlistCard() { const { data: items, isLoading } = useInstrumentsWatchlist() const { mutateAsync: addTicker, isPending: adding } = useAddWatchlistInstrument() @@ -502,7 +539,7 @@ function WatchlistCard() {
{item.ticker} - {item.name} + {item.asset_class}