feat: cockpit
This commit is contained in:
@@ -219,6 +219,16 @@ export const useSetInstrumentSaxoLink = () =>
|
||||
api.put(`/instruments/${encodeURIComponent(instrumentId)}/saxo-link`, { saxo_symbol: saxoSymbol }).then(r => r.data),
|
||||
})
|
||||
|
||||
// Brings a Cockpit Watchlist ticker into Instrument Analysis without hand-authoring an
|
||||
// instruments.json entry — its saxo_quote_symbol (already linked in Config -> Instruments
|
||||
// Watchlist) is copied over automatically. Idempotent: a ticker that already resolves
|
||||
// (real catalog entry or an earlier quick add) is returned unchanged.
|
||||
export const useQuickAddInstrument = () =>
|
||||
useMutation({
|
||||
mutationFn: (ticker: string) =>
|
||||
api.post('/instruments/quick-add', { ticker }).then(r => r.data as { id: string; created: boolean }),
|
||||
})
|
||||
|
||||
// Free-form display name — no longer tied to yfinance's long_name lookup, since an
|
||||
// instrument can now be entirely Saxo-sourced.
|
||||
export const useRenameWatchlistInstrument = () => {
|
||||
|
||||
@@ -482,15 +482,6 @@ export default function Dashboard() {
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{catalogId && (
|
||||
<Link
|
||||
to={`/instruments/${encodeURIComponent(catalogId)}`}
|
||||
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>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ import clsx from 'clsx'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||
import InstrumentChart, { TheoPoint } from '../components/InstrumentChart'
|
||||
import SaxoLinkPicker from '../components/SaxoLinkPicker'
|
||||
import { useSetInstrumentSaxoLink } from '../hooks/useApi'
|
||||
import { useSetInstrumentSaxoLink, useInstrumentsWatchlist, useQuickAddInstrument } from '../hooks/useApi'
|
||||
import { fmtPrice } from '../lib/format'
|
||||
|
||||
const api = axios.create({ baseURL: '/api' })
|
||||
@@ -197,6 +197,18 @@ const CATEGORY_LABELS: Record<string, string> = {
|
||||
fx: 'Forex', volatility: 'Volatilité', stock: 'Actions', crypto: 'Crypto',
|
||||
}
|
||||
|
||||
// Instrument Analysis's catalog keys FX pairs with yfinance's "=X" suffix (EURUSD=X),
|
||||
// while the Cockpit watchlist stores the plain ticker (EURUSD) — try both forms before
|
||||
// concluding a watchlist instrument has no catalog entry yet (same logic as Dashboard.tsx's
|
||||
// resolveCatalogId, duplicated here since the two pages don't share a components module).
|
||||
function resolveWatchlistCatalogId(ticker: string, catalog: { id: string }[]): string | undefined {
|
||||
const upper = ticker.toUpperCase()
|
||||
const ids = new Set(catalog.map(i => i.id.toUpperCase()))
|
||||
if (ids.has(upper)) return upper
|
||||
if (ids.has(`${upper}=X`)) return `${upper}=X`
|
||||
return undefined
|
||||
}
|
||||
|
||||
function regimeColor(label: string): string {
|
||||
const l = label.toLowerCase()
|
||||
if (/bull|expan|recov|rally|strength|risk.on|inflow|demand|falling|weakness/i.test(l)) return 'emerald'
|
||||
@@ -1424,6 +1436,9 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
const [chartStyle, setChartStyle] = useState<'candles' | 'line'>('candles')
|
||||
const [instruments, setInstruments] = useState<InstrumentConfig[]>([])
|
||||
const setSaxoLink = useSetInstrumentSaxoLink()
|
||||
const { data: watchlistItems } = useInstrumentsWatchlist()
|
||||
const quickAdd = useQuickAddInstrument()
|
||||
const [addingTicker, setAddingTicker] = useState<string | null>(null)
|
||||
const [snapshot, setSnapshot] = useState<Snapshot | null>(null)
|
||||
const [narrative, setNarrative] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -1635,6 +1650,12 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
items: instruments.filter(i => i.category === cat),
|
||||
})).filter(g => g.items.length > 0)
|
||||
|
||||
// Cockpit Watchlist tickers that don't already resolve to a catalog entry — offered as
|
||||
// "quick add" picks (see resolveWatchlistCatalogId below for the resolution itself).
|
||||
// Excludes anything already resolvable so picking it never creates a duplicate id.
|
||||
const watchlistCandidates = ((watchlistItems as any[]) ?? [])
|
||||
.filter(w => !resolveWatchlistCatalogId(w.ticker, instruments))
|
||||
|
||||
const selected = instruments.find(i => i.id === instrumentId)
|
||||
const isLastDate = effectiveDate === sortedDates[sortedDates.length - 1]
|
||||
const dateLabel = effectiveDate ? fmtDateFR(effectiveDate) : '—'
|
||||
@@ -1692,6 +1713,32 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
})
|
||||
}
|
||||
|
||||
// Picking a Watchlist instrument that has no catalog entry yet: quick-add it (copies
|
||||
// its saxo_quote_symbol over automatically, see services.instrument_service.
|
||||
// quick_add_instrument_from_watchlist) then navigate straight there. Already-resolvable
|
||||
// tickers (e.g. EURUSD -> EURUSD=X) skip the round-trip and navigate directly.
|
||||
const handleSelectWatchlist = (ticker: string) => {
|
||||
const existing = resolveWatchlistCatalogId(ticker, instruments)
|
||||
if (existing) {
|
||||
navigate(`/instruments/${encodeURIComponent(existing)}`)
|
||||
setSelectorOpen(false)
|
||||
return
|
||||
}
|
||||
setAddingTicker(ticker)
|
||||
quickAdd.mutate(ticker, {
|
||||
onSuccess: ({ id }) => {
|
||||
api.get('/instruments').then(r => setInstruments(r.data)).catch(() => {})
|
||||
navigate(`/instruments/${encodeURIComponent(id)}`)
|
||||
setSelectorOpen(false)
|
||||
setAddingTicker(null)
|
||||
},
|
||||
onError: (e) => {
|
||||
console.error('Quick-add from watchlist failed', e)
|
||||
setAddingTicker(null)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const waveletChartData = useMemo(() => {
|
||||
if (!waveletData) return []
|
||||
const { dates, original, bands, mean } = waveletData
|
||||
@@ -1917,6 +1964,27 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{watchlistCandidates.length > 0 && (
|
||||
<div className="mt-1 pt-2 border-t border-slate-700/40">
|
||||
<div className="text-xs text-slate-500 uppercase tracking-wide mb-1.5 px-1">Depuis la Watchlist</div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{watchlistCandidates.map((w: any) => (
|
||||
<button key={w.ticker}
|
||||
onClick={() => handleSelectWatchlist(w.ticker)}
|
||||
disabled={addingTicker === w.ticker}
|
||||
className="text-left px-2.5 py-1.5 rounded-lg text-xs transition-colors text-slate-400 hover:bg-dark-700 hover:text-white disabled:opacity-50"
|
||||
title={w.saxo_quote_symbol ? `Lien Saxo auto: ${w.saxo_quote_symbol}` : 'Pas encore lié à Saxo (Config → Instruments Watchlist)'}
|
||||
>
|
||||
<div className="font-semibold flex items-center gap-1">
|
||||
{w.ticker}
|
||||
{w.saxo_quote_symbol && <Link2 className="w-2.5 h-2.5 text-emerald-500 shrink-0" />}
|
||||
</div>
|
||||
<div className="text-slate-500 truncate">{addingTicker === w.ticker ? 'Ajout…' : (w.name || w.ticker)}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user