Files
OpenFin/frontend/src/pages/Config.tsx
2026-07-21 12:41:26 +02:00

2197 lines
110 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, { description: string; link?: string; cost: string }> = {
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 (
<div className="flex items-center gap-2 text-emerald-400/80">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
<span>Next cycle in <span className="font-mono font-semibold text-emerald-300">{remaining}</span></span>
<span className="text-slate-600">({absTime} local time)</span>
</div>
)
}
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 (
<div className="flex items-center gap-3 py-2 px-3 rounded hover:bg-dark-700/40 group">
<div className="w-2.5 h-2.5 rounded-full shrink-0" style={{ background: profile.color ?? '#3b82f6' }} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className={clsx('text-sm font-semibold', enabled ? 'text-slate-200' : 'text-slate-600')}>{profile.name}</span>
{!enabled && <span className="text-[10px] text-slate-700 italic">disabled</span>}
<span className="text-xs text-slate-500">Score <span className="text-slate-300 font-mono">{profile.min_score}</span></span>
<span className="text-xs text-slate-500">Gain <span className="text-slate-300 font-mono">{profile.min_gain_pct}%</span></span>
<span className={clsx('text-[11px] font-mono', fevc)}>{fev}</span>
<span className="text-[11px] text-slate-600">Trade Score {(profile.trade_score_at_frontier ?? 0).toFixed(0)}</span>
</div>
</div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<button onClick={() => setEditing(true)}
className="text-slate-600 hover:text-slate-300 p-1 rounded">
<Pencil className="w-3 h-3" />
</button>
<button onClick={() => onDelete(profile.id)}
className="text-slate-600 hover:text-red-400 p-1 rounded">
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
)
}
return (
<div className="bg-dark-700/60 rounded-lg p-3 border border-slate-600/40 space-y-3">
<div className="grid grid-cols-3 gap-3">
<div>
<label className="text-[10px] text-slate-500 mb-1 block">Profile name</label>
<input value={name} onChange={e => setName(e.target.value)}
className="w-full bg-dark-800 border border-slate-700 rounded px-2 py-1 text-sm text-white" />
</div>
<div>
<label className="text-[10px] text-slate-500 mb-1 block">Score min = <span className="text-blue-400 font-mono">{score}</span>/100</label>
<input type="range" min="0" max="90" step="5" value={score}
onChange={e => setScore(parseInt(e.target.value))}
className="w-full accent-blue-500" />
</div>
<div>
<label className="text-[10px] text-slate-500 mb-1 block">Gain min = <span className="text-blue-400 font-mono">{gain}%</span></label>
<input type="range" min="10" max="2000" step="10" value={gain}
onChange={e => setGain(parseFloat(e.target.value))}
className="w-full accent-blue-500" />
</div>
</div>
{/* Live EV preview */}
<div className="bg-dark-800 rounded px-3 py-2 text-xs flex items-center gap-4 flex-wrap">
<span className="text-slate-500">At the frontier (score={score}, gain={gain}%)</span>
<span className={clsx('font-mono font-bold', evCls)}>{evText}</span>
<span className="text-slate-600">Trade Score = <span className="text-slate-400">{trade_score.toFixed(1)}</span>/100</span>
<span className="text-slate-600">
Score min EV=0 : <span className="text-slate-400 font-mono">{Math.ceil(100 / (G + 1))}</span>/100
</span>
</div>
<div className="flex items-center gap-3">
<div className="flex gap-1.5">
{PROFILE_COLORS.map(c => (
<button key={c.value} onClick={() => setColor(c.value)}
title={c.label}
className={clsx('w-5 h-5 rounded-full border-2 transition-all',
color === c.value ? 'border-white scale-110' : 'border-transparent opacity-60 hover:opacity-100')}
style={{ background: c.value }} />
))}
</div>
<button onClick={() => setEnabled(!enabled)}
className={clsx('text-xs px-2 py-0.5 rounded border transition-all',
enabled ? 'border-emerald-600/40 text-emerald-400 bg-emerald-900/20' : 'border-slate-700 text-slate-600')}>
{enabled ? '✓ Enabled' : '○ Disabled'}
</button>
<div className="ml-auto flex gap-2">
<button onClick={() => setEditing(false)}
className="text-xs text-slate-500 hover:text-slate-300 px-2 py-1">Cancel</button>
<button onClick={save}
className="text-xs bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded font-semibold">
Save
</button>
</div>
</div>
</div>
)
}
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 (
<div className="card bg-dark-700/20 mb-4">
<div className="flex items-center justify-between mb-3">
<div>
<div className="text-sm font-semibold text-slate-300">Risk Profiles</div>
<div className="text-[10px] text-slate-600 mt-0.5">
A trade is logged in the journal if it passes <span className="text-slate-500">at least one</span> enabled profile
· Formula: Net EV = (score/100 × gain/100) (1 score/100)
</div>
</div>
<button onClick={() => setShowNew(true)}
className="flex items-center gap-1 text-xs border border-blue-500/40 text-blue-400 hover:bg-blue-900/20 px-2.5 py-1 rounded">
<Plus className="w-3 h-3" /> New profile
</button>
</div>
{isLoading ? (
<div className="h-16 animate-pulse bg-dark-700 rounded" />
) : profiles.length === 0 ? (
<div className="text-center py-6 text-slate-600 text-sm">No profiles all trades will be ignored</div>
) : (
<div className="space-y-1">
{profiles.map((p: any) => (
<ProfileRow key={p.id} profile={p}
onSave={data => upsert(data)}
onDelete={id => del(id)} />
))}
</div>
)}
{/* New profile form */}
{showNew && (
<div className="bg-dark-700/60 rounded-lg p-3 border border-blue-700/30 space-y-3 mt-3">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-blue-400">New profile</span>
<button onClick={() => setShowNew(false)} className="text-slate-600 hover:text-slate-400"><X className="w-3.5 h-3.5" /></button>
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="text-[10px] text-slate-500 mb-1 block">Name</label>
<input value={newName} onChange={e => 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" />
</div>
<div>
<label className="text-[10px] text-slate-500 mb-1 block">Score min = <span className="text-blue-400 font-mono">{newScore}</span></label>
<input type="range" min="0" max="90" step="5" value={newScore}
onChange={e => setNewScore(parseInt(e.target.value))} className="w-full accent-blue-500" />
</div>
<div>
<label className="text-[10px] text-slate-500 mb-1 block">Gain min = <span className="text-blue-400 font-mono">{newGain}%</span></label>
<input type="range" min="10" max="2000" step="10" value={newGain}
onChange={e => setNewGain(parseFloat(e.target.value))} className="w-full accent-blue-500" />
</div>
</div>
{/* Live math */}
<div className="bg-dark-800 rounded px-3 py-2 text-xs flex items-center gap-4 flex-wrap">
<span className={clsx('font-mono font-bold', evNetLabel(nEvNet).cls)}>{evNetLabel(nEvNet).text}</span>
<span className="text-slate-600">Trade Score = <span className="text-slate-400">{nTradeScore.toFixed(1)}</span>/100</span>
<span className="text-slate-700">Min score for EV=0 with gain {newGain}% : <span className="text-slate-500 font-mono">{nMinScore}</span>/100</span>
</div>
<div className="flex items-center gap-3">
<div className="flex gap-1.5">
{PROFILE_COLORS.map(c => (
<button key={c.value} onClick={() => setNewColor(c.value)} title={c.label}
className={clsx('w-5 h-5 rounded-full border-2 transition-all',
newColor === c.value ? 'border-white scale-110' : 'border-transparent opacity-60 hover:opacity-100')}
style={{ background: c.value }} />
))}
</div>
<div className="ml-auto flex gap-2">
<button onClick={() => setShowNew(false)} className="text-xs text-slate-500 hover:text-slate-300 px-2 py-1">Cancel</button>
<button onClick={saveNew} disabled={!newName.trim()}
className="text-xs bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-3 py-1 rounded font-semibold">
Create
</button>
</div>
</div>
</div>
)}
{/* Frontier visualization */}
{profiles.length > 0 && (
<div className="mt-3 pt-3 border-t border-slate-700/30">
<div className="text-[10px] text-slate-600 mb-2">Acceptance frontier each point represents the minimum required per profile</div>
<div className="flex flex-wrap gap-2">
{profiles.filter((p: any) => p.enabled).map((p: any) => {
const { text: ev, cls } = evNetLabel(p.ev_net_at_frontier ?? 0)
return (
<div key={p.id} className="flex items-center gap-2 bg-dark-700 rounded px-2.5 py-1.5 text-xs">
<div className="w-2 h-2 rounded-full" style={{ background: p.color }} />
<span className="text-slate-400 font-semibold">{p.name}</span>
<span className="font-mono text-slate-500">{p.min_score}pts</span>
<span className="text-slate-600">×</span>
<span className="font-mono text-slate-500">{p.min_gain_pct}%</span>
<span className={clsx('font-mono text-[10px]', cls)}> {ev}</span>
</div>
)
})}
</div>
</div>
)}
</div>
)
}
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 (
<div className="card bg-dark-700/20 mb-4">
<div className="flex items-center justify-between mb-3">
<div>
<div className="text-sm font-semibold text-slate-300 flex items-center gap-1.5">
<Radar className="w-4 h-4 text-blue-400" /> Instruments Watchlist
</div>
<div className="text-[10px] text-slate-600 mt-0.5">
Instruments surveillés pour le radar du Dashboard et les highlights Options Lab liste indépendante des autres watchlists (Markets, Options Lab, InstrumentModels).
</div>
</div>
</div>
<div className="flex items-center gap-2 mb-3">
<input
value={input}
onChange={e => 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"
/>
<button onClick={handleAdd} disabled={adding || !input.trim()}
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 transition-colors">
<Plus className="w-3.5 h-3.5" /> Add
</button>
</div>
{error && <div className="text-[10px] text-red-400 mb-3">{error}</div>}
{isLoading ? (
<div className="text-xs text-slate-600">Loading</div>
) : list.length === 0 ? (
<div className="text-xs text-slate-600">No instrument watched yet.</div>
) : (
<div className="space-y-1.5">
{list.map(item => (
<div key={item.ticker} className="flex items-center justify-between bg-dark-800/60 rounded px-3 py-1.5">
<div className="flex items-center gap-2 min-w-0">
<span className="text-xs font-mono font-bold text-white">{item.ticker}</span>
<span className="text-[10px] text-slate-500 truncate">{item.name}</span>
<span className="text-[9px] text-slate-700 bg-dark-700 px-1.5 py-0.5 rounded capitalize">{item.asset_class}</span>
</div>
<button onClick={() => removeTicker(item.ticker)} className="text-slate-600 hover:text-red-400 transition-colors">
<X className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
)}
</div>
)
}
// ── 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 (
<div className="card">
<div className="flex items-center justify-between mb-3">
<div className="section-title mb-0 flex items-center gap-1">
<Link2 className="w-3 h-3" /> Saxo OpenAPI ({(status?.environment ?? 'sim').toUpperCase()})
</div>
<span className={clsx('badge', status?.connected ? 'badge-green' : 'badge-red')}>
{status?.connected ? 'Connecté' : 'Déconnecté'}
</span>
</div>
{!status?.configured && (
<div className="text-xs text-amber-400 bg-amber-900/10 border border-amber-700/30 rounded px-3 py-2 mb-3">
SAXO_APP_KEY / SAXO_APP_SECRET manquants dans backend/.env ajoutez-les puis redémarrez le backend.
</div>
)}
{status?.connected && (
<div className="text-xs text-slate-500 mb-3 space-y-0.5">
<div>Token valide encore <span className="text-slate-300">{timeUntil(status.expires_at)}</span></div>
<div>Rafraîchi automatiquement en tâche de fond pas besoin de te reconnecter.</div>
</div>
)}
<div className="flex items-center gap-2 mb-4">
{status?.connected ? (
<button
onClick={() => disconnect.mutate()}
disabled={disconnect.isPending}
className="flex items-center gap-1.5 bg-dark-700 hover:bg-dark-600 border border-slate-700/50 text-slate-300 px-3 py-1.5 rounded text-xs font-semibold disabled:opacity-40"
>
<Unlink className="w-3.5 h-3.5" /> Déconnecter
</button>
) : (
<a
href="/oauth/saxo/login"
className={clsx(
'flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-semibold text-white',
status?.configured ? 'bg-blue-600 hover:bg-blue-500' : 'bg-dark-700 opacity-40 pointer-events-none',
)}
>
<Link2 className="w-3.5 h-3.5" /> Se connecter à Saxo
</a>
)}
</div>
<div className="flex items-center justify-between mb-2">
<div className="text-xs text-slate-500 flex items-center gap-2">
<span>Watchlist snapshotée toutes les</span>
<input
type="number" min={1} max={120}
value={settings?.snapshot_minutes ?? ''}
onChange={e => 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"
/>
<span>min (historique Date/Spot/Expiry/Strike/Bid/Ask/Mid/VolatilityPct/Greeks) :</span>
</div>
<div className="flex items-center gap-2 shrink-0">
<button
onClick={handleSnapshotAllNow}
disabled={!status?.connected || snapshotAllNow.isPending}
title="Snapshot immédiat de toute la watchlist, sans attendre le prochain cycle"
className="flex items-center gap-1 text-xs text-slate-400 hover:text-slate-200 border border-slate-700/50 px-2 py-1 rounded disabled:opacity-40"
>
<RefreshCw className={clsx('w-3.5 h-3.5', snapshotAllNow.isPending && 'animate-spin')} /> Rafraîchir maintenant
</button>
<button
onClick={() => runValidation()}
disabled={!status?.connected || validating}
title="Vérifie que chaque symbole résout bien vers un vrai instrument Saxo"
className="flex items-center gap-1 text-xs text-slate-400 hover:text-slate-200 border border-slate-700/50 px-2 py-1 rounded disabled:opacity-40"
>
<ShieldCheck className={clsx('w-3.5 h-3.5', validating && 'animate-pulse')} /> Vérifier l'orthographe
</button>
<Link to="/saxo-history" className="flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300">
<ExternalLink className="w-3.5 h-3.5" /> Historique
</Link>
</div>
</div>
<div className="flex items-center justify-between mb-2">
<div className="text-[10px] text-slate-600">
Catalogue local (Futures/FX — tickers Saxo non-évidents, ex. Or = <code>OG:xcme</code>) :{' '}
{(catalogSummary ?? []).map(s => `${s.asset_type} (${s.count})`).join(' · ') || 'vide'}
</div>
<button
onClick={() => refreshCatalog.mutate(undefined)}
disabled={!status?.connected || refreshCatalog.isPending}
className="flex items-center gap-1 text-[10px] text-slate-400 hover:text-slate-200 border border-slate-700/50 px-2 py-1 rounded disabled:opacity-40"
>
<RefreshCw className={clsx('w-3 h-3', refreshCatalog.isPending && 'animate-spin')} /> Rafraîchir le catalogue
</button>
</div>
<div className="flex items-center gap-2 mb-2">
<input
value={input}
onChange={e => 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"
/>
<datalist id="saxo-catalog-datalist">
{(catalogMatches ?? []).map(m => (
<option key={m.uic} value={m.symbol}>{m.description} ({m.asset_type})</option>
))}
</datalist>
<button onClick={addSymbol} disabled={!input.trim() || updateWatchlist.isPending}
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
</button>
</div>
<div className="flex items-center gap-2 mb-2">
<input
value={expandKeyword}
onChange={e => 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"
/>
<button
onClick={handleExpandWatchlist}
disabled={!expandKeyword.trim() || expandWatchlist.isPending}
title="Ajoute toutes les maturités/racines du catalogue correspondant à ce mot-clé (pas besoin de connaître chaque ticker exact)"
className="flex items-center gap-1 px-3 py-1.5 rounded text-xs bg-dark-700 hover:bg-dark-600 border border-slate-700/50 text-slate-300 disabled:opacity-40"
>
<Plus className="w-3.5 h-3.5" /> Ajouter toutes les maturités
</button>
</div>
{expandMsg && <div className="text-[10px] text-slate-500 mb-2">{expandMsg}</div>}
{symbols.length > 0 && (
<div className="flex justify-end mb-1.5">
<button onClick={clearAllSymbols} disabled={updateWatchlist.isPending}
title="Retire tous les tickers de la watchlist en un seul appel"
className="flex items-center gap-1 px-2 py-1 rounded text-[10px] border border-red-700/40 text-red-400 hover:bg-red-900/20 disabled:opacity-40">
<Trash2 className="w-3 h-3" /> Tout effacer ({symbols.length})
</button>
</div>
)}
<div className="flex flex-wrap gap-1.5">
{symbols.map(sym => {
const check = validation?.find(v => v.symbol === sym.toUpperCase())
return (
<span key={sym} className={clsx('badge flex items-center gap-1.5',
check ? (check.valid ? 'badge-green' : 'badge-red') : 'badge-blue')}
title={check ? (check.valid
? `Résolu: ${check.matched_symbol ?? '?'} — ${check.description ?? ''} (${check.asset_type ?? '?'}, Uic ${check.uic})`
: check.error ?? 'Symbole non résolu par Saxo') : undefined}
>
{sym}
{check && (check.valid ? <CheckCircle className="w-3 h-3" /> : <XCircle className="w-3 h-3" />)}
<button onClick={() => handleSnapshotNow(sym)} title="Snapshot maintenant" className="hover:text-white">
<Camera className="w-3 h-3" />
</button>
<button onClick={() => removeSymbol(sym)} disabled={updateWatchlist.isPending} className="disabled:opacity-40">
<X className="w-3 h-3" />
</button>
</span>
)
})}
</div>
{snapMsg && <div className="text-[10px] text-slate-500 mt-2">{snapMsg}</div>}
<div className="mt-4 pt-3 border-t border-slate-700/30">
<div className="text-xs text-slate-500 mb-2">
Tester un prix hors options (isole si un blocage est spécifique aux options ou plus large) :
</div>
<div className="flex items-center gap-2">
<input
value={quoteSymbol}
onChange={e => 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"
/>
<select
value={quoteAssetType}
onChange={e => setQuoteAssetType(e.target.value)}
className="bg-dark-800 border border-slate-700/40 rounded px-2 py-1.5 text-xs text-white"
>
<option value="FxSpot">FxSpot</option>
<option value="Stock">Stock</option>
<option value="StockIndex">StockIndex</option>
<option value="ContractFutures">ContractFutures</option>
</select>
<button
onClick={() => testQuote.mutate({ symbol: quoteSymbol, assetType: quoteAssetType })}
disabled={!status?.connected || !quoteSymbol.trim() || testQuote.isPending}
className="flex items-center gap-1 px-3 py-1.5 rounded text-xs bg-dark-700 hover:bg-dark-600 border border-slate-700/50 text-slate-300 disabled:opacity-40"
>
<RefreshCw className={clsx('w-3.5 h-3.5', testQuote.isPending && 'animate-spin')} /> Tester
</button>
</div>
{testQuote.isSuccess && (
<div className="text-xs text-emerald-400 mt-2">
✓ {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>
)}
{testQuote.isError && (
<div className="text-xs text-red-400 mt-2">
✗ {(testQuote.error as any)?.response?.data?.detail ?? 'échec'}
</div>
)}
</div>
</div>
)
}
// ── 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<string, any>
onChange: (v: Record<string, any>) => 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 (
<div className={clsx('rounded-lg border transition-colors', enabled ? 'border-cyan-700/40 bg-cyan-900/10' : 'border-slate-700/30 bg-dark-800/60')}>
<div className="flex items-center gap-3 px-3 py-2.5">
{hasToggle ? (
<button onClick={() => update('enabled', !enabled)} className="shrink-0 text-xs px-2 py-1 rounded border font-semibold"
style={{ borderColor: enabled ? '#22d3ee88' : '#47556955', color: enabled ? '#22d3ee' : '#64748b' }}>
{enabled ? 'ON' : 'OFF'}
</button>
) : (
<span className="w-2 h-2 rounded-full bg-slate-600 shrink-0" />
)}
<div className="flex-1 min-w-0">
<div className={clsx('text-sm font-medium', enabled ? 'text-white' : 'text-slate-500')}>{step.label}</div>
<div className="text-xs text-slate-600 truncate">{step.description}</div>
</div>
{enabled && otherParams.length > 0 && (
<button onClick={() => setOpen(o => !o)} className="text-slate-500 hover:text-slate-300 text-xs shrink-0">
{open ? '▲' : '▼'}
</button>
)}
</div>
{open && enabled && otherParams.length > 0 && (
<div className="px-4 pb-3 space-y-2 border-t border-slate-700/20 pt-2">
{otherParams.map(([key, p]) => (
<div key={key} className="flex items-center gap-3">
<label className="text-xs text-slate-400 w-40 shrink-0">{p.label ?? key}</label>
<input
type="number"
min={p.min}
max={p.max}
step={p.type === 'float' ? 0.01 : 1}
value={value[key] ?? p.default}
onChange={e => 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"
/>
</div>
))}
</div>
)}
</div>
)
}
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<Record<string, any> | 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<string[]>(['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<string[]>(['08:00', '22:00'])
const [stepConfig, setStepConfig] = useState<Record<string, Record<string, any>>>({})
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<string, string> = {}
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 (
<div className="p-6 space-y-5">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Settings className="w-5 h-5 text-blue-400" /> Configuration
</h1>
<p className="text-xs text-slate-500 mt-0.5">API keys, sources, AI &amp; cycle settings</p>
</div>
{savedMsg && (
<div className="flex items-center gap-2 text-emerald-400 text-sm bg-emerald-900/10 border border-emerald-700/30 rounded px-3 py-1.5">
<CheckCircle className="w-4 h-4" /> {savedMsg}
</div>
)}
</div>
{/* Tab navigation */}
<div className="flex gap-1 bg-dark-700 p-1 rounded w-fit">
{CONFIG_TABS.map(({ key, label, icon: Icon }) => (
<button key={key} onClick={() => setConfigTab(key as any)}
className={clsx('flex items-center gap-1.5 px-3 py-1.5 rounded text-sm transition-colors', {
'bg-blue-600 text-white': configTab === key,
'text-slate-400 hover:text-slate-200': configTab !== key,
})}>
<Icon className="w-3.5 h-3.5" />
{label}
</button>
))}
</div>
{/* ── IA & Analyse ── */}
{configTab === 'ia' && (
<div className="grid grid-cols-2 gap-6">
<div className="space-y-4">
{/* AI Status */}
<div className={clsx('card', aiStatus?.enabled ? 'border-emerald-700/40' : 'border-slate-700/40')}>
<div className="section-title flex items-center gap-1"><Key className="w-3 h-3" /> AI Status</div>
<div className="flex items-center gap-3 mb-3">
{aiStatus?.enabled ? (
<><CheckCircle className="w-5 h-5 text-emerald-400" />
<div><div className="text-sm text-emerald-400 font-semibold">OpenAI Connected</div>
<div className="text-xs text-slate-500">GPT-4o + GPT-4o-mini</div></div></>
) : (
<><XCircle className="w-5 h-5 text-red-400" />
<div><div className="text-sm text-red-400 font-semibold">OpenAI Not configured</div>
<div className="text-xs text-slate-500">Enter key below</div></div></>
)}
</div>
<div className="text-xs text-slate-600 space-y-1">
<div> Speech analysis (Trump, Powell...)</div>
<div> AI news classification</div>
<div> Pattern evaluation</div>
<div> Top 10 contextualized ideas</div>
</div>
</div>
{/* OpenAI API Key */}
<div className="card">
<div className="flex items-center justify-between mb-3">
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> OpenAI Key</div>
<button onClick={() => setShowKeys(!showKeys)} className="text-slate-500 hover:text-slate-300">
{showKeys ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
</div>
<div className="space-y-3">
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-slate-500">OpenAI API Key</label>
<span className={clsx('text-xs', aiStatus?.enabled ? 'text-emerald-400' : 'text-slate-600')}>
{aiStatus?.enabled ? '✓ Active' : '○ Inactive'}
</span>
</div>
<input
type={showKeys ? 'text' : 'password'}
value={openaiKey}
onChange={e => 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"
/>
</div>
<button
onClick={saveKeys}
disabled={savingKeys || !openaiKey}
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white rounded py-1.5 text-xs font-semibold flex items-center justify-center gap-1.5"
>
<Save className="w-3.5 h-3.5" />
{savingKeys ? 'Saving...' : 'Save key'}
</button>
</div>
</div>
</div>
{/* Right: Analysis params */}
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-4">
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> AI Analysis Settings
</h2>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-1 block">Default top N results</label>
<div className="flex gap-1">
{[5, 10, 15, 20].map(n => (
<button key={n} onClick={() => setAnalysisTopN(n)}
className={clsx('flex-1 py-1.5 rounded text-sm transition-colors', {
'bg-blue-600 text-white': analysisTopN === n,
'bg-dark-700 text-slate-400 hover:text-slate-200': analysisTopN !== n,
})}>
Top {n}
</button>
))}
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Default category</label>
<select value={analysisCategoryDefault} onChange={e => setAnalysisCategoryDefault(e.target.value)}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500">
<option value="all">All categories</option>
<option value="energy">Energy</option>
<option value="metals">Metals</option>
<option value="agriculture">Agriculture</option>
<option value="indices">Indices</option>
<option value="equities">Equities</option>
<option value="forex">Forex</option>
</select>
</div>
</div>
<div className="mb-4">
<div className="flex items-center justify-between mb-1">
<label className="text-xs text-slate-500">Analysis template (GPT-4o prompt)</label>
<button onClick={() => setAnalysisTemplate('')} className="text-xs text-slate-600 hover:text-slate-400">
Default
</button>
</div>
<textarea
value={analysisTemplate}
onChange={e => setAnalysisTemplate(e.target.value)}
rows={8}
placeholder="Leave empty to use default template..."
className="w-full bg-dark-700 border border-slate-700 rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:border-blue-500 resize-y"
/>
<div className="text-xs text-slate-600 mt-1">
Market context (prices, IV, 1d change) and filtered news are automatically injected.
</div>
</div>
<button
onClick={() => saveAnalysis(
{ top_n: analysisTopN, category_filter: analysisCategoryDefault, template: analysisTemplate || undefined },
{ onSuccess: () => { setSavedMsg('Analysis settings saved'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingAnalysis}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingAnalysis ? 'Saving...' : 'Save'}
</button>
</div>
</div>
)}
{/* ── Auto-Cycle & Logging ── */}
{configTab === 'cycle' && (
<div className="space-y-5">
{/* Score min threshold — prominent */}
<div className="card border-blue-700/40 bg-blue-900/5">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-1">
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> Minimum score for the cycle
</h2>
<p className="text-xs text-slate-500 mb-4">
Cycle pre-filter patterns with a score below this are not submitted for analysis.
Risk profiles then determine which trades get logged.
</p>
<div className="grid grid-cols-2 gap-6">
<div>
<label className="text-xs text-slate-500 mb-2 block">
Score minimum pre-cycle : <span className="text-blue-300 font-mono font-bold text-lg">{minScore}</span>
<span className="text-slate-600 ml-1">/100</span>
</label>
<input type="range" min="0" max="80" step="5"
value={minScore}
onChange={e => setMinScore(parseInt(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>0 (all)</span><span>80 (very selective)</span>
</div>
<div className="text-xs text-slate-500 mt-2">
{minScore === 0
? 'All patterns processed by the cycle'
: `Only patterns with score ≥ ${minScore} are submitted to the cycle`}
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
EV minimum : <span className="text-blue-300 font-mono font-bold">{minEv >= 0 ? '+' : ''}{(minEv * 100).toFixed(0)}%</span>
<span className="text-slate-600 ml-1">net attendu</span>
</label>
<input type="range" min="-0.5" max="0.5" step="0.05"
value={minEv}
onChange={e => setMinEv(parseFloat(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>-50%</span><span>+50%</span>
</div>
<div className="text-xs text-slate-500 mt-2">
{minEv === 0
? 'Any net EV accepted'
: minEv > 0
? `Net EV ≥ +${(minEv * 100).toFixed(0)}% required`
: `Filter disabled (EV < ${(minEv * 100).toFixed(0)}%)`}
</div>
</div>
</div>
</div>
{/* Trade Parameters */}
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-1">
<DollarSign className="w-4 h-4 text-emerald-400" /> Trade Parameters
</h2>
<p className="text-xs text-slate-500 mb-4">
Passed into every AI cycle controls capital sizing and filters out patterns outside your investment horizon.
</p>
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
<div>
<label className="text-xs text-slate-500 mb-2 block">
Simulation budget: <span className="text-emerald-400 font-mono font-bold">{tradeBudget.toLocaleString()}</span>
</label>
<input type="range" min="500" max="50000" step="500"
value={tradeBudget}
onChange={e => setTradeBudget(parseInt(e.target.value))}
className="w-full accent-emerald-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>500</span><span>50,000</span>
</div>
<div className="text-xs text-slate-500 mt-1">
Max per position: <span className="text-slate-300 font-mono">{Math.round(tradeBudget * 0.25).toLocaleString()}</span> (25%)
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Min horizon: <span className="text-blue-400 font-mono font-bold">{horizonMin}d</span>
</label>
<input type="range" min="1" max="90" step="1"
value={horizonMin}
onChange={e => setHorizonMin(Math.min(parseInt(e.target.value), horizonMax - 5))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>1d</span><span>90d</span>
</div>
<div className="text-xs text-slate-500 mt-1">Patterns shorter than {horizonMin}d penalized in R/R</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Max horizon: <span className="text-blue-400 font-mono font-bold">{horizonMax}d</span>
</label>
<input type="range" min="30" max="365" step="5"
value={horizonMax}
onChange={e => setHorizonMax(Math.max(parseInt(e.target.value), horizonMin + 5))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>30d</span><span>365d</span>
</div>
<div className="text-xs text-slate-500 mt-1">Patterns longer than {horizonMax}d penalized in R/R</div>
</div>
</div>
<div className="mt-4 p-3 rounded bg-emerald-900/10 border border-emerald-700/20 text-xs text-emerald-300">
Active mandate: budget <span className="font-mono">{tradeBudget.toLocaleString()}</span> · horizon{' '}
<span className="font-mono">{horizonMin}{horizonMax}d</span> · AI will size + filter suggestions accordingly
</div>
</div>
{/* Auto-Cycle settings */}
<div className="card">
<div className="flex items-center justify-between mb-4">
<h2 className="text-base font-bold text-white flex items-center gap-2">
<Zap className="w-4 h-4 text-blue-400" /> Auto-Cycle Intelligence
</h2>
{cs && (
<div className="flex items-center gap-2 text-xs">
{cs.running ? (
<span className="flex items-center gap-1 text-blue-400 bg-blue-900/30 border border-blue-700/30 px-2 py-0.5 rounded animate-pulse">
<RefreshCw className="w-3 h-3 animate-spin" /> Running...
</span>
) : cs.scheduler_alive ? (
<span className="text-emerald-400 bg-emerald-900/30 border border-emerald-700/30 px-2 py-0.5 rounded">
Scheduler active
</span>
) : (
<span className="text-slate-600 bg-dark-700 border border-slate-700/30 px-2 py-0.5 rounded">
Scheduler inactive
</span>
)}
</div>
)}
</div>
<p className="text-xs text-slate-500 mb-4">
Every N hours: suggests new patterns, deduplicates, scores everything, logs prices, and generates an AI comment.
</p>
<div className="grid grid-cols-3 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">Enable auto-cycle</label>
<button
onClick={() => setCycleEnabled(!cycleEnabled)}
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
'bg-blue-600 border-blue-500 text-white': cycleEnabled,
'bg-dark-700 border-slate-700 text-slate-400 hover:border-slate-500': !cycleEnabled,
})}>
{cycleEnabled ? '✓ Enabled' : '○ Disabled'}
</button>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">Interval (hours)</label>
<div className="flex gap-1">
{[1, 3, 6, 12].map(h => (
<button key={h} onClick={() => setCycleHours(h)}
className={clsx('flex-1 py-2 rounded text-sm transition-colors', {
'bg-blue-600 text-white': cycleHours === h,
'bg-dark-700 text-slate-400 hover:text-slate-200': cycleHours !== h,
})}>
{h}h
</button>
))}
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Max similarity ({Math.round(cycleSimilarity * 100)}%)
</label>
<input type="range" min="0.1" max="0.8" step="0.05"
value={cycleSimilarity}
onChange={e => setCycleSimilarity(parseFloat(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>10% (strict)</span><span>80% (permissif)</span>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">
Journal retention ({retentionDays}d)
</label>
<div className="flex gap-1">
{[30, 60, 90, 180].map(d => (
<button key={d} onClick={() => setRetentionDays(d)}
className={clsx('flex-1 py-2 rounded text-sm transition-colors', {
'bg-blue-600 text-white': retentionDays === d,
'bg-dark-700 text-slate-400 hover:text-slate-200': retentionDays !== d,
})}>
{d}j
</button>
))}
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Maturity threshold ({maturityThreshold}%)
</label>
<div className="flex gap-1">
{[20, 30, 35, 50].map(p => (
<button key={p} onClick={() => setMaturityThreshold(p)}
className={clsx('flex-1 py-2 rounded text-sm transition-colors', {
'bg-blue-600 text-white': maturityThreshold === p,
'bg-dark-700 text-slate-400 hover:text-slate-200': maturityThreshold !== p,
})}>
{p}%
</button>
))}
</div>
</div>
</div>
{/* Weekend scheduling */}
<div className="border border-slate-700/50 rounded-lg p-3 mb-4 bg-dark-700/30">
<div className="flex items-center justify-between mb-3">
<div>
<span className="text-xs font-semibold text-slate-300">Weekend cycles</span>
<p className="text-[10px] text-slate-500 mt-0.5">Sat/Sun Globex opens Sun ~18h UTC. Times in UTC.</p>
</div>
<button
onClick={() => setWeekendEnabled(!weekendEnabled)}
className={clsx('px-3 py-1.5 rounded border text-xs font-semibold transition-all', {
'bg-amber-600/20 border-amber-500/50 text-amber-300': weekendEnabled,
'bg-dark-700 border-slate-700 text-slate-500 hover:border-slate-500': !weekendEnabled,
})}>
{weekendEnabled ? '✓ Enabled' : '○ Disabled'}
</button>
</div>
{weekendEnabled && (
<div>
<label className="text-[10px] text-slate-500 mb-1.5 block">UTC times (click to enable/disable)</label>
<div className="flex gap-1.5 flex-wrap">
{['06:00', '08:00', '12:00', '18:00', '22:00', '00:00'].map(t => {
const active = weekendTimes.includes(t)
return (
<button key={t}
onClick={() => setWeekendTimes(prev =>
active ? prev.filter(x => x !== t) : [...prev, t].sort()
)}
className={clsx('px-2.5 py-1 rounded text-xs font-mono transition-colors', {
'bg-amber-600 text-white': active,
'bg-dark-700 text-slate-500 hover:text-slate-300 border border-slate-700': !active,
})}>
{t}
</button>
)
})}
</div>
{weekendTimes.length === 0 && (
<p className="text-[10px] text-red-400 mt-1">Select at least one time</p>
)}
</div>
)}
</div>
{cs?.last_cycle && (
<div className="card bg-dark-700/50 mb-2 text-xs space-y-2">
<div className="flex items-center gap-4 flex-wrap">
<span className="text-slate-500">Last cycle:</span>
<span className="text-slate-300">{cs.last_cycle.started_at?.slice(0, 16)} UTC</span>
<span className={clsx('badge', cs.last_cycle.status === 'completed' ? 'badge-green' : 'badge-red')}>
{cs.last_cycle.status}
</span>
<span className="text-slate-500">+{cs.last_cycle.patterns_added} patterns</span>
<span className="text-slate-500">{cs.last_cycle.patterns_scored} scored</span>
<span className="text-slate-500">Geo: {cs.last_cycle.geo_score}</span>
<span className="text-slate-500 capitalize">{cs.last_cycle.dominant_regime}</span>
</div>
</div>
)}
{cs?.enabled && cs?.next_run_at && !cs?.running && (
<div className="card bg-dark-700/50 mb-4 text-xs">
<NextRunCountdown nextRunAt={cs.next_run_at} />
</div>
)}
<div className="flex items-center gap-3">
<button
onClick={() => updateCycleConfig(
{
enabled: cycleEnabled,
interval_hours: cycleHours,
similarity_threshold: cycleSimilarity,
min_ev_threshold: minEv,
min_score_threshold: minScore,
trade_budget_eur: tradeBudget,
preferred_horizon_min: horizonMin,
preferred_horizon_max: horizonMax,
journal_retention_days: retentionDays,
maturity_threshold_pct: maturityThreshold,
weekend_cycle_enabled: weekendEnabled,
weekend_cycle_times: weekendTimes.length > 0 ? weekendTimes.join(',') : '08:00,22:00',
cycle_step_config: stepConfig,
},
{ onSuccess: () => { refetchCycle(); setSavedMsg('Auto-cycle configured'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingCycle}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingCycle ? 'Saving...' : 'Apply'}
</button>
<button
onClick={() => triggerCycle(undefined, { onSuccess: () => { refetchCycle(); setSavedMsg('Cycle started!'); setTimeout(() => setSavedMsg(''), 3000) } })}
disabled={triggeringCycle || !aiStatus?.enabled}
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 disabled:opacity-40 px-4 py-2 rounded text-sm font-semibold">
<RefreshCw className={clsx('w-4 h-4', triggeringCycle && 'animate-spin')} />
Run now
</button>
</div>
</div>
{/* ── Étapes du cycle ── */}
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-1">
<SlidersHorizontal className="w-4 h-4 text-cyan-400" /> Étapes du cycle
</h2>
<p className="text-xs text-slate-500 mb-4">
Chaque étape du cycle automatique (y compris en mode planifié) lit ces réglages
activer/désactiver ou ajuster une fenêtre ici change réellement ce qui se passe au prochain cycle.
</p>
{['portfolio', 'ai', 'data'].map(group => {
const groupSteps = (stepCatalog ?? []).filter(s => s.group === group)
if (groupSteps.length === 0) return null
return (
<div key={group} className="mb-4 last:mb-0">
<div className="text-[10px] uppercase tracking-wide text-slate-600 mb-2">
{group === 'portfolio' ? 'Portefeuille' : group === 'ai' ? 'Analyse IA' : 'Données & fenêtres'}
</div>
<div className="space-y-2">
{groupSteps.map(step => (
<CycleStepRow
key={step.id}
step={step}
value={stepConfig[step.id] ?? {}}
onChange={v => setStepConfig(prev => ({ ...prev, [step.id]: v }))}
/>
))}
</div>
</div>
)
})}
<div className="mt-2 text-[10px] text-slate-600">
Ces réglages sont appliqués via le bouton "Apply" ci-dessus (Auto-Cycle Intelligence).
</div>
</div>
{/* ── VaR & PnL Schedulers ── */}
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-4">
<Gauge className="w-4 h-4 text-red-400" /> Schedulers VaR &amp; PnL
</h2>
<p className="text-xs text-slate-500 mb-4">
Automatically computes and saves VaR (Black-Scholes delta) and PnL snapshots at defined intervals.
Data is accessible from the VaR Analysis page with full history.
</p>
<div className="grid grid-cols-2 gap-6 mb-4">
{/* VaR scheduler */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Gauge className="w-3.5 h-3.5 text-red-400" />
<span className="text-sm font-semibold text-slate-300">Scheduler VaR</span>
{schedStatus?.var?.alive
? <span className="text-[10px] bg-emerald-900/30 text-emerald-400 border border-emerald-700/30 px-1.5 py-0.5 rounded ml-auto"> Active</span>
: <span className="text-[10px] bg-dark-700 text-slate-600 border border-slate-700/30 px-1.5 py-0.5 rounded ml-auto"> Inactive</span>}
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Enable</label>
<button onClick={() => setVarSchedEnabled(!varSchedEnabled)}
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
'bg-red-700 border-red-600 text-white': varSchedEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !varSchedEnabled,
})}>
{varSchedEnabled ? '✓ Enabled' : '○ Disabled'}
</button>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Interval</label>
<div className="flex gap-1">
{[1, 3, 6, 12, 24].map(h => (
<button key={h} onClick={() => setVarSchedHours(h)}
className={clsx('flex-1 py-1.5 rounded text-xs transition-colors', {
'bg-red-700 text-white': varSchedHours === h,
'bg-dark-700 text-slate-400 hover:text-slate-200': varSchedHours !== h,
})}>
{h}h
</button>
))}
</div>
</div>
</div>
{/* PnL scheduler */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<DollarSign className="w-3.5 h-3.5 text-emerald-400" />
<span className="text-sm font-semibold text-slate-300">Scheduler PnL</span>
{schedStatus?.pnl?.alive
? <span className="text-[10px] bg-emerald-900/30 text-emerald-400 border border-emerald-700/30 px-1.5 py-0.5 rounded ml-auto"> Active</span>
: <span className="text-[10px] bg-dark-700 text-slate-600 border border-slate-700/30 px-1.5 py-0.5 rounded ml-auto"> Inactive</span>}
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Enable</label>
<button onClick={() => setPnlSchedEnabled(!pnlSchedEnabled)}
className={clsx('w-full py-2 rounded border text-sm font-semibold transition-all', {
'bg-emerald-700 border-emerald-600 text-white': pnlSchedEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !pnlSchedEnabled,
})}>
{pnlSchedEnabled ? '✓ Enabled' : '○ Disabled'}
</button>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Interval</label>
<div className="flex gap-1">
{[1, 2, 4, 8, 24].map(h => (
<button key={h} onClick={() => setPnlSchedHours(h)}
className={clsx('flex-1 py-1.5 rounded text-xs transition-colors', {
'bg-emerald-700 text-white': pnlSchedHours === h,
'bg-dark-700 text-slate-400 hover:text-slate-200': pnlSchedHours !== h,
})}>
{h}h
</button>
))}
</div>
</div>
</div>
</div>
<div className="flex items-center gap-3 flex-wrap">
<button
onClick={() => saveSchedConfig({ var_enabled: varSchedEnabled, var_hours: varSchedHours, pnl_enabled: pnlSchedEnabled, pnl_hours: pnlSchedHours })}
disabled={savingSched}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingSched ? 'Saving…' : 'Apply'}
</button>
<button
onClick={() => triggerVarNow()}
disabled={triggeringVar}
className="flex items-center gap-1.5 border border-red-500/50 text-red-400 hover:bg-red-900/20 disabled:opacity-40 px-4 py-2 rounded text-sm font-semibold">
<Gauge className={clsx('w-4 h-4', triggeringVar && 'animate-spin')} />
VaR snapshot now
</button>
<button
onClick={() => triggerPnlNow()}
disabled={triggeringPnl}
className="flex items-center gap-1.5 border border-emerald-500/50 text-emerald-400 hover:bg-emerald-900/20 disabled:opacity-40 px-4 py-2 rounded text-sm font-semibold">
<DollarSign className={clsx('w-4 h-4', triggeringPnl && 'animate-spin')} />
PnL snapshot now
</button>
</div>
</div>
{/* Calendar auto-sync frequency */}
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-3">
<RefreshCw className="w-4 h-4 text-indigo-400" /> Calendar Auto-Sync
</h2>
<div className="text-xs text-slate-400 mb-3">
Interval between automatic economic calendar syncs (Forex Factory live JSON + HTML scraper; FMP API used as fallback for weeks 3+ if the FF scraper gets blocked needs an FMP key below).
</div>
<div className="flex items-center gap-3">
<label className="text-xs text-slate-500 whitespace-nowrap">Refresh every</label>
<div className="flex gap-1">
{[2, 4, 6, 12, 24].map(h => (
<button key={h} onClick={() => setCalendarRefreshH(h)}
className={clsx('px-3 py-1.5 rounded text-xs transition-colors', {
'bg-indigo-700 text-white': calendarRefreshH === h,
'bg-dark-700 text-slate-400 hover:text-slate-200': calendarRefreshH !== h,
})}>
{h}h
</button>
))}
</div>
<button
onClick={() => updateApiKeys({ calendar_refresh_h: String(calendarRefreshH) }, {
onSuccess: () => { setSavedMsg('Calendar refresh saved'); setTimeout(() => setSavedMsg(''), 2000) }
})}
disabled={savingKeys}
className="flex items-center gap-1.5 bg-indigo-700 hover:bg-indigo-600 disabled:opacity-40 text-white px-3 py-1.5 rounded text-xs font-semibold">
<Save className="w-3.5 h-3.5" />
{savingKeys ? 'Saving…' : 'Save'}
</button>
</div>
</div>
</div>
)}
{/* ── Sources ── */}
{configTab === 'sources' && (
<div className="space-y-5">
{/* Data API Keys */}
<div className="card">
<div className="flex items-center justify-between mb-3">
<div className="section-title mb-0 flex items-center gap-1"><Key className="w-3 h-3" /> Data API Keys</div>
<button onClick={() => setShowKeys(!showKeys)} className="text-slate-500 hover:text-slate-300">
{showKeys ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
</div>
<div className="grid grid-cols-4 gap-4">
{[
{ label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Get at newsapi.org', configKey: 'newsapi_key' },
{ label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Get at eia.gov', configKey: 'eia_api_key' },
{ label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Get at fred.stlouisfed.org', configKey: 'fred_api_key' },
{ label: 'FMP API Key', value: fmpKey, setter: setFmpKey, placeholder: 'Calendar fallback — financialmodelingprep.com', configKey: 'fmp_api_key' },
].map(({ label, value, setter, placeholder, configKey }) => (
<div key={label}>
<label className="text-xs text-slate-500 mb-1 flex items-center justify-between">
<span>{label}</span>
{config?.[`${configKey}_set`] ? (
<span className="text-emerald-400 text-[10px] font-semibold"> active</span>
) : (
<span className="text-slate-600 text-[10px]">not set</span>
)}
</label>
<input
type={showKeys ? 'text' : 'password'}
value={value}
onChange={e => setter(e.target.value)}
placeholder={placeholder}
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"
/>
</div>
))}
</div>
<button
onClick={saveKeys}
disabled={savingKeys || (!newsapiKey && !eiaKey && !fredKey && !fmpKey)}
className="mt-3 flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingKeys ? 'Saving...' : 'Save keys'}
</button>
</div>
<SaxoConnectionCard />
{/* RSS / data sources */}
{isLoading ? (
<div className="card animate-pulse h-40 bg-dark-700" />
) : (
<>
{Object.entries(SOURCE_CATEGORIES).map(([catName, keys]) => (
<div key={catName} className="card">
<div className="section-title flex items-center gap-1">
<Globe className="w-3 h-3" /> {catName}
</div>
<div className="space-y-2">
{keys.map(key => {
const src = displaySources[key] ?? {}
const enabled = src.enabled ?? false
const requiresKey = src.requires_key
const doc = SOURCE_DOCS[key]
const keySet = requiresKey ? !!config?.[requiresKey + '_set'] : true
return (
<div key={key} className={clsx('flex items-start justify-between p-3 rounded border transition-all',
enabled ? 'border-blue-500/40 bg-blue-900/5' : 'border-slate-700/30 bg-dark-700/30')}>
<div className="flex-1 min-w-0 mr-3">
<div className="flex items-center gap-2">
<span className="text-xs text-white font-semibold">{src.name || key}</span>
<span className={clsx('badge text-xs', {
'badge-green': doc?.cost === 'Free' || doc?.cost === 'Totally free',
'badge-blue': doc?.cost?.includes('free') || doc?.cost?.includes('registration'),
'badge-yellow': doc?.cost?.includes('key') || doc?.cost?.includes('key'),
'badge-red': doc?.cost?.includes('Paid'),
})}>
{doc?.cost}
</span>
{requiresKey && !keySet && <span className="badge badge-orange text-xs">Key required</span>}
</div>
<div className="text-xs text-slate-500 mt-0.5">{doc?.description}</div>
</div>
<button onClick={() => toggleSource(key)} disabled={requiresKey && !keySet}
className={clsx('shrink-0 w-10 h-5 rounded-full border transition-all relative',
enabled ? 'bg-blue-600 border-blue-500' : 'bg-dark-600 border-slate-600',
requiresKey && !keySet && 'opacity-40 cursor-not-allowed')}>
<div className={clsx('absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all',
enabled ? 'left-5' : 'left-0.5')} />
</button>
</div>
)
})}
</div>
</div>
))}
<div className="flex justify-end">
<button onClick={saveSources} disabled={savingSources || !localSources}
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingSources ? 'Saving...' : 'Apply changes'}
</button>
</div>
</>
)}
</div>
)}
{/* ── Options — Paramètres ── */}
{configTab === 'options' && (
<div className="space-y-5">
{/* ── IV Gate — Paramètres d'entrée ── */}
<div className="card border-blue-700/30 bg-blue-900/5">
<div className="flex items-center justify-between mb-1">
<h2 className="text-base font-bold text-white flex items-center gap-2">
<ShieldAlert className="w-4 h-4 text-blue-400" /> IV Gate Entry Filters
</h2>
<button
onClick={() => setIvGateEnabled(!ivGateEnabled)}
className={clsx('text-xs px-3 py-1 rounded border font-semibold transition-all', {
'bg-blue-600 border-blue-500 text-white': ivGateEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !ivGateEnabled,
})}>
{ivGateEnabled ? '✓ Gate active' : '○ Gate disabled'}
</button>
</div>
<p className="text-xs text-slate-500 mb-5">
Automatically blocks trades with verdict <span className="text-red-400 font-semibold">ALERT</span> before
they are logged in the journal. An ALERT trade is a strategy incompatible with the current volatility regime
(e.g. Long Call at IVR 100%). Blocked trades appear in skipped trades with reason <span className="text-slate-400 font-mono">[IV_GATE]</span>.
</p>
<div className={clsx('space-y-5 transition-opacity', !ivGateEnabled && 'opacity-40 pointer-events-none')}>
{/* IVR High — seuil vol chère */}
<div className="grid grid-cols-2 gap-6">
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">
High vol threshold (IVR High)
</label>
<span className={clsx('font-mono font-bold text-base', ivGateHigh >= 70 ? 'text-orange-400' : 'text-yellow-400')}>
{ivGateHigh}%
</span>
</div>
<input type="range" min="30" max="85" step="5"
value={ivGateHigh}
onChange={e => {
const v = parseInt(e.target.value)
setIvGateHigh(v)
if (ivGateExtreme <= v) setIvGateExtreme(Math.min(v + 10, 100))
}}
className="w-full accent-yellow-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>30% (strict)</span><span>85% (permissif)</span>
</div>
<div className="text-[10px] text-slate-500 mt-1.5 space-y-0.5">
<div>IVR &lt; {ivGateHigh}% <span className="text-emerald-400">Long Call / Long Put OK</span></div>
<div>IVR {ivGateHigh}{ivGateExtreme}% <span className="text-yellow-400">Spreads requis</span> naked long WARN</div>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">
Extreme vol threshold (IVR Extreme)
</label>
<span className="font-mono font-bold text-base text-red-400">{ivGateExtreme}%</span>
</div>
<input type="range" min="50" max="100" step="5"
value={ivGateExtreme}
onChange={e => {
const v = parseInt(e.target.value)
setIvGateExtreme(v)
if (ivGateHigh >= v) setIvGateHigh(Math.max(v - 10, 30))
}}
className="w-full accent-red-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>50%</span><span>100%</span>
</div>
<div className="text-[10px] text-slate-500 mt-1.5 space-y-0.5">
<div>IVR &gt; {ivGateExtreme}% <span className="text-red-400 font-semibold">BLOCKED</span> naked long vol ALERT</div>
<div>Straddle / Strangle ALERT when IVR &gt; {ivGateExtreme}% (penalty ×1.3)</div>
</div>
</div>
</div>
{/* Skew threshold */}
<div className="grid grid-cols-2 gap-6">
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">
Put skew threshold (expensive protection)
</label>
<span className="font-mono font-bold text-base text-orange-400">{ivGateSkew} pts</span>
</div>
<input type="range" min="2" max="20" step="1"
value={ivGateSkew}
onChange={e => setIvGateSkew(parseInt(e.target.value))}
className="w-full accent-orange-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>2pts (very sensitive)</span><span>20pts (very tolerant)</span>
</div>
<div className="text-[10px] text-slate-500 mt-1.5">
Put skew &gt; {ivGateSkew}pts puts very expensive, WARN signal on Long Put
</div>
</div>
{/* Visual summary */}
<div className="bg-dark-700/60 rounded-lg px-4 py-3 text-xs space-y-2">
<div className="text-slate-500 font-semibold mb-2 text-[10px] uppercase tracking-wide">Current gate summary</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-emerald-500" />
<span className="text-slate-400">IVR &lt; <span className="text-emerald-400 font-mono">{ivGateHigh}%</span> Any strategy OK</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-yellow-500" />
<span className="text-slate-400">IVR <span className="text-yellow-400 font-mono">{ivGateHigh}{ivGateExtreme}%</span> Spreads preferred</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-red-500 animate-pulse" />
<span className="text-slate-400">IVR &gt; <span className="text-red-400 font-mono">{ivGateExtreme}%</span> Long naked <strong className="text-red-400">BLOCKED</strong></span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-500" />
<span className="text-slate-400">Skew &gt; <span className="text-orange-400 font-mono">{ivGateSkew}pts</span> Expensive puts (WARN)</span>
</div>
</div>
</div>
</div>
<div className="mt-5 pt-4 border-t border-slate-700/30">
<button
onClick={() => saveOptionsGate(
{ iv_gate_enabled: ivGateEnabled, iv_gate_ivr_high: ivGateHigh,
iv_gate_ivr_extreme: ivGateExtreme, iv_gate_skew_threshold: ivGateSkew },
{ onSuccess: () => { setSavedMsg('IV Gate saved'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingOptionsGate}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingOptionsGate ? 'Saving...' : 'Save gate'}
</button>
</div>
</div>
{/* ── Paramètres de sortie ── */}
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-1">
<Lock className="w-4 h-4 text-amber-400" /> Trade Exit Settings
</h2>
<p className="text-xs text-slate-500 mb-5">
Default values for targets and stop-losses. Each trade can have its own thresholds (Journal Open).
</p>
<div className="grid grid-cols-2 gap-6 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">
Profit target: <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span>
<span className="text-slate-600 ml-1">(underlying P&L)</span>
</label>
<input type="range" min="5" max="150" step="5"
value={exitTarget}
onChange={e => setExitTarget(parseFloat(e.target.value))}
className="w-full accent-emerald-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>+5%</span><span>+150%</span>
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Stop-loss: <span className="text-red-400 font-mono font-bold">{exitStop}%</span>
<span className="text-slate-600 ml-1">(underlying P&L)</span>
</label>
<input type="range" min="-90" max="-5" step="5"
value={exitStop}
onChange={e => setExitStop(parseFloat(e.target.value))}
className="w-full accent-red-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>-90%</span><span>-5%</span>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-6 mb-5">
<div>
<label className="text-xs text-slate-500 mb-2 block">AI reversal signal mode</label>
<div className="flex gap-2">
{[
{ value: 'badge_only', label: '🔔 Badge only' },
{ value: 'auto', label: '⚡ Auto (future)' },
].map(opt => (
<button key={opt.value} onClick={() => setExitSignalMode(opt.value)}
className={clsx('flex-1 py-2 rounded text-xs border transition-all', {
'bg-blue-600 border-blue-500 text-white': exitSignalMode === opt.value,
'bg-dark-700 border-slate-700 text-slate-400 hover:text-slate-200': exitSignalMode !== opt.value,
})}>
{opt.label}
</button>
))}
</div>
<div className="text-[10px] text-slate-600 mt-1">
Badge only: visible alert, you confirm manually. Auto: closure suggested.
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Signal reversal threshold: <span className="text-blue-400 font-mono font-bold">{exitSignalThreshold}pts</span>
</label>
<input type="range" min="10" max="60" step="5"
value={exitSignalThreshold}
onChange={e => setExitSignalThreshold(parseFloat(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>10pts (sensitive)</span><span>60pts (tolerant)</span>
</div>
</div>
</div>
<button
onClick={() => saveExitDefaults(
{ target_pct: exitTarget, stop_loss_pct: exitStop,
signal_reversal_mode: exitSignalMode, signal_reversal_threshold: exitSignalThreshold },
{ onSuccess: () => { setSavedMsg('Exit settings saved'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingExitDefaults}
className="flex items-center gap-1.5 bg-amber-600 hover:bg-amber-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingExitDefaults ? 'Saving...' : 'Save thresholds'}
</button>
</div>
{/* ── Indicateurs techniques du sous-jacent ── */}
<div className="card border-purple-700/30 bg-purple-900/5">
<div className="flex items-center justify-between mb-1">
<h2 className="text-base font-bold text-white flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-purple-400" /> Underlying Technical Indicators
</h2>
<button
onClick={() => setTechEnabled(!techEnabled)}
className={clsx('text-xs px-3 py-1 rounded border font-semibold transition-all', {
'bg-purple-600 border-purple-500 text-white': techEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !techEnabled,
})}>
{techEnabled ? '✓ Enabled' : '○ Disabled'}
</button>
</div>
<p className="text-xs text-slate-500 mb-4">
Injects RSI, MA, Bollinger and ATR into the AI context at each cycle. Periods are automatically calibrated based on the option's horizon.
</p>
<div className={clsx('space-y-4 transition-opacity', !techEnabled && 'opacity-40 pointer-events-none')}>
{/* Indicateurs à activer */}
<div>
<label className="text-xs text-slate-400 font-medium block mb-2">Active indicators</label>
<div className="flex flex-wrap gap-2">
{[
{ key: 'rsi', label: 'RSI', desc: 'Momentum / overbought-oversold' },
{ key: 'ma', label: 'MA fast/slow', desc: 'Trend & golden/death cross' },
{ key: 'bollinger', label: 'Bollinger', desc: 'Position in vol bands' },
{ key: 'atr', label: 'ATR', desc: 'Recent realized volatility' },
].map(({ key, label, desc }) => {
const active = techList.includes(key)
return (
<button
key={key}
title={desc}
onClick={() => setTechList(prev =>
active ? prev.filter(k => k !== key) : [...prev, key]
)}
className={clsx('text-xs px-3 py-1.5 rounded border font-semibold transition-all', {
'bg-purple-700 border-purple-500 text-white': active,
'bg-dark-700 border-slate-700 text-slate-400 hover:border-purple-600': !active,
})}>
{active ? ' ' : ''}{label}
</button>
)
})}
</div>
<p className="text-[10px] text-slate-600 mt-1.5">Hover to see each indicator's description.</p>
</div>
{/* Calibration auto */}
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-300 font-medium">Auto-calibration by horizon</p>
<p className="text-[10px] text-slate-500">30j RSI14, MA20/50 | 90j RSI21, MA50/100 | &gt;90j RSI28, MA100/200</p>
</div>
<button
onClick={() => setTechAutoCalibrate(!techAutoCalibrate)}
className={clsx('text-xs px-3 py-1 rounded border font-semibold transition-all flex-shrink-0 ml-4', {
'bg-purple-600 border-purple-500 text-white': techAutoCalibrate,
'bg-dark-700 border-slate-700 text-slate-400': !techAutoCalibrate,
})}>
{techAutoCalibrate ? '✓ Auto' : '○ Fixed'}
</button>
</div>
</div>
<button
onClick={() => saveTechIndicators(
{ tech_indicators_enabled: techEnabled, tech_indicators_list: techList.join(','), tech_indicators_auto_calibrate: techAutoCalibrate },
{ onSuccess: () => { setSavedMsg('Technical indicators saved'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingTechIndicators}
className="mt-4 flex items-center gap-1.5 bg-purple-700 hover:bg-purple-600 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingTechIndicators ? 'Saving...' : 'Save indicators'}
</button>
</div>
</div>
)}
{/* ── Profils de risque ── */}
{configTab === 'profiles' && <RiskProfilesCard />}
{/* ── Watchlist ── */}
{configTab === 'watchlist' && <WatchlistCard />}
{/* ── Data Management ── */}
{configTab === 'data' && <DataManagementCard />}
</div>
)
}
// ── Data Management card ───────────────────────────────────────────────────────
function PurgeButton({ label, description, tables, endpoint, color = 'red' }: {
label: string
description: string
tables: string[]
endpoint: string
color?: 'red' | 'orange'
}) {
const qc = useQueryClient()
const [confirm, setConfirm] = useState(false)
const [loading, setLoading] = useState(false)
const [done, setDone] = useState<string | null>(null)
const handlePurge = async () => {
setLoading(true)
try {
const res = await fetch(endpoint, { method: 'DELETE' })
const data = await res.json()
setDone(`Deleted ${data.deleted ?? '?'} rows`)
setConfirm(false)
qc.invalidateQueries()
} catch (e: any) {
setDone(`Error: ${e?.message}`)
} finally {
setLoading(false)
}
}
const borderCol = color === 'red' ? 'border-red-800/40' : 'border-orange-800/40'
const bgCol = color === 'red' ? 'bg-red-900/5' : 'bg-orange-900/5'
const btnCol = color === 'red'
? 'bg-red-700 hover:bg-red-600 disabled:opacity-50'
: 'bg-orange-700 hover:bg-orange-600 disabled:opacity-50'
return (
<div className={`border ${borderCol} ${bgCol} rounded-lg p-4`}>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="text-sm font-semibold text-slate-200 mb-0.5">{label}</div>
<div className="text-xs text-slate-500 mb-1.5">{description}</div>
<div className="flex flex-wrap gap-1">
{tables.map(t => (
<span key={t} className="text-[10px] font-mono bg-slate-800 text-slate-400 px-1.5 py-0.5 rounded">{t}</span>
))}
</div>
</div>
<div className="flex-shrink-0 flex flex-col items-end gap-1.5">
{done && <span className="text-[10px] text-emerald-400">{done}</span>}
{!confirm ? (
<button onClick={() => { setConfirm(true); setDone(null) }}
className={`text-xs px-3 py-1.5 rounded text-white font-semibold ${btnCol}`}>
<Trash2 className="w-3 h-3 inline mr-1" />Purge
</button>
) : (
<div className="flex items-center gap-2">
<span className="text-xs text-red-400 font-semibold">Are you sure?</span>
<button onClick={handlePurge} disabled={loading}
className={`text-xs px-3 py-1.5 rounded text-white font-semibold ${btnCol}`}>
{loading ? 'Deleting…' : 'Yes, delete all'}
</button>
<button onClick={() => setConfirm(false)} className="text-xs text-slate-500 hover:text-slate-300">Cancel</button>
</div>
)}
</div>
</div>
</div>
)
}
function DataManagementCard() {
return (
<div className="space-y-4">
<div className="flex items-center gap-2 mb-2">
<DatabaseBackup className="w-4 h-4 text-red-400" />
<h2 className="text-base font-bold text-white">Data Management</h2>
<span className="text-xs text-slate-500 ml-2">Irreversible operations all deletes are permanent</span>
</div>
<PurgeButton
label="System Logs"
description="Clear all system logs immediately (normally kept 30 days)."
tables={['system_logs']}
endpoint="/api/logs/purge-all"
color="orange"
/>
<PurgeButton
label="AI Reports (Rapport IA)"
description="Delete all generated cycle reports and trade assessments."
tables={['cycle_reports', 'ai_reports']}
endpoint="/api/reports/purge-all"
color="orange"
/>
<PurgeButton
label="Portfolio — All Positions"
description="Delete every position (open + closed) and all trade entry price history. Cannot be undone."
tables={['portfolio', 'trade_entry_prices']}
endpoint="/api/portfolio/purge-all"
color="red"
/>
<PurgeButton
label="Analytics History"
description="Purge all pattern score history, regime clusters, embeddings, cycle run logs, and macro/geo alert history."
tables={['pattern_score_history', 'regime_clusters', 'pattern_embeddings', 'cycle_runs', 'macro_regime_history', 'geo_alert_history']}
endpoint="/api/analytics/purge-all"
color="red"
/>
<PurgeButton
label="VaR & PnL Snapshots"
description="Delete all Value-at-Risk snapshots and daily PnL snapshots."
tables={['var_snapshots', 'pnl_snapshots']}
endpoint="/api/var/purge-all"
color="orange"
/>
<PurgeButton
label="Pattern Lab Runs"
description="Delete all Pattern Lab backtest runs (event presets + instrument scans). Does not affect the Pattern Library."
tables={['backtest_lab_runs']}
endpoint="/api/pattern-lab/purge-all"
color="orange"
/>
<PurgeButton
label="Pattern Library"
description="Delete all custom and backtested patterns from the library. Trade ideas will regenerate from scratch on the next cycle. Does not affect Pattern Lab run history."
tables={['custom_patterns']}
endpoint="/api/patterns/purge-all"
color="red"
/>
<div className="mt-2 p-3 border border-slate-700/40 rounded-lg bg-dark-700/30">
<p className="text-xs text-slate-500">
<span className="font-semibold text-slate-400">Note:</span> Trade Ideas are computed live from the Pattern Library they have no separate table. Risk profiles, config, API keys, and the Knowledge Base are managed from their respective sections.
</p>
</div>
</div>
)
}