From d4a016a535765b8b9557d2fc16ac0105b76791a3 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Sat, 20 Jun 2026 11:00:31 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20P&L=20c=C3=B4te=20=C3=A0=20c=C3=B4te=20?= =?UTF-8?q?(Ouvert|R=C3=A9alis=C3=A9)=20+=20filtres=20journal=20+=20suppre?= =?UTF-8?q?ssion=20trades=20ferm=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dashboard: P&L card séparé en deux colonnes (Ouvertes/Réalisées) pour Simulé et Portfolio - Dashboard: closed trades P&L locked from pnl_realized, ne fluctue plus après fermeture - Journal Ouvert: filtres ticker/stratégie + classe d'actif + direction (haussier/baissier) - Journal Fermés: mêmes filtres + filtre P&L (gagnants/perdants) + bouton supprimer par ligne - Journal Non loggés: filtres ticker + classe d'actif + raison de skip - Backend: DELETE /api/journal/trades/{id} + delete_trade() dans database.py Co-Authored-By: Claude Sonnet 4.6 --- backend/routers/journal.py | 56 +++-- backend/services/database.py | 8 + frontend/src/hooks/useApi.ts | 12 + frontend/src/pages/Dashboard.tsx | 183 +++++++++------ frontend/src/pages/JournalDeBord.tsx | 329 ++++++++++++++++++++++----- 5 files changed, 444 insertions(+), 144 deletions(-) diff --git a/backend/routers/journal.py b/backend/routers/journal.py index de6b06a..d4c3bb1 100644 --- a/backend/routers/journal.py +++ b/backend/routers/journal.py @@ -6,7 +6,7 @@ from services.database import ( get_macro_regime_history, get_geo_alert_history, get_trade_entry_prices, get_closed_trades, close_trade, update_trade_exit_params, get_trade_entry_by_id, get_config, set_config, reset_journal_history, - _fetch_live_prices, _trade_maturity, get_skipped_trades, + _fetch_live_prices, _trade_maturity, get_skipped_trades, delete_trade, ) import json @@ -61,11 +61,35 @@ def trade_mtm(days: int = 30): for e in entries: ticker = (e.get("underlying") or "").upper() entry_price = e.get("entry_price") - current_price = current_prices.get(ticker) - pnl_pct = None - if entry_price and current_price and entry_price > 0: - raw_pnl = (current_price - entry_price) / entry_price * 100 - pnl_pct = round(-raw_pnl if _is_bearish(e.get("strategy", "")) else raw_pnl, 2) + is_closed = e.get("status") == "closed" + + # For closed trades: pnl_pct is fixed from pnl_realized (locked in). + # For open trades: compute live from current market price. + if is_closed: + pnl_realized = e.get("pnl_realized") + capital = e.get("capital_invested") or entry_price or 0 + if pnl_realized is not None and capital and capital > 0: + pnl_pct = round(pnl_realized / capital * 100, 2) + elif e.get("close_price") and entry_price and entry_price > 0: + raw = (e["close_price"] - entry_price) / entry_price * 100 + pnl_pct = round(-raw if _is_bearish(e.get("strategy", "")) else raw, 2) + else: + pnl_pct = None + current_price = e.get("close_price") + price_warning = None if pnl_pct is not None else "no_close_price" + else: + current_price = current_prices.get(ticker) + pnl_pct = None + if entry_price and current_price and entry_price > 0: + raw_pnl = (current_price - entry_price) / entry_price * 100 + pnl_pct = round(-raw_pnl if _is_bearish(e.get("strategy", "")) else raw_pnl, 2) + price_warning = None + if entry_price is None and current_price is None: + price_warning = "no_price_data" + elif entry_price is None: + price_warning = "no_entry_price" + elif current_price is None: + price_warning = "no_live_price" days_held = None try: @@ -74,27 +98,19 @@ def trade_mtm(days: int = 30): pass horizon = e.get("horizon_days") or 90 - maturity = _trade_maturity(days_held or 0, horizon) + maturity = "closed" if is_closed else _trade_maturity(days_held or 0, horizon) defaults = _get_exit_defaults() target = e.get("target_pct") if e.get("target_pct") is not None else defaults.get("target_pct", 30.0) stop = e.get("stop_loss_pct") if e.get("stop_loss_pct") is not None else defaults.get("stop_loss_pct", -50.0) alert_type = None - if pnl_pct is not None: + if not is_closed and pnl_pct is not None: if pnl_pct >= target: alert_type = "target_reached" elif pnl_pct <= stop: alert_type = "stop_loss" - price_warning = None - if entry_price is None and current_price is None: - price_warning = "no_price_data" - elif entry_price is None: - price_warning = "no_entry_price" - elif current_price is None: - price_warning = "no_live_price" - result.append({ **e, "current_price": current_price, @@ -260,6 +276,14 @@ def skipped_trades_endpoint(days: int = 30): return _sanitize({"trades": trades, "days": days, "count": len(trades)}) +@router.delete("/trades/{trade_id}") +def delete_trade_endpoint(trade_id: int): + ok = delete_trade(trade_id) + if not ok: + raise HTTPException(404, "Trade non trouvé") + return {"deleted": True, "trade_id": trade_id} + + @router.delete("/reset") def reset_journal(): """Truncate all journal history (trades, macro, geo, cycles). Irreversible.""" diff --git a/backend/services/database.py b/backend/services/database.py index 39808ea..a6a32a3 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -1327,6 +1327,14 @@ def get_closed_trades(days: int = 180) -> List[Dict[str, Any]]: return [dict(r) for r in rows] +def delete_trade(trade_id: int) -> bool: + conn = get_conn() + cur = conn.execute("DELETE FROM trade_entry_prices WHERE id = ?", (trade_id,)) + conn.commit() + conn.close() + return cur.rowcount > 0 + + def get_skipped_trades(days: int = 30) -> List[Dict[str, Any]]: conn = get_conn() rows = conn.execute( diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index e18dd16..e5b24f9 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -438,6 +438,18 @@ export const useCloseTrade = () => { }) } +export const useDeleteTrade = () => { + const qc = useQueryClient() + return useMutation({ + mutationFn: (id: number) => api.delete(`/journal/trades/${id}`).then(r => r.data), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['journal-mtm'] }) + qc.invalidateQueries({ queryKey: ['journal-closed'] }) + qc.invalidateQueries({ queryKey: ['journal-summary'] }) + }, + }) +} + export const useUpdateExitParams = () => { const qc = useQueryClient() return useMutation({ diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 73fef23..b171488 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -347,97 +347,148 @@ export default function Dashboard() { {/* PnL */} {(() => { const trades: any[] = mtmTrades - const withPnl = trades.filter((t: any) => t.pnl_pct != null) - const avgPnl = withPnl.length - ? withPnl.reduce((s: number, t: any) => s + t.pnl_pct, 0) / withPnl.length - : null - const winners = withPnl.filter((t: any) => t.pnl_pct > 0).length - const losers = withPnl.filter((t: any) => t.pnl_pct < 0).length - const closedTrades = trades.filter((t: any) => t.status === 'closed') const openTrades = trades.filter((t: any) => t.status !== 'closed') - // Use capital_invested if set, otherwise entry_price as proxy cost - const totalCapital = trades.reduce((s: number, t: any) => - s + (t.capital_invested ?? t.entry_price ?? 0), 0) - const totalProfit = withPnl.reduce((s: number, t: any) => - s + ((t.capital_invested ?? t.entry_price ?? 0) * t.pnl_pct / 100), 0) + const closedTrades = trades.filter((t: any) => t.status === 'closed') + + // Unrealized — open positions + const openWithPnl = openTrades.filter((t: any) => t.pnl_pct != null) + const openCapital = openTrades.reduce((s: number, t: any) => s + (t.capital_invested ?? t.entry_price ?? 0), 0) + const openProfit = openWithPnl.reduce((s: number, t: any) => s + ((t.capital_invested ?? t.entry_price ?? 0) * t.pnl_pct / 100), 0) + const openPnlPct = openCapital > 0 ? openProfit / openCapital * 100 : null + const openWinners = openWithPnl.filter((t: any) => t.pnl_pct > 0).length + const openLosers = openWithPnl.filter((t: any) => t.pnl_pct < 0).length + const targetHit = openTrades.filter((t: any) => t.alert_type === 'target_reached').length + const stopHit = openTrades.filter((t: any) => t.alert_type === 'stop_loss').length + + // Realized — closed positions (pnl_pct locked from close_price/pnl_realized by backend) + const closedCapital = closedTrades.reduce((s: number, t: any) => s + (t.capital_invested ?? t.entry_price ?? 0), 0) + const closedProfit = closedTrades.reduce((s: number, t: any) => { + const cap = t.capital_invested ?? t.entry_price ?? 0 + return s + (t.pnl_pct != null ? cap * t.pnl_pct / 100 : 0) + }, 0) + const closedWinners = closedTrades.filter((t: any) => (t.pnl_pct ?? 0) > 0).length + const closedLosers = closedTrades.filter((t: any) => (t.pnl_pct ?? 0) < 0).length + const isEstimated = trades.some((t: any) => t.capital_invested == null && t.entry_price != null) - const targetHit = trades.filter((t: any) => t.alert_type === 'target_reached').length - const stopHit = trades.filter((t: any) => t.alert_type === 'stop_loss').length + const totalCapital = openCapital + closedCapital + const totalProfit = openProfit + closedProfit const pf = portfolio as any const pfPnlPct = pf?.unrealized_pnl_pct ?? null const pfPnl = pf?.unrealized_pnl ?? null const pfInvest = pf?.total_invested ?? null const pfReal = pf?.realized_pnl ?? null + const pfNet = pf?.net_pnl ?? null return ( -
+
📊 P&L
- {pnlView === 'simulated' ? ( - <> -
-
-
= 0 ? 'text-emerald-400' : 'text-red-400')}> - {avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'} + + {/* ── Side-by-side: Unrealized | Realized ── */} +
+ {/* Left: Unrealized */} +
+
Ouvertes
+ {pnlView === 'simulated' ? ( + <> +
= 0 ? 'text-emerald-400' : 'text-red-400')}> + {openPnlPct !== null ? `${openPnlPct >= 0 ? '+' : ''}${openPnlPct.toFixed(1)}%` : '—'} +
+
= 0 ? 'text-emerald-500' : 'text-red-400')}> + {openCapital > 0 ? `${openProfit >= 0 ? '+' : ''}${openProfit.toFixed(0)}€` : '—'}
- {trades.length} trades · {winners}✓{' '} - {losers}✗ + {openTrades.length} trades · {openWinners}✓{' '} + {openLosers}✗
-
-
- {targetHit > 0 &&
🎯 {targetHit} cible{targetHit > 1 ? 's' : ''}
} - {stopHit > 0 &&
⛔ {stopHit} stop
} -
{withPnl.length} pricés
-
-
-
-
-
- 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 && ( + + )} +
+ {/* Asset class */} +
+ {CATEGORIES.map(c => ( + + ))} +
+ {/* Direction */} + {onDirection && ( +
+ {DIRECTIONS.map(d => ( + + ))} +
+ )} + {/* P&L filter (Fermés only) */} + {onPnlFilter && ( +
+ {[ + { key: 'all', label: 'Tous' }, + { key: 'winners', label: '✓ Gagnants' }, + { key: 'losers', label: '✗ Perdants' }, + ].map(p => ( + + ))} +
+ )} +
+ ) +} + // ── 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
+ + {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 }) { + @@ -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 ( @@ -340,6 +457,30 @@ function ClosedTradesSection({ days }: { days: number }) { + ) })} @@ -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 }) {
))}
-
-
P&L réalisé Motif Note
{t.pattern_name || t.pattern_id} + _isBearishStr(t.strategy ?? '') ? 'badge-red' : 'badge-green')}> {t.strategy || '—'} {t.close_note || '—'} + {isConfirming ? ( +
+ + +
+ ) : ( + + )} +
- - - - - - - - - - - - - {trades.map((t: any) => ( - - - - - - - - - + + {/* Filters */} +
+ + {/* Reason filter */} + {allReasons.length > 1 && ( +
+ + {allReasons.map(r => ( + ))} -
-
PatternTickerStratégieScoreGain attenduClasseRaison du skip
{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} -
+
+ )}
+ + {trades.length === 0 ? ( +
Aucun résultat pour ces filtres
+ ) : ( +
+ + + + + + + + + + + + + + {trades.map((t: any) => ( + + + + + + + + + + ))} + +
PatternTickerStratégieScoreGain attenduClasseRaison du skip
{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} +
+
+ )}
)