diff --git a/backend/routers/journal.py b/backend/routers/journal.py index 8e187fa..08c429c 100644 --- a/backend/routers/journal.py +++ b/backend/routers/journal.py @@ -1,7 +1,14 @@ -from fastapi import APIRouter -from typing import Any, Dict, List +from fastapi import APIRouter, HTTPException +from typing import Any, Dict, List, Optional import math -from services.database import get_macro_regime_history, get_geo_alert_history, get_trade_entry_prices, reset_journal_history, _fetch_live_prices, _trade_maturity +from pydantic import BaseModel +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, +) +import json def _sanitize(obj: Any) -> Any: @@ -69,6 +76,17 @@ def trade_mtm(days: int = 30): horizon = e.get("horizon_days") or 90 maturity = _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 pnl_pct >= target: + alert_type = "target_reached" + elif pnl_pct <= stop: + alert_type = "stop_loss" + result.append({ **e, "current_price": current_price, @@ -76,11 +94,121 @@ def trade_mtm(days: int = 30): "days_held": days_held, "direction": "bearish" if _is_bearish(e.get("strategy", "")) else "bullish", "maturity": maturity, + "alert_type": alert_type, + "target_pct": target, + "stop_loss_pct": stop, }) return _sanitize({"trades": result, "days": days, "tickers_fetched": len(current_prices)}) +def _get_exit_defaults() -> Dict[str, Any]: + raw = get_config("exit_defaults") + if raw: + try: + return json.loads(raw) + except Exception: + pass + return {"target_pct": 30.0, "stop_loss_pct": -50.0, + "signal_reversal_mode": "badge_only", "signal_reversal_threshold": 25} + + +@router.get("/exit-defaults") +def exit_defaults(): + return _get_exit_defaults() + + +class ExitDefaultsRequest(BaseModel): + target_pct: Optional[float] = None + stop_loss_pct: Optional[float] = None + signal_reversal_mode: Optional[str] = None + signal_reversal_threshold: Optional[float] = None + + +@router.put("/exit-defaults") +def save_exit_defaults(body: ExitDefaultsRequest): + current = _get_exit_defaults() + if body.target_pct is not None: + current["target_pct"] = body.target_pct + if body.stop_loss_pct is not None: + current["stop_loss_pct"] = body.stop_loss_pct + if body.signal_reversal_mode is not None: + current["signal_reversal_mode"] = body.signal_reversal_mode + if body.signal_reversal_threshold is not None: + current["signal_reversal_threshold"] = body.signal_reversal_threshold + set_config("exit_defaults", json.dumps(current)) + return current + + +@router.get("/closed-trades") +def closed_trades(days: int = 180): + trades = get_closed_trades(days) + if not trades: + return _sanitize({"trades": [], "days": days, "stats": {}}) + pnls = [t["pnl_realized"] for t in trades if t.get("pnl_realized") is not None] + wins = [p for p in pnls if p >= 0] + losses = [p for p in pnls if p < 0] + stats = { + "total": len(trades), + "with_pnl": len(pnls), + "win_rate": round(len(wins) / len(pnls) * 100, 1) if pnls else None, + "avg_pnl": round(sum(pnls) / len(pnls), 2) if pnls else None, + "total_pnl": round(sum(pnls), 2) if pnls else None, + "avg_win": round(sum(wins) / len(wins), 2) if wins else None, + "avg_loss": round(sum(losses) / len(losses), 2) if losses else None, + "best": max(pnls, default=None), + "worst": min(pnls, default=None), + } + return _sanitize({"trades": trades, "days": days, "stats": stats}) + + +class CloseTradeRequest(BaseModel): + close_price: float + pnl_realized: Optional[float] = None + close_reason: str = "manual" + close_note: str = "" + + +class ExitParamsRequest(BaseModel): + target_pct: Optional[float] = None + stop_loss_pct: Optional[float] = None + signal_threshold: Optional[float] = None + + +@router.patch("/trades/{trade_id}/exit-params") +def set_exit_params(trade_id: int, body: ExitParamsRequest): + ok = update_trade_exit_params( + trade_id, + target_pct=body.target_pct, + stop_loss_pct=body.stop_loss_pct, + signal_threshold=body.signal_threshold, + ) + if not ok: + raise HTTPException(404, "Trade non trouvé") + return {"updated": True} + + +@router.patch("/trades/{trade_id}/close") +def close_trade_endpoint(trade_id: int, body: CloseTradeRequest): + trade = get_trade_entry_by_id(trade_id) + if not trade: + raise HTTPException(404, "Trade non trouvé") + if trade.get("status") == "closed": + raise HTTPException(409, "Trade déjà clôturé") + + pnl = body.pnl_realized + if pnl is None and trade.get("entry_price") and body.close_price > 0: + raw = (body.close_price - trade["entry_price"]) / trade["entry_price"] * 100 + is_bearish = any(kw in (trade.get("strategy") or "").lower() + for kw in _BEARISH_KEYWORDS) + pnl = round(-raw if is_bearish else raw, 2) + + ok = close_trade(trade_id, body.close_price, pnl, body.close_reason, body.close_note) + if not ok: + raise HTTPException(409, "Impossible de clôturer ce trade") + return {"closed": True, "trade_id": trade_id, "pnl_realized": pnl} + + @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 98a546b..21e732f 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -209,6 +209,15 @@ def init_db(): ("capital_invested", "REAL"), ("strike_guidance", "TEXT"), ("expiry_days_at_entry", "INTEGER"), + ("status", "TEXT DEFAULT 'open'"), + ("closed_at", "TEXT"), + ("close_reason", "TEXT"), + ("close_note", "TEXT"), + ("pnl_realized", "REAL"), + ("close_price", "REAL"), + ("target_pct", "REAL"), + ("stop_loss_pct", "REAL"), + ("signal_threshold", "REAL"), ]: try: c.execute(f"ALTER TABLE trade_entry_prices ADD COLUMN {col} {definition}") @@ -216,6 +225,7 @@ def init_db(): pass try: c.execute("CREATE INDEX IF NOT EXISTS idx_tep_date ON trade_entry_prices(entry_date DESC)") + c.execute("CREATE INDEX IF NOT EXISTS idx_tep_status ON trade_entry_prices(status, closed_at DESC)") except Exception: pass @@ -273,6 +283,12 @@ def init_db(): "auto_cycle_similarity_threshold": "0.30", "min_ev_threshold": "0.0", "min_score_threshold": "0", + "exit_defaults": json.dumps({ + "target_pct": 30.0, + "stop_loss_pct": -50.0, + "signal_reversal_mode": "badge_only", + "signal_reversal_threshold": 25, + }), } for k, v in defaults.items(): c.execute("INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)", (k, v)) @@ -1160,7 +1176,8 @@ def get_trade_entry_prices(days: int = 30) -> List[Dict[str, Any]]: conn = get_conn() rows = conn.execute( """SELECT * FROM trade_entry_prices - WHERE entry_date >= date('now', ?) + WHERE (status IS NULL OR status = 'open') + AND entry_date >= date('now', ?) ORDER BY entry_date DESC, score_at_entry DESC""", (f"-{days} days",) ).fetchall() @@ -1168,6 +1185,57 @@ def get_trade_entry_prices(days: int = 30) -> List[Dict[str, Any]]: return [dict(r) for r in rows] +def get_closed_trades(days: int = 180) -> List[Dict[str, Any]]: + conn = get_conn() + rows = conn.execute( + """SELECT * FROM trade_entry_prices + WHERE status = 'closed' + AND closed_at >= date('now', ?) + ORDER BY closed_at DESC""", + (f"-{days} days",) + ).fetchall() + conn.close() + return [dict(r) for r in rows] + + +def close_trade(trade_id: int, close_price: float, pnl_realized: float, + close_reason: str, close_note: str = "") -> bool: + conn = get_conn() + cur = conn.execute( + """UPDATE trade_entry_prices + SET status='closed', closed_at=datetime('now'), close_price=?, + pnl_realized=?, close_reason=?, close_note=? + WHERE id=? AND (status IS NULL OR status='open')""", + (close_price, pnl_realized, close_reason, close_note, trade_id) + ) + conn.commit() + conn.close() + return cur.rowcount > 0 + + +def update_trade_exit_params(trade_id: int, target_pct: float = None, + stop_loss_pct: float = None, + signal_threshold: float = None) -> bool: + updates: List[str] = [] + vals: List[Any] = [] + if target_pct is not None: + updates.append("target_pct=?"); vals.append(target_pct) + if stop_loss_pct is not None: + updates.append("stop_loss_pct=?"); vals.append(stop_loss_pct) + if signal_threshold is not None: + updates.append("signal_threshold=?"); vals.append(signal_threshold) + if not updates: + return False + conn = get_conn() + cur = conn.execute( + f"UPDATE trade_entry_prices SET {', '.join(updates)} WHERE id=?", + vals + [trade_id] + ) + conn.commit() + conn.close() + return cur.rowcount > 0 + + def get_trade_entry_by_id(trade_id: int) -> Optional[Dict[str, Any]]: conn = get_conn() row = conn.execute("SELECT * FROM trade_entry_prices WHERE id=?", (trade_id,)).fetchone() diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index 946e09f..7fbdf5b 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -404,10 +404,60 @@ export const useTradeMtm = (days = 30) => queryKey: ['journal-mtm', days], queryFn: () => api.get(`/journal/trade-mtm?days=${days}`).then(r => r.data), staleTime: 0, - refetchInterval: 5 * 60_000, // re-fetch live prices every 5 minutes + refetchInterval: 5 * 60_000, refetchIntervalInBackground: false, }) +export const useClosedTrades = (days = 180) => + useQuery({ + queryKey: ['journal-closed', days], + queryFn: () => api.get(`/journal/closed-trades?days=${days}`).then(r => r.data), + staleTime: 30_000, + }) + +export const useExitDefaults = () => + useQuery({ + queryKey: ['journal-exit-defaults'], + queryFn: () => api.get('/journal/exit-defaults').then(r => r.data), + staleTime: 60_000, + }) + +export const useCloseTrade = () => { + const qc = useQueryClient() + return useMutation({ + mutationFn: ({ id, body }: { id: number; body: { + close_price: number; pnl_realized?: number; + close_reason: string; close_note: string + }}) => api.patch(`/journal/trades/${id}/close`, body).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({ + mutationFn: ({ id, body }: { id: number; body: { + target_pct?: number; stop_loss_pct?: number; signal_threshold?: number + }}) => api.patch(`/journal/trades/${id}/exit-params`, body).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: ['journal-mtm'] }), + }) +} + +export const useSaveExitDefaults = () => { + const qc = useQueryClient() + return useMutation({ + mutationFn: (body: { + target_pct?: number; stop_loss_pct?: number; + signal_reversal_mode?: string; signal_reversal_threshold?: number + }) => api.put('/journal/exit-defaults', body).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: ['journal-exit-defaults'] }), + }) +} + // ── Risk Profiles ───────────────────────────────────────────────────────────── export const useRiskProfiles = () => diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index 9b7d832..80a6333 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -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 | 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() { + {/* ── Fermeture de trades (Exit Defaults) ── */} +
+

+ Paramètres de sortie des trades +

+

+ 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). +

+ +
+
+ + setExitTarget(parseFloat(e.target.value))} + className="w-full accent-emerald-500" /> +
+ +5%+150% +
+
+
+ + setExitStop(parseFloat(e.target.value))} + className="w-full accent-red-500" /> +
+ -90%-5% +
+
+
+ +
+
+ +
+ {[ + { value: 'badge_only', label: '🔔 Badge uniquement' }, + { value: 'auto', label: '⚡ Auto (futur)' }, + ].map(opt => ( + + ))} +
+
+ Badge only : alerte visible, vous confirmez manuellement. Auto : fermeture proposée par le système. +
+
+
+ + setExitSignalThreshold(parseFloat(e.target.value))} + className="w-full accent-blue-500" /> +
+ 10pts (sensible)60pts (tolérant) +
+
+
+ +
+ + {savedMsg && {savedMsg}} +
+
+ {/* ── Auto-Cycle ── */}
diff --git a/frontend/src/pages/JournalDeBord.tsx b/frontend/src/pages/JournalDeBord.tsx index 14c9380..a7e082a 100644 --- a/frontend/src/pages/JournalDeBord.tsx +++ b/frontend/src/pages/JournalDeBord.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, Fragment } from 'react' -import { BookOpen, TrendingUp, TrendingDown, Activity, AlertTriangle, RefreshCw, Zap, CheckCircle, XCircle, Brain, Trash2, Search, X, ChevronDown, ChevronUp, Terminal } from 'lucide-react' +import { BookOpen, TrendingUp, TrendingDown, Activity, AlertTriangle, RefreshCw, Zap, CheckCircle, XCircle, Brain, Trash2, Search, X, ChevronDown, ChevronUp, Terminal, Lock } from 'lucide-react' import clsx from 'clsx' -import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, api } from '../hooks/useApi' +import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useClosedTrades, useCloseTrade, useUpdateExitParams, useExitDefaults, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, api } from '../hooks/useApi' import { useQueryClient } from '@tanstack/react-query' import { format } from 'date-fns' import { fr } from 'date-fns/locale' @@ -101,6 +101,255 @@ function KellyCell({ patternId, capital = 10000 }: { patternId: string; capital? ) } +// ── Close Trade Modal ───────────────────────────────────────────────────────── + +const CLOSE_REASONS = [ + { value: 'target', label: '🎯 Objectif atteint' }, + { value: 'stop_loss', label: '🛑 Stop-loss déclenché' }, + { value: 'signal_reversal', label: '🔄 Signal IA retourné' }, + { value: 'manual', label: '✍️ Fermeture manuelle' }, +] + +function CloseTradeModal({ trade, onClose }: { trade: any; onClose: () => void }) { + const { mutate, isPending, isError } = useCloseTrade() + const [closePrice, setClosePrice] = useState(String(trade.current_price ?? trade.entry_price ?? '')) + const [reason, setReason] = useState(trade.alert_type === 'target_reached' ? 'target' : + trade.alert_type === 'stop_loss' ? 'stop_loss' : 'manual') + const [note, setNote] = useState('') + + const ep = parseFloat(closePrice) || 0 + const isBearish = trade.direction === 'bearish' + const pnlPreview = trade.entry_price && ep > 0 + ? (() => { + const raw = (ep - trade.entry_price) / trade.entry_price * 100 + return round2(isBearish ? -raw : raw) + })() + : null + + function round2(n: number) { return Math.round(n * 100) / 100 } + + const handleSubmit = () => { + mutate({ + id: trade.id, + body: { close_price: ep, close_reason: reason, close_note: note } + }, { onSuccess: onClose }) + } + + return ( +
+
+
+
+
+ Fermer le trade +
+
+ {trade.underlying} · {trade.strategy} · entrée {trade.entry_price?.toFixed(2) ?? '—'} +
+
+ +
+ + {trade.alert_type && ( +
+ {trade.alert_type === 'target_reached' ? '🎯 Objectif atteint !' : '🛑 Stop-loss atteint'} + {' — P&L actuel '} + {trade.pnl_pct >= 0 ? '+' : ''}{trade.pnl_pct?.toFixed(2)}% +
+ )} + +
+
+ + setClosePrice(e.target.value)} + className="w-full bg-dark-700 border border-slate-600/50 rounded px-3 py-1.5 text-sm text-white font-mono" + placeholder="Prix actuel du sous-jacent" + /> + {pnlPreview !== null && ( +
= 0 ? 'text-emerald-400' : 'text-red-400')}> + P&L réalisé : {pnlPreview >= 0 ? '+' : ''}{pnlPreview}% +
+ )} +
+ +
+ + +
+ +
+ + setNote(e.target.value)} + className="w-full bg-dark-700 border border-slate-600/50 rounded px-3 py-1.5 text-sm text-white" + placeholder="Contexte, raison…" + maxLength={200} + /> +
+
+ + {isError &&
Erreur lors de la fermeture du trade.
} + +
+ + +
+
+
+ ) +} + +// ── Closed Trades Section ───────────────────────────────────────────────────── + +function ClosedTradesSection({ days }: { days: number }) { + const { data, isLoading, refetch, isFetching } = useClosedTrades(days) + const trades: any[] = (data as any)?.trades ?? [] + const stats: any = (data as any)?.stats ?? {} + + const REASON_META: Record = { + target: { label: '🎯 Objectif', color: 'text-emerald-400' }, + stop_loss: { label: '🛑 Stop-loss', color: 'text-red-400' }, + signal_reversal: { label: '🔄 Signal IA', color: 'text-blue-400' }, + manual: { label: '✍️ Manuel', color: 'text-slate-400' }, + } + + return ( +
+ {/* Stats banner */} + {trades.length > 0 && ( +
+ {[ + { label: 'Trades fermés', value: stats.total ?? 0, sub: '', color: 'text-white' }, + { label: 'Taux de succès', value: stats.win_rate != null ? `${stats.win_rate}%` : '—', sub: '', color: stats.win_rate >= 50 ? 'text-emerald-400' : 'text-red-400' }, + { label: 'P&L moyen', value: stats.avg_pnl != null ? `${stats.avg_pnl >= 0 ? '+' : ''}${stats.avg_pnl?.toFixed(1)}%` : '—', sub: '', color: (stats.avg_pnl ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400' }, + { label: 'P&L total', value: stats.total_pnl != null ? `${stats.total_pnl >= 0 ? '+' : ''}${stats.total_pnl?.toFixed(1)}%` : '—', sub: `meilleur ${stats.best?.toFixed(1) ?? '—'}%`, color: (stats.total_pnl ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400' }, + ].map(s => ( +
+
{s.value}
+
{s.label}
+ {s.sub &&
{s.sub}
} +
+ ))} +
+ )} + +
+
+ {trades.length} trades clôturés · {days} derniers jours +
+ +
+ + {isLoading ? ( +
+ ) : trades.length === 0 ? ( +
+ + Aucun trade clôturé dans les {days} derniers jours +
+ ) : ( +
+ + + + + + + + + + + + + + + + + + {trades.map((t: any) => { + const meta = REASON_META[t.close_reason] ?? REASON_META.manual + const daysHeld = t.entry_date && t.closed_at + ? Math.round((new Date(t.closed_at).getTime() - new Date(t.entry_date).getTime()) / 86400000) + : null + return ( + + + + + + + + + + + + + + ) + })} + +
PatternStratégieTickerEntréeSortieDate entréeDate sortieDuréeP&L réaliséMotifNote
{t.pattern_name || t.pattern_id} + + {t.strategy || '—'} + + {t.underlying} + {t.entry_price != null ? t.entry_price.toFixed(2) : '—'} + + {t.close_price != null ? t.close_price.toFixed(2) : '—'} + {t.entry_date ?? '—'} + {t.closed_at ? t.closed_at.slice(0, 10) : '—'} + + {daysHeld != null ? `${daysHeld}j` : '—'} + + {t.pnl_realized != null ? ( + = 0 ? 'text-emerald-400' : 'text-red-400')}> + {t.pnl_realized >= 0 ? '+' : ''}{t.pnl_realized.toFixed(2)}% + + ) : } + + {meta.label} + + {t.close_note || '—'} +
+
+ )} +
+ ) +} + // ── IBKR Ticket helpers ─────────────────────────────────────────────────────── function computeStrikeDollars(price: number, guidance: string, strategy: string): number { @@ -499,6 +748,7 @@ function TradeMtmSection({ days }: { days: number }) { const { data, isLoading, refetch, isFetching } = useTradeMtm(days) 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) @@ -536,6 +786,10 @@ function TradeMtmSection({ days }: { days: number }) {
+ {closingTrade && ( + setClosingTrade(null)} /> + )} + {isLoading ? (
) : trades.length === 0 ? ( @@ -563,10 +817,12 @@ function TradeMtmSection({ days }: { days: number }) { Date Prix entrée Prix actuel + Cible/Stop Maturité IV Rank Kelly - P&L th. + P&L + @@ -633,6 +889,41 @@ function TradeMtmSection({ days }: { days: number }) { {t.current_price != null ? t.current_price.toFixed(2) : '—'} + + {t.pnl_pct != null ? ( +
+
+ {t.alert_type === 'target_reached' && ( + 🎯 + )} + {t.alert_type === 'stop_loss' && ( + 🛑 + )} +
+ {/* progress bar: stop_loss (negative) to target (positive) */} +
+
+
= 0 ? 'bg-emerald-500' : 'bg-red-500')} + style={{ + width: `${Math.min( + 100, + t.pnl_pct >= 0 + ? (t.pnl_pct / (t.target_pct || 30)) * 100 + : (Math.abs(t.pnl_pct) / Math.abs(t.stop_loss_pct || 50)) * 100 + )}%` + }} + /> +
+
+ {t.stop_loss_pct ?? -50}% + +{t.target_pct ?? 30}% +
+
+
+ ) : } + {t.maturity ? (
@@ -688,10 +979,24 @@ function TradeMtmSection({ days }: { days: number }) { {isExpanded ? : } + e.stopPropagation()}> + + {isExpanded && ( - + ('cycles') + const [tab, setTab] = useState<'cycles' | 'macro' | 'mtm' | 'closed' | 'geo'>('cycles') const [days, setDays] = useState(15) const [confirmReset, setConfirmReset] = useState(false) const [resetting, setResetting] = useState(false) @@ -1068,6 +1374,7 @@ export default function JournalDeBord() { {tab === 'cycles' && } {tab === 'macro' && } {tab === 'mtm' && } + {tab === 'closed' && } {tab === 'geo' && }
)