Files
OpenFin/frontend/src/pages/Config.tsx
OpenSquared 67546d39af fix: URLs relatives pour VaR et Config (bypass proxy corrigé)
Remplace http://localhost:8000 par des URLs relatives (/api/...)
pour passer par le proxy Vite/Nginx comme toutes les autres pages.
Cause du NetworkError en prod Docker.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 06:27:43 +02:00

1132 lines
58 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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults } from '../hooks/useApi'
import { Settings, Key, Globe, CheckCircle, XCircle, AlertCircle, Save, Eye, EyeOff, Brain, SlidersHorizontal, RefreshCw, Zap, Plus, Trash2, Pencil, X, Lock, Gauge, DollarSign } from 'lucide-react'
import clsx from 'clsx'
const API = ''
const SOURCE_CATEGORIES = {
'Flux RSS actifs': ['reuters_world', 'reuters_business', 'reuters_energy', 'ap_top', 'aljazeera', 'ft', 'bloomberg'],
'Données économiques': ['newsapi', 'gdelt', 'eia', 'fred', 'usda'],
'Santé & Catastrophes': ['who', 'emdat'],
'Réseaux sociaux': ['twitter_trump'],
}
const SOURCE_DOCS: Record<string, { description: string; link?: string; cost: string }> = {
reuters_world: { description: 'Actualités mondiales Reuters', cost: 'Gratuit' },
reuters_business: { description: 'Business et marchés Reuters', cost: 'Gratuit' },
reuters_energy: { description: 'Énergie et commodités Reuters', cost: 'Gratuit' },
ap_top: { description: 'Associated Press — Top Stories', cost: 'Gratuit' },
aljazeera: { description: 'Al Jazeera — couverture Moyen-Orient/Monde', cost: 'Gratuit' },
ft: { description: 'Financial Times — finance internationale', cost: 'Gratuit' },
bloomberg: { description: 'Bloomberg Markets RSS', cost: 'Gratuit' },
newsapi: { description: '100 req/jour gratuit — actualités mondiales multi-sources', link: 'https://newsapi.org', cost: '100 req/j gratuit' },
gdelt: { description: 'Base de données géopolitique mondiale — 300K events/jour', link: 'https://gdeltproject.org', cost: 'Gratuit total' },
eia: { description: 'US Energy Information Administration — données pétrole/gaz hebdo', link: 'https://www.eia.gov/opendata', cost: 'Gratuit avec clé' },
fred: { description: 'Federal Reserve St. Louis — macro US (PIB, inflation, emploi)', link: 'https://fred.stlouisfed.org/docs/api/fred/', cost: 'Gratuit avec clé' },
usda: { description: 'USDA — rapports agricoles officiels US', cost: 'Gratuit' },
who: { description: 'OMS — alertes sanitaires mondiales RSS', cost: 'Gratuit' },
emdat: { description: 'EM-DAT — base de données catastrophes naturelles', link: 'https://www.emdat.be', cost: 'Inscription gratuite' },
twitter_trump: { description: 'Flux X/Twitter Trump — discours, annonces tarifs', cost: 'API payante ($100/mois+)' },
}
// ── Risk Profiles Component ───────────────────────────────────────────────────
const PROFILE_COLORS = [
{ value: '#22c55e', label: 'Vert' },
{ value: '#3b82f6', label: 'Bleu' },
{ value: '#f97316', label: 'Orange' },
{ value: '#ef4444', label: 'Rouge' },
{ value: '#8b5cf6', label: 'Violet' },
{ value: '#eab308', label: 'Jaune' },
]
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>Prochain cycle dans <span className="font-mono font-semibold text-emerald-300">{remaining}</span></span>
<span className="text-slate-600">({absTime} heure locale)</span>
</div>
)
}
function evNetLabel(evNet: number): { text: string; cls: string } {
if (evNet > 0.05) return { text: `EV nette +${(evNet * 100).toFixed(0)}%`, cls: 'text-emerald-400' }
if (evNet >= -0.01) return { text: 'EV nette ≈ 0', cls: 'text-yellow-400' }
return { text: `EV nette ${(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">désactivé</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">Nom du profil</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">À la frontière (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 ? '✓ Activé' : '○ Désactivé'}
</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">Annuler</button>
<button onClick={save}
className="text-xs bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded font-semibold">
Sauvegarder
</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">Profils de risque</div>
<div className="text-[10px] text-slate-600 mt-0.5">
Un trade est loggé dans le journal s'il passe <span className="text-slate-500">au moins un</span> profil activé
· Formule : EV nette = (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" /> Nouveau profil
</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">Aucun profil — tous les trades seront ignorés</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">Nouveau profil</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">Nom</label>
<input value={newName} onChange={e => setNewName(e.target.value)} placeholder="ex: Ultra-risqué"
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">Score min pour EV=0 avec 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">Annuler</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">
Créer
</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">Frontière d'acceptation chaque point représente le minimum requis par profil</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>
)
}
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 [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 [showKeys, setShowKeys] = useState(false)
const [savedMsg, setSavedMsg] = useState('')
const [configTab, setConfigTab] = useState<'ia' | 'cycle' | 'sources' | 'journal' | 'profiles'>('ia')
// 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 sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) },
})
const { mutate: triggerVarNow, isPending: triggeringVar } = useMutation({
mutationFn: () => fetch(`${API}/api/var/run-now`, { method: 'POST' }).then(r => r.json()),
onSuccess: () => { setSavedMsg('Snapshot VaR sauvegardé'); 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('Snapshot PnL sauvegardé'); 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 [retentionDays, setRetentionDays] = useState(90)
const [maturityThreshold, setMaturityThreshold] = useState(35)
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)
setRetentionDays(cs.journal_retention_days ?? 90)
setMaturityThreshold(cs.maturity_threshold_pct ?? 35)
}
}, [cs])
useEffect(() => {
if (analysisCfg) {
setAnalysisTopN(analysisCfg.top_n ?? 10)
setAnalysisCategoryDefault(analysisCfg.category_filter ?? 'all')
setAnalysisTemplate(analysisCfg.template ?? '')
}
}, [analysisCfg])
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 sauvegardées'); 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
updateApiKeys(keys, {
onSuccess: () => {
setSavedMsg('Clés API sauvegardées')
setTimeout(() => setSavedMsg(''), 2000)
setOpenaiKey(''); setNewsapiKey(''); setEiaKey(''); setFredKey('')
}
})
}
const CONFIG_TABS = [
{ key: 'ia', label: 'IA & Analyse', icon: Brain },
{ key: 'cycle', label: 'Auto-Cycle & Logging', icon: Zap },
{ key: 'sources', label: 'Sources', icon: Globe },
{ key: 'journal', label: 'Journal & Sortie', icon: Lock },
{ key: 'profiles', label: 'Profils de risque', icon: SlidersHorizontal },
] 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">Clés API, sources, paramètres IA &amp; cycle</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" /> Statut IA</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 Connecté</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 Non configuré</div>
<div className="text-xs text-slate-500">Entrer la clé ci-dessous</div></div></>
)}
</div>
<div className="text-xs text-slate-600 space-y-1">
<div> Analyse de discours (Trump, Powell...)</div>
<div> Classification IA des actualités</div>
<div> Évaluation de patterns</div>
<div> Top 10 idées contextualisées</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" /> Clé OpenAI</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 ? '✓ Actif' : '○ Inactif'}
</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 ? 'Sauvegarde...' : 'Sauvegarder la clé'}
</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" /> Paramètres d'analyse IA
</h2>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-1 block">Top N résultats par défaut</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">Catégorie par défaut</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">Toutes les catégories</option>
<option value="energy">Énergie</option>
<option value="metals">Métaux</option>
<option value="agriculture">Agriculture</option>
<option value="indices">Indices</option>
<option value="equities">Actions</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">Template d'analyse (prompt GPT-4o)</label>
<button onClick={() => setAnalysisTemplate('')} className="text-xs text-slate-600 hover:text-slate-400">
Par défaut
</button>
</div>
<textarea
value={analysisTemplate}
onChange={e => setAnalysisTemplate(e.target.value)}
rows={8}
placeholder="Laisser vide pour utiliser le template par défaut..."
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">
Contexte marché (prix, IV, variation 1j) et news filtrées sont injectés automatiquement.
</div>
</div>
<button
onClick={() => saveAnalysis(
{ top_n: analysisTopN, category_filter: analysisCategoryDefault, template: analysisTemplate || undefined },
{ onSuccess: () => { setSavedMsg('Paramètres d\'analyse sauvegardés'); 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 ? 'Sauvegarde...' : 'Sauvegarder'}
</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" /> Score minimum pour le cycle
</h2>
<p className="text-xs text-slate-500 mb-4">
Pré-filtre du cycle les patterns avec un score inférieur ne sont pas soumis à l'analyse.
Les profils de risque déterminent ensuite quels trades sont loggés.
</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 (tous)</span><span>80 (très sélectif)</span>
</div>
<div className="text-xs text-slate-500 mt-2">
{minScore === 0
? 'Tous les patterns analysés par le cycle'
: `Seuls les patterns avec score ≥ ${minScore} sont soumis au 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
? 'Toute EV nette acceptée'
: minEv > 0
? `EV nette ≥ +${(minEv * 100).toFixed(0)}% requise`
: `Filtre désactivé (EV < ${(minEv * 100).toFixed(0)}%)`}
</div>
</div>
</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" /> En cours...
</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 actif
</span>
) : (
<span className="text-slate-600 bg-dark-700 border border-slate-700/30 px-2 py-0.5 rounded">
○ Scheduler inactif
</span>
)}
</div>
)}
</div>
<p className="text-xs text-slate-500 mb-4">
Toutes les N heures : suggère de nouveaux patterns, filtre les doublons, score tout, log les prix et génère un commentaire IA.
</p>
<div className="grid grid-cols-3 gap-4 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">Activer l'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 ? '✓ Activé' : '○ Désactivé'}
</button>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">Intervalle (heures)</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">
Similarité max ({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">
Rétention Journal ({retentionDays}j)
</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">
Seuil maturité ({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>
{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">Dernier 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} scorés</span>
<span className="text-slate-500">Géo: {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, journal_retention_days: retentionDays, maturity_threshold_pct: maturityThreshold },
{ onSuccess: () => { refetchCycle(); setSavedMsg('Auto-cycle configuré'); 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 ? 'Sauvegarde...' : 'Appliquer'}
</button>
<button
onClick={() => triggerCycle(undefined, { onSuccess: () => { refetchCycle(); setSavedMsg('Cycle lancé !'); 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')} />
Lancer maintenant
</button>
</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">
Calcule et sauvegarde automatiquement les snapshots VaR (Black-Scholes delta) et PnL à intervalles définis.
Les données sont accessibles depuis la page VaR Analyse avec un historique complet.
</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"> Actif</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"> Inactif</span>}
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Activer</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 ? '✓ Activé' : '○ Désactivé'}
</button>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Intervalle</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"> Actif</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"> Inactif</span>}
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Activer</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 ? '✓ Activé' : '○ Désactivé'}
</button>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Intervalle</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 ? 'Sauvegarde…' : 'Appliquer'}
</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')} />
Snapshot VaR maintenant
</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')} />
Snapshot PnL maintenant
</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" /> Clés API données</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-3 gap-4">
{[
{ label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Obtenir sur newsapi.org' },
{ label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Obtenir sur eia.gov' },
{ label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Obtenir sur fred.stlouisfed.org' },
].map(({ label, value, setter, placeholder }) => (
<div key={label}>
<label className="text-xs text-slate-500 mb-1 block">{label}</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)}
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 ? 'Sauvegarde...' : 'Sauvegarder les clés'}
</button>
</div>
{/* 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 === 'Gratuit' || doc?.cost === 'Gratuit total',
'badge-blue': doc?.cost?.includes('gratuit') || doc?.cost?.includes('Inscription'),
'badge-yellow': doc?.cost?.includes('clé'),
'badge-red': doc?.cost?.includes('payante'),
})}>
{doc?.cost}
</span>
{requiresKey && !keySet && <span className="badge badge-orange text-xs">Clé requise</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 ? 'Sauvegarde...' : 'Appliquer les changements'}
</button>
</div>
</>
)}
</div>
)}
{/* ── Journal & Sortie ── */}
{configTab === 'journal' && (
<div className="card">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-4">
<Lock className="w-4 h-4 text-amber-400" /> Paramètres de sortie des trades
</h2>
<p className="text-xs text-slate-500 mb-4">
Valeurs par défaut pour les objectifs et stop-loss. Chaque trade peut avoir ses propres seuils (Journal Ouverts).
</p>
<div className="grid grid-cols-2 gap-6 mb-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">
Objectif de gain : <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span>
<span className="text-slate-600 ml-1">(P&L sous-jacent)</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">(P&L sous-jacent)</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-4">
<div>
<label className="text-xs text-slate-500 mb-2 block">Mode signal IA de retournement</label>
<div className="flex gap-2">
{[
{ value: 'badge_only', label: '🔔 Badge uniquement' },
{ value: 'auto', label: '⚡ Auto (futur)' },
].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 : alerte visible, vous confirmez manuellement. Auto : fermeture proposée.
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Seuil retournement signal : <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 (sensible)</span><span>60pts (tolérant)</span>
</div>
</div>
</div>
<button
onClick={() => saveExitDefaults(
{ target_pct: exitTarget, stop_loss_pct: exitStop,
signal_reversal_mode: exitSignalMode, signal_reversal_threshold: exitSignalThreshold },
{ onSuccess: () => { setSavedMsg('Paramètres de sortie sauvegardés'); 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 ? 'Sauvegarde...' : 'Sauvegarder les seuils'}
</button>
</div>
)}
{/* ── Profils de risque ── */}
{configTab === 'profiles' && <RiskProfilesCard />}
</div>
)
}