feat: saxo price

This commit is contained in:
OpenSquared
2026-07-23 17:59:27 +02:00
parent 619f521b32
commit d3dc85fee9
4 changed files with 84 additions and 3 deletions

View File

@@ -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({

View File

@@ -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<string, string> = {
'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 (
<button onClick={() => { setEditing(true); setValue(name) }} className="flex items-center gap-1 text-[10px] text-slate-500 hover:text-slate-300 truncate group">
{name}
<Pencil className="w-2.5 h-2.5 opacity-0 group-hover:opacity-60 shrink-0" />
</button>
)
}
return (
<input
autoFocus
value={value}
onChange={e => 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() {
<div key={item.ticker} className="flex items-center justify-between bg-dark-800/60 rounded px-3 py-1.5">
<div className="flex items-center gap-2 min-w-0">
<span className="text-xs font-mono font-bold text-white">{item.ticker}</span>
<span className="text-[10px] text-slate-500 truncate">{item.name}</span>
<EditableName ticker={item.ticker} name={item.name} />
<span className="text-[9px] text-slate-700 bg-dark-700 px-1.5 py-0.5 rounded capitalize">{item.asset_class}</span>
</div>
<div className="flex items-center gap-2 shrink-0">