refactor: move trade ideas to Journal tab, tabify Config, trim Dashboard
- Extract TradeCard/TradeRow/IBKRTicket + TradeIdeasTab self-contained component to frontend/src/components/TradeIdeas.tsx - Dashboard: remove trade ideas section, MtM section, all related hooks/ state/computations; replace Signaux Géo mini-card with Trades du cycle; add News géo link on Risque Géopolitique card - JournalDeBord: add Idées de trade tab (left of Ouverts) using TradeIdeasTab - Config: reorganize into 5 tabs — IA & Analyse, Auto-Cycle & Logging, Sources, Journal & Sortie, Profils de risque; expose min_score_threshold as a prominent slider in the Auto-Cycle tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -348,6 +348,7 @@ export default function Config() {
|
||||
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)
|
||||
@@ -428,24 +429,48 @@ export default function Config() {
|
||||
})
|
||||
}
|
||||
|
||||
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-6">
|
||||
<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 d'information, paramètres IA</p>
|
||||
<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 & 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>
|
||||
|
||||
{savedMsg && (
|
||||
<div className="card border-emerald-700/40 bg-emerald-900/10 text-emerald-400 text-sm flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" /> {savedMsg}
|
||||
</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>
|
||||
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
{/* Left col: API Keys + AI status */}
|
||||
<div className="col-span-1 space-y-4">
|
||||
{/* ── 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>
|
||||
@@ -468,30 +493,328 @@ export default function Config() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Keys */}
|
||||
{/* 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és API</div>
|
||||
<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>
|
||||
</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: 'OpenAI API Key', value: openaiKey, setter: setOpenaiKey, placeholder: 'sk-proj-...', status: aiStatus?.enabled },
|
||||
{ label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Obtenir sur newsapi.org', status: false },
|
||||
{ label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Obtenir sur eia.gov', status: false },
|
||||
{ label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Obtenir sur fred.stlouisfed.org', status: false },
|
||||
].map(({ label, value, setter, placeholder, status }) => (
|
||||
{ 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}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="text-xs text-slate-500">{label}</label>
|
||||
{status !== undefined && (
|
||||
<span className={clsx('text-xs', status ? 'text-emerald-400' : 'text-slate-600')}>
|
||||
{status ? '✓ Actif' : '○ Inactif'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">{label}</label>
|
||||
<input
|
||||
type={showKeys ? 'text' : 'password'}
|
||||
value={value}
|
||||
@@ -501,20 +824,17 @@ export default function Config() {
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={saveKeys}
|
||||
disabled={savingKeys || (!openaiKey && !newsapiKey && !eiaKey && !fredKey)}
|
||||
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 les clés'}
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{/* Right col: Sources */}
|
||||
<div className="col-span-2 space-y-4">
|
||||
{/* RSS / data sources */}
|
||||
{isLoading ? (
|
||||
<div className="card animate-pulse h-40 bg-dark-700" />
|
||||
) : (
|
||||
@@ -531,17 +851,9 @@ export default function Config() {
|
||||
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 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>
|
||||
@@ -553,27 +865,16 @@ export default function Config() {
|
||||
})}>
|
||||
{doc?.cost}
|
||||
</span>
|
||||
{requiresKey && !keySet && (
|
||||
<span className="badge badge-orange text-xs">Clé requise</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 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>
|
||||
)
|
||||
@@ -581,13 +882,9 @@ export default function Config() {
|
||||
</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"
|
||||
>
|
||||
<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>
|
||||
@@ -595,154 +892,79 @@ export default function Config() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Paramètres d'analyse IA ── */}
|
||||
<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">Nombre de résultats par défaut (Top N)</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>
|
||||
))}
|
||||
{/* ── 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>
|
||||
<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 envoyé à GPT-4o)</label>
|
||||
<button onClick={() => setAnalysisTemplate('')}
|
||||
className="text-xs text-slate-600 hover:text-slate-400">
|
||||
Remettre par défaut
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={analysisTemplate}
|
||||
onChange={e => setAnalysisTemplate(e.target.value)}
|
||||
rows={10}
|
||||
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">
|
||||
Variables disponibles dans le template : le contexte marché (prix, IV, variation 1j) et les news filtrées par keywords sont toujours injectées automatiquement.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<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>
|
||||
{savedMsg && <span className="text-xs text-emerald-400">{savedMsg}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Fermeture de trades (Exit Defaults) ── */}
|
||||
<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 (via la colonne Cible/Stop dans 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 du 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 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>
|
||||
<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 du 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 par le système.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">
|
||||
Seuil de retournement signal : <span className="text-blue-400 font-mono font-bold">{exitSignalThreshold}pts</span>
|
||||
<span className="text-slate-600 ml-1">(chute du score)</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>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => saveExitDefaults(
|
||||
{ target_pct: exitTarget, stop_loss_pct: exitStop,
|
||||
@@ -754,159 +976,12 @@ export default function Config() {
|
||||
<Save className="w-4 h-4" />
|
||||
{savingExitDefaults ? 'Sauvegarde...' : 'Sauvegarder les seuils'}
|
||||
</button>
|
||||
{savedMsg && <span className="text-xs text-emerald-400">{savedMsg}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Auto-Cycle ── */}
|
||||
<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>
|
||||
{/* ── Profils de risque ── */}
|
||||
{configTab === 'profiles' && <RiskProfilesCard />}
|
||||
|
||||
<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 sur les performances.
|
||||
</p>
|
||||
|
||||
<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 — trades effacés après N jours)
|
||||
</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 de maturité ({maturityThreshold}% — trades en-dessous exclus des stats)
|
||||
</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>
|
||||
|
||||
<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)}% — nouveaux patterns en-dessous)
|
||||
</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>
|
||||
|
||||
{/* Risk Profiles */}
|
||||
<RiskProfilesCard />
|
||||
|
||||
{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>
|
||||
{savedMsg && <span className="text-xs text-emerald-400">{savedMsg}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef, Fragment } from 'react'
|
||||
import { BookOpen, TrendingUp, TrendingDown, Activity, AlertTriangle, RefreshCw, Zap, CheckCircle, XCircle, Brain, Trash2, Search, X, ChevronDown, ChevronUp, Terminal, Lock, ShieldAlert, PieChart } from 'lucide-react'
|
||||
import { BookOpen, TrendingUp, TrendingDown, Activity, AlertTriangle, RefreshCw, Zap, CheckCircle, XCircle, Brain, Trash2, Search, X, ChevronDown, ChevronUp, Terminal, Lock, ShieldAlert, PieChart, Target } from 'lucide-react'
|
||||
import { TradeIdeasTab } from '../components/TradeIdeas'
|
||||
import clsx from 'clsx'
|
||||
import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useClosedTrades, useCloseTrade, useUpdateExitParams, useExitDefaults, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, useSimPortfolioRisk, useSkippedTrades, api } from '../hooks/useApi'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
@@ -1441,6 +1442,7 @@ function SimPortfolioRiskPanel() {
|
||||
const TABS = [
|
||||
{ key: 'cycles', label: 'Cycles IA', icon: Zap },
|
||||
{ key: 'macro', label: 'Régimes Macro', icon: Activity },
|
||||
{ key: 'ideas', label: 'Idées de trade', icon: Target },
|
||||
{ key: 'mtm', label: 'Ouverts', icon: TrendingUp },
|
||||
{ key: 'closed', label: 'Fermés', icon: Lock },
|
||||
{ key: 'geo', label: 'Alertes Géo', icon: AlertTriangle },
|
||||
@@ -1541,7 +1543,7 @@ function SkippedTradesSection({ days }: { days: number }) {
|
||||
}
|
||||
|
||||
export default function JournalDeBord() {
|
||||
const [tab, setTab] = useState<'cycles' | 'macro' | 'mtm' | 'closed' | 'geo' | 'skipped'>('cycles')
|
||||
const [tab, setTab] = useState<'cycles' | 'macro' | 'ideas' | 'mtm' | 'closed' | 'geo' | 'skipped'>('cycles')
|
||||
const [days, setDays] = useState(15)
|
||||
const [confirmReset, setConfirmReset] = useState(false)
|
||||
const [resetting, setResetting] = useState(false)
|
||||
@@ -1696,6 +1698,7 @@ export default function JournalDeBord() {
|
||||
{/* Content */}
|
||||
{tab === 'cycles' && <CyclesSection />}
|
||||
{tab === 'macro' && <MacroHistorySection days={days} />}
|
||||
{tab === 'ideas' && <TradeIdeasTab />}
|
||||
{tab === 'mtm' && <TradeMtmSection days={days} />}
|
||||
{tab === 'closed' && <ClosedTradesSection days={days} />}
|
||||
{tab === 'geo' && <GeoHistorySection days={days} />}
|
||||
|
||||
Reference in New Issue
Block a user