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 = { 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 (
Prochain cycle dans {remaining} ({absTime} heure locale)
) } 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 (
{profile.name} {!enabled && désactivé} Score ≥ {profile.min_score} Gain ≥ {profile.min_gain_pct}% {fev} Trade Score ≥ {(profile.trade_score_at_frontier ?? 0).toFixed(0)}
) } return (
setName(e.target.value)} className="w-full bg-dark-800 border border-slate-700 rounded px-2 py-1 text-sm text-white" />
setScore(parseInt(e.target.value))} className="w-full accent-blue-500" />
setGain(parseFloat(e.target.value))} className="w-full accent-blue-500" />
{/* Live EV preview */}
À la frontière (score={score}, gain={gain}%) {evText} Trade Score = {trade_score.toFixed(1)}/100 Score min EV=0 : {Math.ceil(100 / (G + 1))}/100
{PROFILE_COLORS.map(c => (
) } function RiskProfilesCard() { const { data: profileData, isLoading } = useRiskProfiles() const { mutate: upsert } = useUpsertProfile() const { mutate: del } = useDeleteProfile() const [showNew, setShowNew] = useState(false) const [newName, setNewName] = useState('') const [newScore, setNewScore] = useState(30) const [newGain, setNewGain] = useState(200) const [newColor, setNewColor] = useState('#3b82f6') const profiles: any[] = (profileData as any)?.profiles ?? [] // Live preview for new profile const np = newScore / 100 const nG = newGain / 100 const nDenom = np * nG + (1 - np) const nEvNet = np * nG - (1 - np) const nTradeScore = nDenom > 0 ? (np * nG / nDenom * 100) : 0 const nMinScore = Math.ceil(100 / (nG + 1)) const saveNew = () => { if (!newName.trim()) return upsert({ name: newName, min_score: newScore, min_gain_pct: newGain, color: newColor, enabled: true, sort_order: profiles.length }) setShowNew(false) setNewName('') setNewScore(30) setNewGain(200) } return (
Profils de risque
Un trade est loggé dans le journal s'il passe au moins un profil activé · Formule : EV nette = (score/100 × gain/100) − (1 − score/100)
{isLoading ? (
) : profiles.length === 0 ? (
Aucun profil — tous les trades seront ignorés
) : (
{profiles.map((p: any) => ( upsert(data)} onDelete={id => del(id)} /> ))}
)} {/* New profile form */} {showNew && (
Nouveau profil
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" />
setNewScore(parseInt(e.target.value))} className="w-full accent-blue-500" />
setNewGain(parseFloat(e.target.value))} className="w-full accent-blue-500" />
{/* Live math */}
{evNetLabel(nEvNet).text} Trade Score = {nTradeScore.toFixed(1)}/100 Score min pour EV=0 avec gain {newGain}% : {nMinScore}/100
{PROFILE_COLORS.map(c => (
)} {/* Frontier visualization */} {profiles.length > 0 && (
Frontière d'acceptation — chaque point représente le minimum requis par profil
{profiles.filter((p: any) => p.enabled).map((p: any) => { const { text: ev, cls } = evNetLabel(p.ev_net_at_frontier ?? 0) return (
{p.name} {p.min_score}pts × {p.min_gain_pct}% → {ev}
) })}
)}
) } 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 | 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 = {} 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 (

Configuration

Clés API, sources, paramètres IA & cycle

{savedMsg && (
{savedMsg}
)}
{/* Tab navigation */}
{CONFIG_TABS.map(({ key, label, icon: Icon }) => ( ))}
{/* ── IA & Analyse ── */} {configTab === 'ia' && (
{/* AI Status */}
Statut IA
{aiStatus?.enabled ? ( <>
OpenAI Connecté
GPT-4o + GPT-4o-mini
) : ( <>
OpenAI Non configuré
Entrer la clé ci-dessous
)}
• Analyse de discours (Trump, Powell...)
• Classification IA des actualités
• Évaluation de patterns
• Top 10 idées contextualisées
{/* OpenAI API Key */}
Clé OpenAI
{aiStatus?.enabled ? '✓ Actif' : '○ Inactif'}
setOpenaiKey(e.target.value)} placeholder="sk-proj-..." className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 font-mono" />
{/* Right: Analysis params */}

Paramètres d'analyse IA

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