|
|
|
|
@@ -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, useSetWatchlistSaxoOptionLink, useSetWatchlistSaxoQuoteLink, useRenameWatchlistInstrument, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, useExpandSaxoWatchlist, useWaveletRefreshSettings, useUpdateWaveletRefreshSettings, useWaveletRefreshNow, useAddManualInstrument, 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, useRenameWatchlistInstrument, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, useExpandSaxoWatchlist, useWaveletRefreshSettings, useUpdateWaveletRefreshSettings, useWaveletRefreshNow, useQuickAddInstrument, 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'
|
|
|
|
|
import SaxoLinkPicker from '../components/SaxoLinkPicker'
|
|
|
|
|
@@ -33,21 +33,6 @@ const SOURCE_DOCS: Record<string, { description: string; link?: string; cost: st
|
|
|
|
|
twitter_trump: { description: 'X/Twitter Trump feed — speeches, tariff announcements', cost: 'Paid API ($100/mo+)' },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mirrors InstrumentDashboard.tsx's CATEGORY_LABELS (kept in sync manually — a small,
|
|
|
|
|
// stable list, not worth sharing across the two pages).
|
|
|
|
|
const INSTRUMENT_CATEGORIES: { value: string; label: string }[] = [
|
|
|
|
|
{ value: 'fx', label: 'Forex' },
|
|
|
|
|
{ value: 'equity_index', label: 'Indices US' },
|
|
|
|
|
{ value: 'equity_intl', label: 'Intl Equity' },
|
|
|
|
|
{ value: 'metal', label: 'Métaux' },
|
|
|
|
|
{ value: 'energy', label: 'Énergie' },
|
|
|
|
|
{ value: 'bond', label: 'Obligataire' },
|
|
|
|
|
{ value: 'credit', label: 'Crédit' },
|
|
|
|
|
{ value: 'volatility', label: 'Volatilité' },
|
|
|
|
|
{ value: 'stock', label: 'Actions' },
|
|
|
|
|
{ value: 'crypto', label: 'Crypto' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// ── Risk Profiles Component ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const PROFILE_COLORS = [
|
|
|
|
|
@@ -516,11 +501,18 @@ function SaxoConnectionCard() {
|
|
|
|
|
const testQuote = useTestSaxoQuote()
|
|
|
|
|
const [quoteSymbol, setQuoteSymbol] = useState('EURUSD')
|
|
|
|
|
const [quoteAssetType, setQuoteAssetType] = useState('FxSpot')
|
|
|
|
|
const addManual = useAddManualInstrument()
|
|
|
|
|
const [addName, setAddName] = useState('')
|
|
|
|
|
const [addCategory, setAddCategory] = useState('fx')
|
|
|
|
|
const [addYfTicker, setAddYfTicker] = useState('')
|
|
|
|
|
// "Ajouter à Instrument Analysis" — reuses the Cockpit Watchlist (chips shown below,
|
|
|
|
|
// same table as the "Instruments Watchlist" tab) + its Saxo quote-link + the existing
|
|
|
|
|
// quick-add-from-watchlist flow, rather than a separate storage path: this is the only
|
|
|
|
|
// combination the Instrument Analysis picker (InstrumentDashboard.tsx's "Watchlist
|
|
|
|
|
// (Config)" dropdown) actually reads from.
|
|
|
|
|
const { data: instrumentWatchlistItems } = useInstrumentsWatchlist()
|
|
|
|
|
const addWatchlistTicker = useAddWatchlistInstrument()
|
|
|
|
|
const watchlistQuoteLink = useSetWatchlistSaxoQuoteLink()
|
|
|
|
|
const removeWatchlistTicker = useRemoveWatchlistInstrument()
|
|
|
|
|
const quickAddInstrument = useQuickAddInstrument()
|
|
|
|
|
const [addMsg, setAddMsg] = useState('')
|
|
|
|
|
const [addedInstrumentId, setAddedInstrumentId] = useState<string | null>(null)
|
|
|
|
|
const expandWatchlist = useExpandSaxoWatchlist()
|
|
|
|
|
const [expandKeyword, setExpandKeyword] = useState('')
|
|
|
|
|
const [expandMsg, setExpandMsg] = useState('')
|
|
|
|
|
@@ -582,29 +574,26 @@ function SaxoConnectionCard() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prefill the "add to Instrument Analysis" fields once a test resolves — the user can
|
|
|
|
|
// still override name/category/yf_ticker before actually adding it.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!testQuote.data) return
|
|
|
|
|
setAddName(testQuote.data.description ?? testQuote.data.symbol)
|
|
|
|
|
setAddYfTicker(quoteAssetType === 'FxSpot' ? `${testQuote.data.symbol}=X` : testQuote.data.symbol)
|
|
|
|
|
setAddCategory(quoteAssetType === 'FxSpot' ? 'fx' : quoteAssetType === 'StockIndex' ? 'equity_index' : 'stock')
|
|
|
|
|
setAddMsg('')
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [testQuote.data])
|
|
|
|
|
|
|
|
|
|
const handleAddManual = async () => {
|
|
|
|
|
// Adds the tested symbol to the Cockpit Watchlist (yfinance's "=X" form for FX, so
|
|
|
|
|
// yfinance actually recognizes it as forex and the fallback ticker is correct), links its
|
|
|
|
|
// Saxo quote symbol, then quick-adds it into the Instrument Analysis catalog — same three
|
|
|
|
|
// steps a user would otherwise do by hand across two different tabs.
|
|
|
|
|
const handleAddToInstrumentAnalysis = async () => {
|
|
|
|
|
if (!testQuote.data) return
|
|
|
|
|
setAddMsg('')
|
|
|
|
|
setAddedInstrumentId(null)
|
|
|
|
|
const resolvedSymbol = testQuote.data.symbol
|
|
|
|
|
const watchlistTicker = quoteAssetType === 'FxSpot' ? `${resolvedSymbol}=X` : resolvedSymbol
|
|
|
|
|
try {
|
|
|
|
|
const r = await addManual.mutateAsync({
|
|
|
|
|
id: testQuote.data.symbol,
|
|
|
|
|
name: addName.trim() || testQuote.data.description || testQuote.data.symbol,
|
|
|
|
|
category: addCategory,
|
|
|
|
|
yf_ticker: addYfTicker.trim() || undefined,
|
|
|
|
|
saxo_quote_symbol: testQuote.data.symbol,
|
|
|
|
|
})
|
|
|
|
|
setAddMsg(`✓ ${r.id} ajouté à Instrument Analysis`)
|
|
|
|
|
try {
|
|
|
|
|
await addWatchlistTicker.mutateAsync(watchlistTicker)
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
if (e?.response?.status !== 409) throw e // already in the Watchlist — fine, just (re)link it below
|
|
|
|
|
}
|
|
|
|
|
await watchlistQuoteLink.mutateAsync({ ticker: watchlistTicker, saxoSymbol: resolvedSymbol })
|
|
|
|
|
const r = await quickAddInstrument.mutateAsync(watchlistTicker)
|
|
|
|
|
setAddMsg(`✓ ${r.id} ${r.created ? 'ajouté' : 'déjà présent'} dans Instrument Analysis, lié à Saxo (${resolvedSymbol})`)
|
|
|
|
|
setAddedInstrumentId(r.id)
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
setAddMsg(`✗ ${e?.response?.data?.detail ?? 'échec'}`)
|
|
|
|
|
}
|
|
|
|
|
@@ -854,36 +843,14 @@ function SaxoConnectionCard() {
|
|
|
|
|
✓ {testQuote.data.symbol} ({testQuote.data.description}) — Bid {testQuote.data.bid} / Ask {testQuote.data.ask}
|
|
|
|
|
<span className="text-slate-600"> · {testQuote.data.price_source}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2 mt-2">
|
|
|
|
|
<input
|
|
|
|
|
value={addName}
|
|
|
|
|
onChange={e => setAddName(e.target.value)}
|
|
|
|
|
placeholder="Nom affiché"
|
|
|
|
|
className="bg-dark-800 border border-slate-700/40 rounded px-3 py-1.5 text-xs text-white w-40 focus:outline-none focus:border-blue-500/50"
|
|
|
|
|
/>
|
|
|
|
|
<select
|
|
|
|
|
value={addCategory}
|
|
|
|
|
onChange={e => setAddCategory(e.target.value)}
|
|
|
|
|
className="bg-dark-800 border border-slate-700/40 rounded px-2 py-1.5 text-xs text-white"
|
|
|
|
|
>
|
|
|
|
|
{INSTRUMENT_CATEGORIES.map(c => <option key={c.value} value={c.value}>{c.label}</option>)}
|
|
|
|
|
</select>
|
|
|
|
|
<input
|
|
|
|
|
value={addYfTicker}
|
|
|
|
|
onChange={e => setAddYfTicker(e.target.value.toUpperCase())}
|
|
|
|
|
placeholder="Ticker yfinance (fallback)"
|
|
|
|
|
title="Utilisé si Saxo devient indisponible pour ce ticker — format yfinance, ex. EURCHF=X"
|
|
|
|
|
className="bg-dark-800 border border-slate-700/40 rounded px-3 py-1.5 text-xs text-white w-44 focus:outline-none focus:border-blue-500/50"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleAddManual}
|
|
|
|
|
disabled={addManual.isPending}
|
|
|
|
|
title="Ajoute ce ticker à Instrument Analysis, lié à Saxo — pas besoin de toucher instruments.json"
|
|
|
|
|
className="flex items-center gap-1 px-3 py-1.5 rounded text-xs bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-40"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="w-3.5 h-3.5" /> Ajouter à Instrument Analysis
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleAddToInstrumentAnalysis}
|
|
|
|
|
disabled={addWatchlistTicker.isPending || watchlistQuoteLink.isPending || quickAddInstrument.isPending}
|
|
|
|
|
title="Ajoute au Cockpit Watchlist, lie le symbole Saxo testé ci-dessus, puis l'ajoute au catalogue Instrument Analysis"
|
|
|
|
|
className="flex items-center gap-1 px-3 py-1.5 rounded text-xs bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-40 mt-2"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="w-3.5 h-3.5" /> Ajouter à Instrument Analysis
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{testQuote.isError && (
|
|
|
|
|
@@ -891,7 +858,40 @@ function SaxoConnectionCard() {
|
|
|
|
|
✗ {(testQuote.error as any)?.response?.data?.detail ?? 'échec'}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{addMsg && <div className="text-xs mt-2" style={{ color: addMsg.startsWith('✓') ? '#34d399' : '#f87171' }}>{addMsg}</div>}
|
|
|
|
|
{addMsg && (
|
|
|
|
|
<div className="text-xs mt-2" style={{ color: addMsg.startsWith('✓') ? '#34d399' : '#f87171' }}>
|
|
|
|
|
{addMsg}
|
|
|
|
|
{addedInstrumentId && (
|
|
|
|
|
<>
|
|
|
|
|
{' — '}
|
|
|
|
|
<Link to={`/instruments/${encodeURIComponent(addedInstrumentId)}`} className="text-blue-400 hover:text-blue-300 underline">
|
|
|
|
|
Ouvrir dans Instrument Analysis
|
|
|
|
|
</Link>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{(instrumentWatchlistItems ?? []).length > 0 && (
|
|
|
|
|
<div className="mt-3 pt-2 border-t border-slate-700/20">
|
|
|
|
|
<div className="text-[10px] text-slate-600 mb-1.5">
|
|
|
|
|
Instruments Watchlist (disponibles dans Instrument Analysis via son sélecteur) :
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-wrap gap-1.5">
|
|
|
|
|
{(instrumentWatchlistItems as any[]).map(w => (
|
|
|
|
|
<span key={w.ticker} className={clsx('badge flex items-center gap-1.5', w.saxo_quote_symbol ? 'badge-green' : 'badge-blue')}
|
|
|
|
|
title={w.saxo_quote_symbol ? `Lié à Saxo: ${w.saxo_quote_symbol}` : "Pas encore lié à Saxo (bouton 'Tester' + 'Ajouter' ci-dessus)"}
|
|
|
|
|
>
|
|
|
|
|
{w.ticker}
|
|
|
|
|
{w.saxo_quote_symbol && <Link2 className="w-3 h-3" />}
|
|
|
|
|
<button onClick={() => removeWatchlistTicker.mutate(w.ticker)} className="hover:text-white">
|
|
|
|
|
<X className="w-3 h-3" />
|
|
|
|
|
</button>
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
|