feat; cockpit
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user