- Investi{isEstimated ? ' ~' : ''}
+ >
+ ) : (
+ <>
+
= 0 ? 'text-emerald-400' : 'text-red-400')}>
+ {pfPnlPct !== null ? `${pfPnlPct >= 0 ? '+' : ''}${pfPnlPct.toFixed(1)}%` : '—'}
-
- {totalCapital > 0 ? `${totalCapital.toFixed(0)}€` : '—'}
+
= 0 ? 'text-emerald-500' : 'text-red-400')}>
+ {pfPnl != null ? `${pfPnl >= 0 ? '+' : ''}${pfPnl.toFixed(0)}€` : '—'}
-
-
-
P&L €
-
= 0 ? 'text-emerald-400' : 'text-red-400')}>
- {totalCapital > 0 ? `${totalProfit >= 0 ? '+' : ''}${totalProfit.toFixed(0)}€` : '—'}
+
+ {pf?.open_positions ?? 0} positions
+ >
+ )}
+ {(targetHit > 0 || stopHit > 0) && (
+
+ {targetHit > 0 && 🎯{targetHit} }
+ {stopHit > 0 && ⛔{stopHit} }
-
- >
- ) : (
-
-
-
= 0 ? 'text-emerald-400' : 'text-red-400')}>
- {pfPnlPct !== null ? `${pfPnlPct >= 0 ? '+' : ''}${pfPnlPct.toFixed(1)}%` : '—'}
-
-
- {pf?.open_positions ?? 0} positions ouvertes
-
-
-
- {pfInvest != null &&
{pfInvest.toFixed(0)}€ investi
}
- {pfPnl != null &&
= 0 ? 'text-emerald-400' : 'text-red-400')}>
- {pfPnl >= 0 ? '+' : ''}{pfPnl.toFixed(0)}€
-
}
- {pfReal != null && pfReal !== 0 &&
= 0 ? 'text-emerald-600' : 'text-red-600')}>
- réal. {pfReal >= 0 ? '+' : ''}{pfReal.toFixed(0)}€
-
}
-
+ )}
- )}
+
+ {/* Right: Realized */}
+
+
Réalisé
+ {pnlView === 'simulated' ? (
+ <>
+
= 0 ? 'text-emerald-400' : 'text-red-400')}>
+ {closedTrades.length === 0 ? '—'
+ : `${closedProfit >= 0 ? '+' : ''}${closedProfit.toFixed(0)}€`}
+
+
+ {closedCapital > 0
+ ? `${(closedProfit / closedCapital * 100).toFixed(1)}%`
+ : closedTrades.length > 0 ? 'sans capital' : ''}
+
+
+ {closedTrades.length} fermés · {closedWinners}✓ {' '}
+ {closedLosers}✗
+
+ >
+ ) : (
+ <>
+
0 ? 'text-emerald-400' : 'text-red-400')}>
+ {pfReal != null && pfReal !== 0
+ ? `${pfReal >= 0 ? '+' : ''}${pfReal.toFixed(0)}€`
+ : '—'}
+
+ {pfNet != null && (
+
= 0 ? 'text-emerald-500/70' : 'text-red-400/70')}>
+ net {pfNet >= 0 ? '+' : ''}{pfNet.toFixed(0)}€
+
+ )}
+
+ {pf?.closed_positions ?? 0} fermées
+
+ >
+ )}
+
+
+
+ {/* Total line */}
+
+
+ Investi{isEstimated ? ' ~' : ''} {pnlView === 'simulated'
+ ? (totalCapital > 0 ? `${totalCapital.toFixed(0)}€` : '—')
+ : (pfInvest != null ? `${pfInvest.toFixed(0)}€` : '—')}
+
+ = 0 ? 'text-emerald-400' : 'text-red-400')}>
+ Total {pnlView === 'simulated'
+ ? (totalCapital > 0 ? `${totalProfit >= 0 ? '+' : ''}${totalProfit.toFixed(0)}€` : '—')
+ : (pfNet != null ? `${pfNet >= 0 ? '+' : ''}${pfNet.toFixed(0)}€` : '—')}
+
+
{/* PnL historical sparkline */}
{(() => {
const snaps: any[] = [...(pnlHistoryData?.snapshots ?? [])].reverse()
diff --git a/frontend/src/pages/JournalDeBord.tsx b/frontend/src/pages/JournalDeBord.tsx
index 1febb33..d8cc811 100644
--- a/frontend/src/pages/JournalDeBord.tsx
+++ b/frontend/src/pages/JournalDeBord.tsx
@@ -1,8 +1,8 @@
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, Target } from 'lucide-react'
-import { TradeIdeasTab } from '../components/TradeIdeas'
+import { TradeIdeasTab, CATEGORIES } 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 { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useClosedTrades, useCloseTrade, useUpdateExitParams, useExitDefaults, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, useSimPortfolioRisk, useSkippedTrades, useDeleteTrade, api } from '../hooks/useApi'
import { useQueryClient } from '@tanstack/react-query'
import { format } from 'date-fns'
import { fr } from 'date-fns/locale'
@@ -227,13 +227,118 @@ function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }
)
}
+// ── Shared filter bar ────────────────────────────────────────────────────────
+
+const DIRECTIONS = [
+ { key: 'all', label: 'Tous' },
+ { key: 'bullish', label: '🐂 Haussier' },
+ { key: 'bearish', label: '🐻 Baissier' },
+]
+
+function _isBearishStr(strategy: string) {
+ return /put|bear|short|sell|vente|baissier/i.test(strategy ?? '')
+}
+
+interface FilterBarProps {
+ search: string; onSearch: (v: string) => void
+ assetClass: string; onAssetClass: (v: string) => void
+ direction?: string; onDirection?: (v: string) => void
+ pnlFilter?: string; onPnlFilter?: (v: string) => void
+}
+
+function FilterBar({ search, onSearch, assetClass, onAssetClass, direction, onDirection, pnlFilter, onPnlFilter }: FilterBarProps) {
+ return (
+
+ {/* Text search */}
+
+
+ onSearch(e.target.value)}
+ placeholder="Ticker, stratégie…"
+ className="pl-6 pr-2 py-1 bg-dark-700 border border-slate-700/50 rounded text-xs text-slate-300 placeholder-slate-600 w-36 focus:outline-none focus:border-slate-500"
+ />
+ {search && (
+ onSearch('')} className="absolute right-1.5 top-1/2 -translate-y-1/2 text-slate-600 hover:text-slate-400">
+
+
+ )}
+
+ {/* Asset class */}
+
+ {CATEGORIES.map(c => (
+ onAssetClass(c.key)}
+ className={clsx('px-2 py-1 rounded text-xs transition-colors', {
+ 'bg-blue-600 text-white': assetClass === c.key,
+ 'text-slate-400 hover:text-slate-200': assetClass !== c.key,
+ })}>
+ {c.label}
+
+ ))}
+
+ {/* Direction */}
+ {onDirection && (
+
+ {DIRECTIONS.map(d => (
+ onDirection(d.key)}
+ className={clsx('px-2 py-1 rounded text-xs transition-colors', {
+ 'bg-slate-600 text-white': direction === d.key,
+ 'text-slate-400 hover:text-slate-200': direction !== d.key,
+ })}>
+ {d.label}
+
+ ))}
+
+ )}
+ {/* P&L filter (Fermés only) */}
+ {onPnlFilter && (
+
+ {[
+ { key: 'all', label: 'Tous' },
+ { key: 'winners', label: '✓ Gagnants' },
+ { key: 'losers', label: '✗ Perdants' },
+ ].map(p => (
+ onPnlFilter(p.key)}
+ className={clsx('px-2 py-1 rounded text-xs transition-colors', {
+ 'bg-slate-600 text-white': pnlFilter === p.key,
+ 'text-slate-400 hover:text-slate-200': pnlFilter !== p.key,
+ })}>
+ {p.label}
+
+ ))}
+
+ )}
+
+ )
+}
+
// ── Closed Trades Section ─────────────────────────────────────────────────────
function ClosedTradesSection({ days }: { days: number }) {
const { data, isLoading, refetch, isFetching } = useClosedTrades(days)
- const trades: any[] = (data as any)?.trades ?? []
+ const { mutate: deleteTrade, isPending: deleting } = useDeleteTrade()
+ const allTrades: any[] = (data as any)?.trades ?? []
const stats: any = (data as any)?.stats ?? {}
+ const [search, setSearch] = useState('')
+ const [assetClass, setAssetClass] = useState('all')
+ const [direction, setDirection] = useState('all')
+ const [pnlFilter, setPnlFilter] = useState('all')
+ const [confirmDeleteId, setConfirmDeleteId] = useState
(null)
+
+ const trades = allTrades.filter((t: any) => {
+ const q = search.toLowerCase()
+ if (q && !`${t.underlying} ${t.strategy ?? ''} ${t.pattern_name ?? ''}`.toLowerCase().includes(q)) return false
+ if (assetClass !== 'all' && t.asset_class !== assetClass) return false
+ const bearish = _isBearishStr(t.strategy ?? '')
+ if (direction === 'bullish' && bearish) return false
+ if (direction === 'bearish' && !bearish) return false
+ if (pnlFilter === 'winners' && (t.pnl_realized ?? 0) < 0) return false
+ if (pnlFilter === 'losers' && (t.pnl_realized ?? 0) >= 0) return false
+ return true
+ })
+
const REASON_META: Record = {
target: { label: '🎯 Objectif', color: 'text-emerald-400' },
stop_loss: { label: '🛑 Stop-loss', color: 'text-red-400' },
@@ -244,7 +349,7 @@ function ClosedTradesSection({ days }: { days: number }) {
return (
{/* Stats banner */}
- {trades.length > 0 && (
+ {allTrades.length > 0 && (
{[
{ label: 'Trades fermés', value: stats.total ?? 0, sub: '', color: 'text-white' },
@@ -261,9 +366,9 @@ function ClosedTradesSection({ days }: { days: number }) {
)}
-
+
- {trades.length} trades clôturés · {days} derniers jours
+ {trades.length}/{allTrades.length} trades clôturés · {days} derniers jours
refetch()} disabled={isFetching}
className="text-slate-600 hover:text-slate-400 disabled:opacity-40">
@@ -271,13 +376,24 @@ function ClosedTradesSection({ days }: { days: number }) {
+
+
{isLoading ? (
- ) : trades.length === 0 ? (
+ ) : allTrades.length === 0 ? (
Aucun trade clôturé dans les {days} derniers jours
+ ) : trades.length === 0 ? (
+
+ Aucun résultat pour ces filtres
+
) : (
@@ -294,6 +410,7 @@ function ClosedTradesSection({ days }: { days: number }) {
P&L réalisé
Motif
Note
+
@@ -302,13 +419,13 @@ function ClosedTradesSection({ days }: { days: number }) {
const daysHeld = t.entry_date && t.closed_at
? Math.round((new Date(t.closed_at).getTime() - new Date(t.entry_date).getTime()) / 86400000)
: null
+ const isConfirming = confirmDeleteId === t.id
return (
{t.pattern_name || t.pattern_id}
+ _isBearishStr(t.strategy ?? '') ? 'badge-red' : 'badge-green')}>
{t.strategy || '—'}
@@ -340,6 +457,30 @@ function ClosedTradesSection({ days }: { days: number }) {
{t.close_note || '—'}
+
+ {isConfirming ? (
+
+ { deleteTrade(t.id, { onSuccess: () => setConfirmDeleteId(null) }) }}
+ disabled={deleting}
+ className="text-[10px] font-semibold text-red-400 hover:text-red-300 bg-red-900/30 border border-red-700/40 rounded px-1.5 py-0.5 disabled:opacity-40"
+ >
+ {deleting ? '…' : 'Supprimer'}
+
+ setConfirmDeleteId(null)} className="text-slate-600 hover:text-slate-400">
+
+
+
+ ) : (
+ setConfirmDeleteId(t.id)}
+ className="p-1 rounded text-slate-700 hover:text-red-400 hover:bg-red-900/20 transition-colors"
+ title="Supprimer ce trade"
+ >
+
+
+ )}
+
)
})}
@@ -750,8 +891,20 @@ function TradeMtmSection({ days }: { days: number }) {
const [minScoreFilter, setMinScoreFilter] = useState(0)
const [selectedTradeId, setSelectedTradeId] = useState(null)
const [closingTrade, setClosingTrade] = useState(null)
- const allTrades: any[] = (data as any)?.trades ?? []
- const trades = allTrades.filter((t: any) => (t.latest_score ?? t.score_at_entry ?? 0) >= minScoreFilter)
+ const [search, setSearch] = useState('')
+ const [assetClass, setAssetClass] = useState('all')
+ const [direction, setDirection] = useState('all')
+
+ const allTrades: any[] = ((data as any)?.trades ?? []).filter((t: any) => t.status !== 'closed')
+ const trades = allTrades.filter((t: any) => {
+ if ((t.latest_score ?? t.score_at_entry ?? 0) < minScoreFilter) return false
+ const q = search.toLowerCase()
+ if (q && !`${t.underlying} ${t.strategy ?? ''} ${t.pattern_name ?? ''}`.toLowerCase().includes(q)) return false
+ if (assetClass !== 'all' && t.asset_class !== assetClass) return false
+ if (direction === 'bullish' && t.direction !== 'bullish') return false
+ if (direction === 'bearish' && t.direction !== 'bearish') return false
+ return true
+ })
const withPnl = trades.filter((t: any) => t.pnl_pct != null)
const avgPnl = withPnl.length
@@ -760,7 +913,7 @@ function TradeMtmSection({ days }: { days: number }) {
return (
-
+
{trades.length}/{allTrades.length} trades · {withPnl.length} pricés
{avgPnl != null && (
@@ -787,6 +940,12 @@ function TradeMtmSection({ days }: { days: number }) {
+
+
{closingTrade && (
setClosingTrade(null)} />
)}
@@ -797,8 +956,8 @@ function TradeMtmSection({ days }: { days: number }) {
{allTrades.length === 0
- ? 'Aucun trade logué — scorer des patterns pour commencer le suivi'
- : `Aucun trade avec score ≥ ${minScoreFilter}`}
+ ? 'Aucun trade ouvert — scorer des patterns pour commencer le suivi'
+ : 'Aucun résultat pour ces filtres'}
) : (
@@ -1451,9 +1610,13 @@ const TABS = [
function SkippedTradesSection({ days }: { days: number }) {
const { data, isLoading } = useSkippedTrades(days)
- const trades: any[] = (data as any)?.trades ?? []
+ const allTrades: any[] = (data as any)?.trades ?? []
- const ASSET_CLASS_COLORS: Record
= {
+ const [search, setSearch] = useState('')
+ const [assetClass, setAssetClass] = useState('all')
+ const [reasonFilter, setReasonFilter] = useState('all')
+
+ const ASSET_CLASS_BADGE: Record = {
energy: 'bg-orange-900/30 text-orange-300 border-orange-700/30',
metals: 'bg-yellow-900/30 text-yellow-300 border-yellow-700/30',
agriculture: 'bg-green-900/30 text-green-300 border-green-700/30',
@@ -1463,9 +1626,24 @@ function SkippedTradesSection({ days }: { days: number }) {
equities: 'bg-cyan-900/30 text-cyan-300 border-cyan-700/30',
}
+ const allReasons = Array.from(new Set(allTrades.map((t: any) => t.skip_reason).filter(Boolean)))
+
+ const trades = allTrades.filter((t: any) => {
+ const q = search.toLowerCase()
+ if (q && !`${t.underlying} ${t.strategy ?? ''} ${t.pattern_name ?? ''}`.toLowerCase().includes(q)) return false
+ if (assetClass !== 'all' && t.asset_class !== assetClass) return false
+ if (reasonFilter !== 'all' && t.skip_reason !== reasonFilter) return false
+ return true
+ })
+
+ const byReason = allTrades.reduce((acc: Record, t: any) => {
+ acc[t.skip_reason] = (acc[t.skip_reason] ?? 0) + 1
+ return acc
+ }, {})
+
if (isLoading) return
- if (trades.length === 0) {
+ if (allTrades.length === 0) {
return (
@@ -1474,21 +1652,18 @@ function SkippedTradesSection({ days }: { days: number }) {
)
}
- const byReason = trades.reduce((acc: Record
, t: any) => {
- acc[t.skip_reason] = (acc[t.skip_reason] ?? 0) + 1
- return acc
- }, {})
-
return (
- Trades suggérés non loggés — {trades.length} sur {days}j
+ Trades suggérés non loggés — {trades.length}/{allTrades.length} sur {days}j
(score ou gain insuffisant pour tout profil actif)
+
+ {/* Reason summary chips */}
{Object.entries(byReason).map(([reason, count]) => (
@@ -1496,47 +1671,77 @@ function SkippedTradesSection({ days }: { days: number }) {
))}
-
-
-
-
- Pattern
- Ticker
- Stratégie
- Score
- Gain attendu
- Classe
- Raison du skip
-
-
-
- {trades.map((t: any) => (
-
- {t.pattern_name || '—'}
- {t.underlying}
- {t.strategy || '—'}
- = 50 ? 'text-emerald-400' : t.score >= 25 ? 'text-yellow-400' : 'text-slate-600')}>
- {t.score ?? '—'}
-
-
- {t.expected_move_pct != null ? `${t.expected_move_pct.toFixed(0)}%` : '—'}
-
-
- {t.asset_class ? (
-
- {t.asset_class}
-
- ) : — }
-
-
- {t.skip_detail || t.skip_reason}
-
-
+
+ {/* Filters */}
+
+
+ {/* Reason filter */}
+ {allReasons.length > 1 && (
+
+ setReasonFilter('all')}
+ className={clsx('px-2 py-1 rounded text-xs transition-colors',
+ reasonFilter === 'all' ? 'bg-slate-600 text-white' : 'text-slate-400 hover:text-slate-200')}>
+ Tous
+
+ {allReasons.map(r => (
+ setReasonFilter(r)}
+ className={clsx('px-2 py-1 rounded text-xs transition-colors',
+ reasonFilter === r ? 'bg-slate-600 text-white' : 'text-slate-400 hover:text-slate-200')}>
+ {r}
+
))}
-
-
+
+ )}
+
+ {trades.length === 0 ? (
+
Aucun résultat pour ces filtres
+ ) : (
+
+
+
+
+ Pattern
+ Ticker
+ Stratégie
+ Score
+ Gain attendu
+ Classe
+ Raison du skip
+
+
+
+ {trades.map((t: any) => (
+
+ {t.pattern_name || '—'}
+ {t.underlying}
+ {t.strategy || '—'}
+ = 50 ? 'text-emerald-400' : t.score >= 25 ? 'text-yellow-400' : 'text-slate-600')}>
+ {t.score ?? '—'}
+
+
+ {t.expected_move_pct != null ? `${t.expected_move_pct.toFixed(0)}%` : '—'}
+
+
+ {t.asset_class ? (
+
+ {t.asset_class}
+
+ ) : — }
+
+
+ {t.skip_detail || t.skip_reason}
+
+
+ ))}
+
+
+
+ )}
)