From 9cf3086254310160e980babb33047d534339a410 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Thu, 23 Jul 2026 20:38:18 +0200 Subject: [PATCH] feat; cockpit --- frontend/src/hooks/useApi.ts | 10 +++++++ frontend/src/pages/Dashboard.tsx | 45 ++++++++++++++++++++----------- frontend/src/pages/OptionsLab.tsx | 19 ++++++++++--- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index 0a5f619..3484229 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -140,6 +140,16 @@ export const useAddWatchlistInstrument = () => { }) } +// Instrument Analysis runs on its own separate catalog (backend config/instruments.json, +// not services.database.instruments_watchlist) โ€” used to gate navigation links so we +// don't send the user to a 404/error page for a ticker that isn't registered there. +export const useInstrumentCatalogIds = () => + useQuery({ + queryKey: ['instrument-analysis-catalog'], + queryFn: () => api.get('/instruments/').then(r => new Set((r.data ?? []).map((i: any) => String(i.id).toUpperCase()))), + staleTime: 10 * 60_000, + }) + export const useWatchlistHistory = (ticker: string, period: string) => useQuery({ queryKey: ['instruments-watchlist-history', ticker, period], diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 6bb4964..0b8bbf1 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -5,7 +5,7 @@ import { useEcoCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime, useTradeMtm, useRiskDashboard, useGeoNews, useSimPortfolioRisk, usePortfolioRiskRadar, useCycleStatus, useClosedTrades, - useInstrumentsWatchlist, useInstrumentsWatchlistQuotes, useWatchlistHistory, useSaxoIvWatchlist, useLatestCycleReport, + useInstrumentsWatchlist, useInstrumentsWatchlistQuotes, useWatchlistHistory, useInstrumentCatalogIds, useSaxoIvWatchlist, useLatestCycleReport, useWaveletWatchlistSignals, } from '../hooks/useApi' import { Clock, Globe, ShieldAlert, ArrowUpRight, Newspaper, Waves, Link2 } from 'lucide-react' @@ -155,9 +155,11 @@ export default function Dashboard() { const { data: geoNews } = useGeoNews() const { data: watchlistItems } = useInstrumentsWatchlist() const { data: watchlistQuotesData } = useInstrumentsWatchlistQuotes() + const { data: instrumentCatalogIds } = useInstrumentCatalogIds() const [watchlistChartTicker, setWatchlistChartTicker] = useState(null) const [watchlistChartPeriod, setWatchlistChartPeriod] = useState('3m') const activeWatchlistTicker = watchlistChartTicker ?? (watchlistItems as any)?.[0]?.ticker ?? '' + const activeWatchlistName = ((watchlistItems as any) ?? []).find((w: any) => w.ticker === activeWatchlistTicker)?.name || activeWatchlistTicker const { data: watchlistHistoryData, isLoading: watchlistHistoryLoading } = useWatchlistHistory(activeWatchlistTicker, watchlistChartPeriod) const { data: latestCycleReportData } = useLatestCycleReport() const { data: waveletSignalsData } = useWaveletWatchlistSignals() @@ -424,7 +426,7 @@ export default function Dashboard() { {((watchlistQuotesData as any)?.items ?? []).length > 0 ? ( <>
- {activeWatchlistTicker} + {activeWatchlistName}
{['1w', '1m', '3m', '6m', '1y', '5y', 'max'].map(p => (
{fmtPrice(it.price)} = 0 ? 'text-emerald-400' : 'text-red-400')}> @@ -489,8 +495,17 @@ export default function Dashboard() { )} + {(instrumentCatalogIds as Set | undefined)?.has(it.ticker.toUpperCase()) && ( + + + + )}
- +
))}
@@ -1017,7 +1032,7 @@ export default function Dashboard() { const bySaxoSymbol: Record = {} for (const item of ((saxoIvWatchlistData as any)?.items ?? [])) bySaxoSymbol[item.ticker] = item const highlights = linked - .map((w: any) => bySaxoSymbol[w.saxo_option_symbol] ? { ...bySaxoSymbol[w.saxo_option_symbol], watchlistTicker: w.ticker } : null) + .map((w: any) => bySaxoSymbol[w.saxo_option_symbol] ? { ...bySaxoSymbol[w.saxo_option_symbol], watchlistTicker: w.ticker, watchlistName: w.name || w.ticker } : null) .filter(Boolean) .sort((a: any, b: any) => Math.abs((b.iv_rank ?? 50) - 50) - Math.abs((a.iv_rank ?? 50) - 50)) @@ -1043,7 +1058,7 @@ export default function Dashboard() {
{rank != null && rank > 80 ? '๐Ÿ”ด' : rank != null && rank < 20 ? '๐ŸŸข' : 'โšช'} - {h.watchlistTicker} + {h.watchlistName} IVR {rank != null ? rank.toFixed(0) : 'โ€”'} {ivCur != null && ยท IV {ivCur.toFixed(1)}%} {ivChg != null && Math.abs(ivChg) >= 0.1 && ( diff --git a/frontend/src/pages/OptionsLab.tsx b/frontend/src/pages/OptionsLab.tsx index 4642d09..1abbdf9 100644 --- a/frontend/src/pages/OptionsLab.tsx +++ b/frontend/src/pages/OptionsLab.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { useIvWatchlist, useIvSnapshot, useIvHistory, useWatchlistTickers, useAddWatchlistTicker, useRemoveWatchlistTicker, - useSaxoIvWatchlist, useSaxoIvSnapshot, useSaxoIvHistory, + useSaxoIvWatchlist, useSaxoIvSnapshot, useSaxoIvHistory, useInstrumentsWatchlist, } from '../hooks/useApi' import { Activity, TrendingUp, TrendingDown, Minus, RefreshCw, ChevronDown, ChevronUp, Database, Plus, Trash2, List, Link2 } from 'lucide-react' import { api } from '../hooks/useApi' @@ -369,8 +369,8 @@ function SaxoWatchlistRow({ item }: { item: any }) { className="flex items-center gap-3 px-3 py-2.5 cursor-pointer" onClick={() => setExpanded(!expanded)} > -
-
{item.ticker}
+
+
{item.displayName ?? item.ticker}
Saxo
@@ -563,7 +563,18 @@ export default function OptionsLab() { // const needsBootstrap = items.length > 0 && items.filter(i => i.iv_rank == null || i.iv_rank === 50).length > items.length * 0.6 const { data: saxoData, isLoading: saxoLoading, refetch: refetchSaxo, isFetching: saxoFetching } = useSaxoIvWatchlist() - const saxoItems: any[] = saxoData?.items || [] + const { data: watchlistInstruments } = useInstrumentsWatchlist() + // Cross-reference the Saxo IV watchlist (keyed by Saxo option symbol, e.g. "MCLU6") + // against the tracked instruments (keyed by our own ticker, e.g. "CL=F") to show the + // friendly/renamed name instead of the raw Saxo symbol. + const nameBySaxoOptionSymbol: Record = {} + for (const w of ((watchlistInstruments as any[]) ?? [])) { + if (w.saxo_option_symbol) nameBySaxoOptionSymbol[w.saxo_option_symbol] = w.name || w.ticker + } + const saxoItems: any[] = (saxoData?.items || []).map((item: any) => ({ + ...item, + displayName: nameBySaxoOptionSymbol[item.ticker] || item.ticker, + })) return (