feat; cockpit

This commit is contained in:
OpenSquared
2026-07-23 20:38:18 +02:00
parent 6eba6ce5f8
commit 9cf3086254
3 changed files with 55 additions and 19 deletions

View File

@@ -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],

View File

@@ -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<string | null>(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 ? (
<>
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] font-mono font-bold text-white">{activeWatchlistTicker}</span>
<span className="text-[10px] font-bold text-white truncate max-w-[130px]" title={activeWatchlistTicker}>{activeWatchlistName}</span>
<div className="flex gap-0.5">
{['1w', '1m', '3m', '6m', '1y', '5y', 'max'].map(p => (
<button
@@ -453,29 +455,33 @@ export default function Dashboard() {
<YAxis domain={['auto', 'auto']} hide />
<Tooltip
contentStyle={{ background: '#0f172a', border: '1px solid #334155', borderRadius: 6, fontSize: 10, padding: '4px 8px' }}
formatter={(v: any) => [fmtPrice(v), activeWatchlistTicker]}
formatter={(v: any) => [fmtPrice(v), activeWatchlistName]}
/>
<Area type="monotone" dataKey="close" stroke="#3b82f6" strokeWidth={1.5} fill="url(#wlChartGrad)" isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
) : (
<div className="h-[110px] flex items-center justify-center text-slate-600 text-[10px]">No chart data for {activeWatchlistTicker}</div>
<div className="h-[110px] flex items-center justify-center text-slate-600 text-[10px]">No chart data for {activeWatchlistName}</div>
)}
<div className="mt-1.5 pt-1.5 border-t border-slate-700/30 space-y-0.5">
{((watchlistQuotesData as any)?.items ?? []).map((it: any) => (
<button
<div
key={it.ticker}
type="button"
onClick={() => setWatchlistChartTicker(it.ticker)}
className={clsx(
'w-full flex items-center justify-between gap-1.5 text-[10px] whitespace-nowrap rounded px-1 -mx-1 py-0.5 text-left transition-colors',
'flex items-center justify-between gap-1.5 text-[10px] whitespace-nowrap rounded px-1 -mx-1 py-0.5 transition-colors',
it.ticker === activeWatchlistTicker ? 'bg-blue-900/20' : 'hover:bg-dark-700/40'
)}
>
<span className={clsx('font-mono shrink-0 flex items-center gap-1', it.ticker === activeWatchlistTicker ? 'text-white font-bold' : 'text-slate-300')}>
{it.ticker}
{it.quote_source === 'saxo' && <span className="text-emerald-500" title="Priced from Saxo, not yfinance"></span>}
</span>
<button
type="button"
onClick={() => setWatchlistChartTicker(it.ticker)}
className="flex items-center gap-1 min-w-0 flex-1 text-left"
>
<span className={clsx('truncate', it.ticker === activeWatchlistTicker ? 'text-white font-bold' : 'text-slate-300')} title={it.ticker}>
{it.name || it.ticker}
</span>
{it.quote_source === 'saxo' && <span className="text-emerald-500 shrink-0" title="Priced from Saxo, not yfinance"></span>}
</button>
<div className="flex items-center gap-1.5 shrink-0">
<span className="text-slate-400 font-mono">{fmtPrice(it.price)}</span>
<span className={clsx('font-mono font-bold w-11 text-right shrink-0', (it.change_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
@@ -489,8 +495,17 @@ export default function Dashboard() {
</span>
)}
</span>
{(instrumentCatalogIds as Set<string> | undefined)?.has(it.ticker.toUpperCase()) && (
<Link
to={`/instruments/${encodeURIComponent(it.ticker)}`}
className="text-slate-600 hover:text-blue-400 shrink-0 transition-colors"
title={`Open ${it.name || it.ticker} in Instrument Analysis`}
>
<ArrowUpRight className="w-3 h-3" />
</Link>
)}
</div>
</button>
</div>
))}
</div>
</>
@@ -1017,7 +1032,7 @@ export default function Dashboard() {
const bySaxoSymbol: Record<string, any> = {}
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() {
<div key={h.ticker} className="pb-1 border-b border-slate-700/20 last:border-0">
<div className="flex items-center gap-1.5 text-[10px]">
<span>{rank != null && rank > 80 ? '🔴' : rank != null && rank < 20 ? '🟢' : '⚪'}</span>
<span className="font-mono text-slate-200 font-bold shrink-0">{h.watchlistTicker}</span>
<span className="text-slate-200 font-bold shrink-0 truncate max-w-[90px]" title={h.watchlistTicker}>{h.watchlistName}</span>
<span className="text-slate-500">IVR {rank != null ? rank.toFixed(0) : '—'}</span>
{ivCur != null && <span className="text-slate-600">· IV {ivCur.toFixed(1)}%</span>}
{ivChg != null && Math.abs(ivChg) >= 0.1 && (

View File

@@ -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)}
>
<div className="w-14 shrink-0">
<div className="text-sm font-bold text-slate-200">{item.ticker}</div>
<div className="w-20 shrink-0">
<div className="text-sm font-bold text-slate-200 truncate" title={item.ticker}>{item.displayName ?? item.ticker}</div>
<div className="text-[8px] text-slate-500 flex items-center gap-0.5"><Link2 className="w-2 h-2" /> Saxo</div>
</div>
@@ -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<string, string> = {}
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 (
<div className="p-6 space-y-5">