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, validateTicker, useSaxoStatus, useDisconnectSaxo, useSaxoWatchlist, useUpdateSaxoWatchlist, useSnapshotSaxoNow, useValidateSaxoWatchlist, useSaxoCatalog, useSaxoCatalogSummary, useRefreshSaxoCatalog, useTestSaxoQuote, useSaxoSettings, useUpdateSaxoSettings, useSnapshotAllSaxoNow, useExpandSaxoWatchlist, 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' const API = '' const SOURCE_CATEGORIES = { 'Active RSS Feeds': ['reuters_world', 'reuters_business', 'reuters_energy', 'ap_top', 'aljazeera', 'ft', 'bloomberg'], 'Economic Data': ['newsapi', 'gdelt', 'eia', 'fred', 'usda'], 'Health & Disasters': ['who', 'emdat'], 'Social Media': ['twitter_trump'], } const SOURCE_DOCS: Record = { reuters_world: { description: 'Reuters World News', cost: 'Free' }, reuters_business: { description: 'Reuters Business & Markets', cost: 'Free' }, reuters_energy: { description: 'Reuters Energy & Commodities', cost: 'Free' }, ap_top: { description: 'Associated Press — Top Stories', cost: 'Free' }, aljazeera: { description: 'Al Jazeera — Middle East/World coverage', cost: 'Free' }, ft: { description: 'Financial Times — international finance', cost: 'Free' }, bloomberg: { description: 'Bloomberg Markets RSS', cost: 'Free' }, newsapi: { description: '100 req/day free — multi-source world news', link: 'https://newsapi.org', cost: '100 req/d free' }, gdelt: { description: 'Global geopolitical database — 300K events/day', link: 'https://gdeltproject.org', cost: 'Totally free' }, eia: { description: 'US Energy Information Administration — weekly oil/gas data', link: 'https://www.eia.gov/opendata', cost: 'Free with key' }, fred: { description: 'Federal Reserve St. Louis — US macro (GDP, inflation, employment)', link: 'https://fred.stlouisfed.org/docs/api/fred/', cost: 'Free with key' }, usda: { description: 'USDA — official US agricultural reports', cost: 'Free' }, who: { description: 'WHO — global health alerts RSS', cost: 'Free' }, emdat: { description: 'EM-DAT — natural disasters database', link: 'https://www.emdat.be', cost: 'Free registration' }, twitter_trump: { description: 'X/Twitter Trump feed — speeches, tariff announcements', cost: 'Paid API ($100/mo+)' }, } // ── Risk Profiles Component ─────────────────────────────────────────────────── const PROFILE_COLORS = [ { value: '#22c55e', label: 'Green' }, { value: '#3b82f6', label: 'Blue' }, { value: '#f97316', label: 'Orange' }, { value: '#ef4444', label: 'Red' }, { value: '#8b5cf6', label: 'Purple' }, { value: '#eab308', label: 'Yellow' }, ] function NextRunCountdown({ nextRunAt }: { nextRunAt: string }) { const [remaining, setRemaining] = useState('') useEffect(() => { function update() { const diff = new Date(nextRunAt + 'Z').getTime() - Date.now() if (diff <= 0) { setRemaining('Imminent…'); return } const h = Math.floor(diff / 3_600_000) const m = Math.floor((diff % 3_600_000) / 60_000) const s = Math.floor((diff % 60_000) / 1_000) setRemaining(h > 0 ? `${h}h ${m.toString().padStart(2,'0')}min` : `${m}min ${s.toString().padStart(2,'0')}s`) } update() const id = setInterval(update, 1_000) return () => clearInterval(id) }, [nextRunAt]) const absTime = new Date(nextRunAt + 'Z').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) return (
Next cycle in {remaining} ({absTime} local time)
) } function evNetLabel(evNet: number): { text: string; cls: string } { if (evNet > 0.05) return { text: `Net EV +${(evNet * 100).toFixed(0)}%`, cls: 'text-emerald-400' } if (evNet >= -0.01) return { text: 'Net EV ≈ 0', cls: 'text-yellow-400' } return { text: `Net EV ${(evNet * 100).toFixed(0)}%`, cls: 'text-slate-600' } } function ProfileRow({ profile, onSave, onDelete, }: { profile: any onSave: (p: any) => void onDelete: (id: number) => void }) { const [editing, setEditing] = useState(false) const [name, setName] = useState(profile.name) const [score, setScore] = useState(profile.min_score) const [gain, setGain] = useState(profile.min_gain_pct) const [color, setColor] = useState(profile.color ?? '#3b82f6') const [enabled, setEnabled] = useState(profile.enabled !== 0) // Live EV computation const p = score / 100 const G = gain / 100 const denom = p * G + (1 - p) const ev_net = p * G - (1 - p) const trade_score = denom > 0 ? (p * G / denom * 100) : 0 const { text: evText, cls: evCls } = evNetLabel(ev_net) const save = () => { onSave({ id: profile.id, name, min_score: score, min_gain_pct: gain, color, enabled, sort_order: profile.sort_order ?? 0 }) setEditing(false) } if (!editing) { const { text: fev, cls: fevc } = evNetLabel(profile.ev_net_at_frontier ?? 0) return (
{profile.name} {!enabled && disabled} Score ≥ {profile.min_score} Gain ≥ {profile.min_gain_pct}% {fev} Trade Score ≥ {(profile.trade_score_at_frontier ?? 0).toFixed(0)}
) } return (
setName(e.target.value)} className="w-full bg-dark-800 border border-slate-700 rounded px-2 py-1 text-sm text-white" />
setScore(parseInt(e.target.value))} className="w-full accent-blue-500" />
setGain(parseFloat(e.target.value))} className="w-full accent-blue-500" />
{/* Live EV preview */}
At the frontier (score={score}, gain={gain}%) {evText} Trade Score = {trade_score.toFixed(1)}/100 Score min EV=0 : {Math.ceil(100 / (G + 1))}/100
{PROFILE_COLORS.map(c => (
) } function RiskProfilesCard() { const { data: profileData, isLoading } = useRiskProfiles() const { mutate: upsert } = useUpsertProfile() const { mutate: del } = useDeleteProfile() const [showNew, setShowNew] = useState(false) const [newName, setNewName] = useState('') const [newScore, setNewScore] = useState(30) const [newGain, setNewGain] = useState(200) const [newColor, setNewColor] = useState('#3b82f6') const profiles: any[] = (profileData as any)?.profiles ?? [] // Live preview for new profile const np = newScore / 100 const nG = newGain / 100 const nDenom = np * nG + (1 - np) const nEvNet = np * nG - (1 - np) const nTradeScore = nDenom > 0 ? (np * nG / nDenom * 100) : 0 const nMinScore = Math.ceil(100 / (nG + 1)) const saveNew = () => { if (!newName.trim()) return upsert({ name: newName, min_score: newScore, min_gain_pct: newGain, color: newColor, enabled: true, sort_order: profiles.length }) setShowNew(false) setNewName('') setNewScore(30) setNewGain(200) } return (
Risk Profiles
A trade is logged in the journal if it passes at least one enabled profile · Formula: Net EV = (score/100 × gain/100) − (1 − score/100)
{isLoading ? (
) : profiles.length === 0 ? (
No profiles — all trades will be ignored
) : (
{profiles.map((p: any) => ( upsert(data)} onDelete={id => del(id)} /> ))}
)} {/* New profile form */} {showNew && (
New profile
setNewName(e.target.value)} placeholder="e.g. Ultra-aggressive" className="w-full bg-dark-800 border border-slate-700 rounded px-2 py-1 text-sm text-white placeholder:text-slate-700" />
setNewScore(parseInt(e.target.value))} className="w-full accent-blue-500" />
setNewGain(parseFloat(e.target.value))} className="w-full accent-blue-500" />
{/* Live math */}
{evNetLabel(nEvNet).text} Trade Score = {nTradeScore.toFixed(1)}/100 Min score for EV=0 with gain {newGain}% : {nMinScore}/100
{PROFILE_COLORS.map(c => (
)} {/* Frontier visualization */} {profiles.length > 0 && (
Acceptance frontier — each point represents the minimum required per profile
{profiles.filter((p: any) => p.enabled).map((p: any) => { const { text: ev, cls } = evNetLabel(p.ev_net_at_frontier ?? 0) return (
{p.name} {p.min_score}pts × {p.min_gain_pct}% → {ev}
) })}
)}
) } function WatchlistCard() { const { data: items, isLoading } = useInstrumentsWatchlist() const { mutateAsync: addTicker, isPending: adding } = useAddWatchlistInstrument() const { mutate: removeTicker } = useRemoveWatchlistInstrument() const [input, setInput] = useState('') const [error, setError] = useState('') const handleAdd = async () => { const sym = input.trim().toUpperCase() if (!sym) return setError('') const check = await validateTicker(sym) if (!check.valid) { setError(check.reason ?? 'Ticker not found') return } try { await addTicker(sym) setInput('') } catch (e: unknown) { const msg = (e as any)?.response?.data?.detail ?? (e as { message?: string })?.message ?? 'Could not add ticker' setError(msg) } } const list: any[] = items ?? [] return (
Instruments Watchlist
Instruments surveillés pour le radar du Dashboard et les highlights Options Lab — liste indépendante des autres watchlists (Markets, Options Lab, InstrumentModels).
setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} placeholder="Ticker (ex. AAPL, EURUSD=X, ^GSPC)" className="bg-dark-800 border border-slate-700/40 rounded px-3 py-1.5 text-xs text-white w-64 focus:outline-none focus:border-blue-500/50" />
{error &&
{error}
} {isLoading ? (
Loading…
) : list.length === 0 ? (
No instrument watched yet.
) : (
{list.map(item => (
{item.ticker} {item.name} {item.asset_class}
))}
)}
) } // ── Saxo OpenAPI connection ──────────────────────────────────────────────────── function timeUntil(iso?: string): string { if (!iso) return '' const ms = new Date(iso).getTime() - Date.now() if (ms <= 0) return 'expiré' const min = Math.round(ms / 60000) return min < 60 ? `${min} min` : `${(min / 60).toFixed(1)} h` } function SaxoConnectionCard() { const qc = useQueryClient() const { data: status } = useSaxoStatus() const { data: watchlistData } = useSaxoWatchlist() const disconnect = useDisconnectSaxo() const updateWatchlist = useUpdateSaxoWatchlist() const snapshotNow = useSnapshotSaxoNow() const { data: validation, refetch: runValidation, isFetching: validating } = useValidateSaxoWatchlist() const { data: catalogSummary } = useSaxoCatalogSummary() const refreshCatalog = useRefreshSaxoCatalog() const { data: settings } = useSaxoSettings() const updateSettings = useUpdateSaxoSettings() const snapshotAllNow = useSnapshotAllSaxoNow() const [input, setInput] = useState('') const [snapMsg, setSnapMsg] = useState('') const { data: catalogMatches } = useSaxoCatalog(undefined, input.length >= 2 ? input : undefined) const testQuote = useTestSaxoQuote() const [quoteSymbol, setQuoteSymbol] = useState('EURUSD') const [quoteAssetType, setQuoteAssetType] = useState('FxSpot') const expandWatchlist = useExpandSaxoWatchlist() const [expandKeyword, setExpandKeyword] = useState('') const [expandMsg, setExpandMsg] = useState('') useEffect(() => { const params = new URLSearchParams(window.location.search) if (params.has('saxo_connected') || params.has('saxo_error')) { qc.invalidateQueries({ queryKey: ['saxo-status'] }) setSnapMsg(params.has('saxo_connected') ? '✓ Connecté à Saxo' : "✗ Échec de la connexion Saxo — vérifiez l'App Secret dans backend/.env") params.delete('saxo_connected'); params.delete('saxo_error') const qs = params.toString() window.history.replaceState({}, '', window.location.pathname + (qs ? `?${qs}` : '')) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const symbols = watchlistData?.symbols ?? [] const addSymbol = () => { const sym = input.trim().toUpperCase() if (!sym || symbols.includes(sym)) return updateWatchlist.mutate([...symbols, sym]) setInput('') } // Guarded by isPending everywhere it's called from the UI: each call captures `symbols` // from the current render, so firing a second mutation before the first one's // invalidateQueries/refetch completes reads a stale list and undoes the first removal // once it resolves — confirmed as the cause of rapid multi-click removals "coming back". const removeSymbol = (sym: string) => updateWatchlist.mutate(symbols.filter(s => s !== sym)) const clearAllSymbols = () => { if (symbols.length === 0) return if (!window.confirm(`Effacer les ${symbols.length} ticker(s) de la watchlist Saxo ?`)) return updateWatchlist.mutate([]) } const handleExpandWatchlist = async () => { const kw = expandKeyword.trim() if (!kw) return setExpandMsg('') try { const r = await expandWatchlist.mutateAsync(kw) setExpandMsg(r.added.length ? `✓ ${r.added.length} ticker(s) ajouté(s) : ${r.added.join(', ')}` : `○ Aucun nouveau ticker pour "${kw}" (déjà présents, ou rafraîchissez le catalogue)`) setExpandKeyword('') } catch (e: any) { setExpandMsg(`✗ ${e?.response?.data?.detail ?? 'échec'}`) } } const handleSnapshotNow = async (sym: string) => { setSnapMsg('') try { const r = await snapshotNow.mutateAsync(sym) setSnapMsg(`✓ ${sym} — ${r.rows_saved} lignes enregistrées`) } catch (e: any) { setSnapMsg(`✗ ${sym} — ${e?.response?.data?.detail ?? 'échec'}`) } } const handleSnapshotAllNow = async () => { setSnapMsg('') try { const r = await snapshotAllNow.mutateAsync() const total = Object.values(r.results).reduce((a, b) => a + b, 0) setSnapMsg(`✓ Watchlist entière — ${total} lignes enregistrées (${Object.keys(r.results).length} symboles)`) } catch (e: any) { setSnapMsg(`✗ ${e?.response?.data?.detail ?? 'échec'}`) } } return (
Saxo OpenAPI ({(status?.environment ?? 'sim').toUpperCase()})
{status?.connected ? 'Connecté' : 'Déconnecté'}
{!status?.configured && (
SAXO_APP_KEY / SAXO_APP_SECRET manquants dans backend/.env — ajoutez-les puis redémarrez le backend.
)} {status?.connected && (
Token valide encore {timeUntil(status.expires_at)}
Rafraîchi automatiquement en tâche de fond — pas besoin de te reconnecter.
)}
{status?.connected ? ( ) : ( Se connecter à Saxo )}
Watchlist snapshotée toutes les updateSettings.mutate(parseFloat(e.target.value) || 5)} className="w-14 bg-dark-800 border border-slate-700/40 rounded px-1.5 py-0.5 text-xs text-white text-center" /> min (historique Date/Spot/Expiry/Strike/Bid/Ask/Mid/VolatilityPct/Greeks) :
Historique
Catalogue local (Futures/FX — tickers Saxo non-évidents, ex. Or = OG:xcme) :{' '} {(catalogSummary ?? []).map(s => `${s.asset_type} (${s.count})`).join(' · ') || 'vide'}
setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && addSymbol()} placeholder="Symbole (ex. SPY, ou tape pour chercher dans le catalogue)" list="saxo-catalog-datalist" className="bg-dark-800 border border-slate-700/40 rounded px-3 py-1.5 text-xs text-white w-72 focus:outline-none focus:border-blue-500/50" /> {(catalogMatches ?? []).map(m => ( ))}
setExpandKeyword(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleExpandWatchlist()} placeholder="Mot-clé sous-jacent (ex. EURUSD, Gold, Brent)" className="bg-dark-800 border border-slate-700/40 rounded px-3 py-1.5 text-xs text-white w-72 focus:outline-none focus:border-blue-500/50" />
{expandMsg &&
{expandMsg}
} {symbols.length > 0 && (
)}
{symbols.map(sym => { const check = validation?.find(v => v.symbol === sym.toUpperCase()) return ( {sym} {check && (check.valid ? : )} ) })}
{snapMsg &&
{snapMsg}
}
Tester un prix hors options (isole si un blocage est spécifique aux options ou plus large) :
setQuoteSymbol(e.target.value.toUpperCase())} placeholder="EURUSD" className="bg-dark-800 border border-slate-700/40 rounded px-3 py-1.5 text-xs text-white w-32 focus:outline-none focus:border-blue-500/50" />
{testQuote.isSuccess && (
✓ {testQuote.data.symbol} ({testQuote.data.description}) — Bid {testQuote.data.bid} / Ask {testQuote.data.ask} · {testQuote.data.price_source}
)} {testQuote.isError && (
✗ {(testQuote.error as any)?.response?.data?.detail ?? 'échec'}
)}
) } // ── Cycle step config — generic renderer driven by GET /api/cycle/step-catalog ─ // Same declarative pattern as AI Desks' SIGNAL_CATALOG/SignalToggle: adding a new // knob to CYCLE_STEP_CATALOG (backend) needs zero new frontend code here. function CycleStepRow({ step, value, onChange, }: { step: CycleStepDef value: Record onChange: (v: Record) => void }) { const [open, setOpen] = useState(false) const hasToggle = 'enabled' in step.params const enabled = hasToggle ? (value.enabled ?? step.params.enabled.default) : true const otherParams = Object.entries(step.params).filter(([k]) => k !== 'enabled') const update = (key: string, val: any) => onChange({ ...value, [key]: val }) return (
{hasToggle ? ( ) : ( )}
{step.label}
{step.description}
{enabled && otherParams.length > 0 && ( )}
{open && enabled && otherParams.length > 0 && (
{otherParams.map(([key, p]) => (
update(key, p.type === 'float' ? parseFloat(e.target.value) : parseInt(e.target.value))} className="w-28 bg-dark-900 border border-slate-700/40 rounded px-2 py-1 text-xs text-white" />
))}
)}
) } export default function Config() { const { data: sources, isLoading } = useSources() const { data: config } = useConfig() const { data: aiStatus } = useAiStatus() const { data: analysisCfg } = useAnalysisConfig() const { mutate: updateSources, isPending: savingSources } = useUpdateSources() const { mutate: updateApiKeys, isPending: savingKeys } = useUpdateApiKeys() const { mutate: saveAnalysis, isPending: savingAnalysis } = useSaveAnalysisConfig() const { data: cycleStatus, refetch: refetchCycle } = useCycleStatus() const { mutate: updateCycleConfig, isPending: savingCycle } = useUpdateCycleConfig() const { mutate: triggerCycle, isPending: triggeringCycle } = useTriggerCycle() const { data: exitDefaultsData } = useExitDefaults() const { mutate: saveExitDefaults, isPending: savingExitDefaults } = useSaveExitDefaults() const { data: optionsGateData } = useOptionsGate() const { mutate: saveOptionsGate, isPending: savingOptionsGate } = useSaveOptionsGate() const [localSources, setLocalSources] = useState | null>(null) const [openaiKey, setOpenaiKey] = useState('') const [newsapiKey, setNewsapiKey] = useState('') const [eiaKey, setEiaKey] = useState('') const [fredKey, setFredKey] = useState('') const [fmpKey, setFmpKey] = useState('') const [calendarRefreshH, setCalendarRefreshH] = useState(6) const [showKeys, setShowKeys] = useState(false) const [savedMsg, setSavedMsg] = useState('') const [configTab, setConfigTab] = useState<'ia' | 'cycle' | 'sources' | 'options' | 'profiles' | 'watchlist' | 'data'>('ia') // IV gate local state const [ivGateEnabled, setIvGateEnabled] = useState(true) const [ivGateHigh, setIvGateHigh] = useState(60) const [ivGateExtreme, setIvGateExtreme] = useState(80) const [ivGateSkew, setIvGateSkew] = useState(8) // Tech indicators local state const { data: techIndicatorsData } = useTechIndicatorsConfig() const { mutate: saveTechIndicators, isPending: savingTechIndicators } = useSaveTechIndicatorsConfig() const [techEnabled, setTechEnabled] = useState(true) const [techAutoCalibrate, setTechAutoCalibrate] = useState(true) const [techList, setTechList] = useState(['rsi', 'ma', 'bollinger', 'atr']) // Analysis config local state const [analysisTopN, setAnalysisTopN] = useState(10) const [analysisCategoryDefault, setAnalysisCategoryDefault] = useState('all') const [analysisTemplate, setAnalysisTemplate] = useState('') // Exit defaults local state const [exitTarget, setExitTarget] = useState(30) const [exitStop, setExitStop] = useState(-50) const [exitSignalMode, setExitSignalMode] = useState('badge_only') const [exitSignalThreshold, setExitSignalThreshold] = useState(25) useEffect(() => { const ed = exitDefaultsData as any if (ed) { setExitTarget(ed.target_pct ?? 30) setExitStop(ed.stop_loss_pct ?? -50) setExitSignalMode(ed.signal_reversal_mode ?? 'badge_only') setExitSignalThreshold(ed.signal_reversal_threshold ?? 25) } }, [exitDefaultsData]) // VaR + PnL scheduler state const qc = useQueryClient() const { data: schedStatus, refetch: refetchSched } = useQuery({ queryKey: ['sched-status'], queryFn: () => fetch(`${API}/api/var/scheduler/status`).then(r => r.json()), staleTime: 30_000, retry: 1, }) const [varSchedEnabled, setVarSchedEnabled] = useState(false) const [varSchedHours, setVarSchedHours] = useState(6) const [pnlSchedEnabled, setPnlSchedEnabled] = useState(false) const [pnlSchedHours, setPnlSchedHours] = useState(1) useEffect(() => { if (schedStatus) { setVarSchedEnabled(schedStatus.var?.enabled ?? false) setVarSchedHours(schedStatus.var?.hours ?? 6) setPnlSchedEnabled(schedStatus.pnl?.enabled ?? false) setPnlSchedHours(schedStatus.pnl?.hours ?? 1) } }, [schedStatus]) const { mutate: saveSchedConfig, isPending: savingSched } = useMutation({ mutationFn: (cfg: any) => fetch(`${API}/api/var/scheduler/config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg), }).then(r => r.json()), onSuccess: () => { refetchSched(); setSavedMsg('Schedulers saved'); setTimeout(() => setSavedMsg(''), 2000) }, }) const { mutate: triggerVarNow, isPending: triggeringVar } = useMutation({ mutationFn: () => fetch(`${API}/api/var/run-now`, { method: 'POST' }).then(r => r.json()), onSuccess: () => { setSavedMsg('VaR snapshot saved'); setTimeout(() => setSavedMsg(''), 2000) }, }) const { mutate: triggerPnlNow, isPending: triggeringPnl } = useMutation({ mutationFn: () => fetch(`${API}/api/var/pnl/run-now`, { method: 'POST' }).then(r => r.json()), onSuccess: () => { setSavedMsg('PnL snapshot saved'); setTimeout(() => setSavedMsg(''), 2000) }, }) // Auto-cycle local state const cs = cycleStatus as any const [cycleEnabled, setCycleEnabled] = useState(false) const [cycleHours, setCycleHours] = useState(3) const [cycleSimilarity, setCycleSimilarity] = useState(0.30) const [minEv, setMinEv] = useState(0.0) const [minScore, setMinScore] = useState(0) const [tradeBudget, setTradeBudget] = useState(5000) const [horizonMin, setHorizonMin] = useState(30) const [horizonMax, setHorizonMax] = useState(180) const [retentionDays, setRetentionDays] = useState(90) const [maturityThreshold, setMaturityThreshold] = useState(35) const [weekendEnabled, setWeekendEnabled] = useState(true) const [weekendTimes, setWeekendTimes] = useState(['08:00', '22:00']) const [stepConfig, setStepConfig] = useState>>({}) const { data: stepCatalog } = useCycleStepCatalog() useEffect(() => { if (cs) { setCycleEnabled(cs.enabled ?? false) setCycleHours(cs.interval_hours ?? 3) setCycleSimilarity(cs.similarity_threshold ?? 0.30) setMinEv(cs.min_ev_threshold ?? 0.0) setMinScore(cs.min_score_threshold ?? 0) setTradeBudget(cs.trade_budget_eur ?? 5000) setHorizonMin(cs.preferred_horizon_min ?? 30) setHorizonMax(cs.preferred_horizon_max ?? 180) setRetentionDays(cs.journal_retention_days ?? 90) setMaturityThreshold(cs.maturity_threshold_pct ?? 35) setWeekendEnabled(cs.weekend_cycle_enabled ?? true) const times = (cs.weekend_cycle_times || '08:00,22:00').split(',').map((t: string) => t.trim()).filter(Boolean) setWeekendTimes(times) if (cs.cycle_step_config) setStepConfig(cs.cycle_step_config) } }, [cs]) useEffect(() => { if (analysisCfg) { setAnalysisTopN(analysisCfg.top_n ?? 10) setAnalysisCategoryDefault(analysisCfg.category_filter ?? 'all') setAnalysisTemplate(analysisCfg.template ?? '') } }, [analysisCfg]) useEffect(() => { if (optionsGateData) { setIvGateEnabled(optionsGateData.iv_gate_enabled ?? true) setIvGateHigh(optionsGateData.iv_gate_ivr_high ?? 60) setIvGateExtreme(optionsGateData.iv_gate_ivr_extreme ?? 80) setIvGateSkew(optionsGateData.iv_gate_skew_threshold ?? 8) } }, [optionsGateData]) useEffect(() => { if (config?.calendar_refresh_h) { const h = parseFloat(config.calendar_refresh_h) if (!isNaN(h)) setCalendarRefreshH(h) } }, [config]) useEffect(() => { if (techIndicatorsData) { setTechEnabled(techIndicatorsData.tech_indicators_enabled ?? true) setTechAutoCalibrate(techIndicatorsData.tech_indicators_auto_calibrate ?? true) const list = (techIndicatorsData.tech_indicators_list || 'rsi,ma,bollinger,atr').split(',').map((s: string) => s.trim()).filter(Boolean) setTechList(list) } }, [techIndicatorsData]) const displaySources = localSources ?? sources ?? {} const toggleSource = (key: string) => { setLocalSources(prev => { const base = prev ?? sources ?? {} return { ...base, [key]: { ...base[key], enabled: !base[key]?.enabled } } }) } const saveSources = () => { updateSources(displaySources, { onSuccess: () => { setSavedMsg('Sources saved'); setTimeout(() => setSavedMsg(''), 2000) } }) } const saveKeys = () => { const keys: Record = {} if (openaiKey) keys.openai_api_key = openaiKey if (newsapiKey) keys.newsapi_key = newsapiKey if (eiaKey) keys.eia_api_key = eiaKey if (fredKey) keys.fred_api_key = fredKey if (fmpKey) keys.fmp_api_key = fmpKey updateApiKeys(keys, { onSuccess: () => { setSavedMsg('API keys saved') setTimeout(() => setSavedMsg(''), 2000) setOpenaiKey(''); setNewsapiKey(''); setEiaKey(''); setFredKey(''); setFmpKey('') } }) } const CONFIG_TABS = [ { key: 'ia', label: 'AI & Analysis', icon: Brain }, { key: 'cycle', label: 'Auto-Cycle & Logging', icon: Zap }, { key: 'sources', label: 'Sources', icon: Globe }, { key: 'options', label: 'Options — Settings', icon: TrendingUp }, { key: 'profiles', label: 'Risk Profiles', icon: SlidersHorizontal }, { key: 'watchlist', label: 'Watchlist', icon: Radar }, { key: 'data', label: 'Data Management', icon: DatabaseBackup }, ] as const return (

Configuration

API keys, sources, AI & cycle settings

{savedMsg && (
{savedMsg}
)}
{/* Tab navigation */}
{CONFIG_TABS.map(({ key, label, icon: Icon }) => ( ))}
{/* ── IA & Analyse ── */} {configTab === 'ia' && (
{/* AI Status */}
AI Status
{aiStatus?.enabled ? ( <>
OpenAI Connected
GPT-4o + GPT-4o-mini
) : ( <>
OpenAI Not configured
Enter key below
)}
• Speech analysis (Trump, Powell...)
• AI news classification
• Pattern evaluation
• Top 10 contextualized ideas
{/* OpenAI API Key */}
OpenAI Key
{aiStatus?.enabled ? '✓ Active' : '○ Inactive'}
setOpenaiKey(e.target.value)} placeholder="sk-proj-..." className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 font-mono" />
{/* Right: Analysis params */}

AI Analysis Settings

{[5, 10, 15, 20].map(n => ( ))}