feat: saxo price

This commit is contained in:
OpenSquared
2026-07-23 10:33:18 +02:00
parent 1ac2270271
commit a136d6ae11
5 changed files with 118 additions and 40 deletions

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, useSetWatchlistSaxoLink, 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, validateTicker, 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'
@@ -331,16 +331,27 @@ function RiskProfilesCard() {
)
}
// Inline Saxo-symbol link editor for one watchlist row — lets Options Lab's Saxo section
// show only broker data for instruments actually tracked here, instead of relying on an
// independently-managed Saxo watchlist. Linking auto-adds the symbol to the Saxo
// scheduler's watchlist (Config -> Saxo below) so it starts getting snapshotted.
function SaxoLinkPicker({ ticker, saxoSymbol }: { ticker: string; saxoSymbol: string | null }) {
// Inline Saxo-symbol link editor for one watchlist row. Two independent kinds, often
// pointing at DIFFERENT Saxo instruments for the same tracked ticker:
// 'option' — which Saxo options chain Options Lab should analyze (e.g. CL=F -> the
// Saxo micro-futures option chain MCL:XCME). Auto-adds to the Saxo
// 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.
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' },
} as const
function SaxoLinkPicker({ ticker, kind, saxoSymbol }: { ticker: string; kind: keyof typeof SAXO_LINK_KIND_META; saxoSymbol: string | null }) {
const [editing, setEditing] = useState(false)
const [value, setValue] = useState(saxoSymbol ?? '')
const [showDropdown, setShowDropdown] = useState(false)
const { mutate: setLink, isPending } = useSetWatchlistSaxoLink()
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 save = (sym?: string) => {
const resolved = (sym ?? value).trim().toUpperCase() || null
@@ -349,12 +360,12 @@ function SaxoLinkPicker({ ticker, saxoSymbol }: { ticker: string; saxoSymbol: st
if (!editing) {
return saxoSymbol ? (
<button onClick={() => { setEditing(true); setValue(saxoSymbol) }} className="flex items-center gap-1 text-[9px] text-emerald-400 hover:text-emerald-300 bg-emerald-900/20 border border-emerald-700/30 px-1.5 py-0.5 rounded">
<Link2 className="w-2.5 h-2.5" /> {saxoSymbol}
<button onClick={() => { setEditing(true); setValue(saxoSymbol) }} className={clsx('flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border', meta.activeCls)}>
<Link2 className="w-2.5 h-2.5" /> {meta.label}: {saxoSymbol}
</button>
) : (
<button onClick={() => setEditing(true)} className="flex items-center gap-1 text-[9px] text-slate-500 hover:text-slate-300 border border-slate-700/40 px-1.5 py-0.5 rounded">
<Unlink className="w-2.5 h-2.5" /> Link Saxo
<Unlink className="w-2.5 h-2.5" /> {meta.label}
</button>
)
}
@@ -368,7 +379,7 @@ function SaxoLinkPicker({ ticker, saxoSymbol }: { ticker: string; saxoSymbol: st
onFocus={() => setShowDropdown(true)}
onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') setEditing(false) }}
onBlur={() => save()}
placeholder="Search Saxo symbol…"
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 && <span className="text-[9px] text-slate-600 ml-1"></span>}
@@ -461,7 +472,8 @@ function WatchlistCard() {
<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">
<SaxoLinkPicker ticker={item.ticker} saxoSymbol={item.saxo_symbol ?? null} />
<SaxoLinkPicker ticker={item.ticker} kind="option" saxoSymbol={item.saxo_option_symbol ?? null} />
<SaxoLinkPicker ticker={item.ticker} kind="quote" saxoSymbol={item.saxo_quote_symbol ?? null} />
<button onClick={() => removeTicker(item.ticker)} className="text-slate-600 hover:text-red-400 transition-colors">
<X className="w-3.5 h-3.5" />
</button>