import { useState } from 'react' import { Link2, Unlink } from 'lucide-react' import clsx from 'clsx' import { useSaxoCatalog } from '../hooks/useApi' // Saxo has no shared naming convention with yfinance — its catalog search matches on // Saxo's own instrument description text ("Brent Crude"), not the yfinance ticker code // ("BZ=F"). This maps the common yfinance futures-root/index tickers to the plain-English // name worth searching for, so opening a picker pre-fills something useful instead of an // empty box the user has to guess into. Not exhaustive — anything not listed here (most // FX pairs, where the Saxo symbol IS the yfinance root, e.g. "EURUSD=X" -> "EURUSD") falls // back to a best-effort strip of yfinance's own suffix/prefix decoration. 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', 'IEF': 'Treasury', } export function guessSaxoSearchHint(ticker: string): string { if (YFINANCE_SEARCH_HINTS[ticker]) return YFINANCE_SEARCH_HINTS[ticker] return ticker.replace(/=F$|=X$|^\^/g, '') } export 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', 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,CfdOnFutures,FxSpot,StockIndex', }, } as const export type SaxoLinkKind = keyof typeof SAXO_LINK_KIND_META // Generic Saxo symbol linker — ticker/saxoSymbol are display data, onSave/isPending are // injected by the caller so this can drive any backing store (instruments_watchlist's // saxo_option_symbol/saxo_quote_symbol columns via Config.tsx, or instruments.json's // saxo_quote_symbol field via InstrumentDashboard.tsx) without knowing which one it is. export default function SaxoLinkPicker({ ticker, kind, saxoSymbol, onSave, isPending, assetTypes }: { ticker: string kind: SaxoLinkKind saxoSymbol: string | null onSave: (symbol: string | null) => void isPending: boolean // Overrides SAXO_LINK_KIND_META[kind]'s default asset types — e.g. an FX instrument's // "Quote" link should only ever search FxSpot, not the default's ContractFutures/ // CfdOnFutures/StockIndex too (those exist for commodities/indices priced via futures on // Saxo, but searching "EURUSD" under the unfiltered default surfaces CME EURUSD futures // contracts alongside the actual spot rate — a different product, not what a currency // pair's price should ever be linked to). assetTypes?: string }) { const [editing, setEditing] = useState(false) const [value, setValue] = useState(saxoSymbol ?? '') const [showDropdown, setShowDropdown] = useState(false) const meta = SAXO_LINK_KIND_META[kind] const effectiveAssetTypes = assetTypes ?? meta.assetTypes const { data: catalogMatches } = useSaxoCatalog(effectiveAssetTypes, value.length >= 2 ? value : undefined) const save = (sym?: string) => { const resolved = (sym ?? value).trim().toUpperCase() || null onSave(resolved) setEditing(false) } if (!editing) { return saxoSymbol ? ( ) : ( ) } return (
{ setValue(e.target.value); setShowDropdown(true) }} onFocus={() => setShowDropdown(true)} onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') setEditing(false) }} onBlur={() => save()} placeholder={meta.placeholder} className="bg-dark-800 border border-slate-700/40 rounded px-1.5 py-0.5 text-[9px] text-white w-28 focus:outline-none focus:border-blue-500/50" /> {isPending && } {showDropdown && (catalogMatches ?? []).length > 0 && (
{(catalogMatches ?? []).map((c: any) => ( ))}
)}
) }