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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user