feat: trade lifecycle management — close, archive, target/stop alerts

- DB: 9 new columns on trade_entry_prices (status, closed_at, close_reason,
  close_note, pnl_realized, close_price, target_pct, stop_loss_pct, signal_threshold)
  via ALTER TABLE migration; close_trade(), get_closed_trades(),
  update_trade_exit_params() helpers; exit_defaults config key
- Backend: PATCH /trades/{id}/close, PATCH /trades/{id}/exit-params,
  GET/PUT /exit-defaults, GET /closed-trades with win-rate/avg-PnL stats;
  trade-mtm now computes alert_type (target_reached|stop_loss) per trade
- Journal: new "Fermés" tab with closed trades table + stats banner (win rate,
  avg PnL, total PnL, best trade); open trades show Cible/Stop progress bar +
  🎯/🛑 alert badges + 1-click close modal (price, reason, note)
- Config: new "Paramètres de sortie" panel — target_pct, stop_loss_pct,
  signal_reversal_mode, signal_reversal_threshold with live sliders

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-19 14:17:29 +02:00
parent 0ba73cae0b
commit ee69f3cbd9
5 changed files with 673 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useRiskProfiles, useUpsertProfile, useDeleteProfile } from '../hooks/useApi'
import { Settings, Key, Globe, CheckCircle, XCircle, AlertCircle, Save, Eye, EyeOff, Brain, SlidersHorizontal, RefreshCw, Zap, Plus, Trash2, Pencil, X } from 'lucide-react'
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 } from 'lucide-react'
import clsx from 'clsx'
const SOURCE_CATEGORIES = {
@@ -338,6 +338,8 @@ export default function Config() {
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('')
@@ -352,6 +354,21 @@ export default function Config() {
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])
// Auto-cycle local state
const cs = cycleStatus as any
const [cycleEnabled, setCycleEnabled] = useState(false)
@@ -651,6 +668,96 @@ export default function Config() {
</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>
</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,
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>
{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">