diff --git a/backend/routers/journal.py b/backend/routers/journal.py
index 0380de7..de6b06a 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,
+ _fetch_live_prices, _trade_maturity, get_skipped_trades,
)
import json
@@ -253,6 +253,13 @@ def trade_check(body: TradeCheckRequest):
return check_new_trade(body.underlying, body.strategy, body.asset_class)
+@router.get("/skipped-trades")
+def skipped_trades_endpoint(days: int = 30):
+ """Trades suggested by cycle that didn't pass any risk profile threshold."""
+ trades = get_skipped_trades(days)
+ return _sanitize({"trades": trades, "days": days, "count": len(trades)})
+
+
@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 bd85043..c74b2a9 100644
--- a/backend/services/database.py
+++ b/backend/services/database.py
@@ -379,6 +379,25 @@ def init_db():
details TEXT
)""")
+ c.execute("""CREATE TABLE IF NOT EXISTS skipped_trades (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ run_id TEXT,
+ pattern_id TEXT,
+ pattern_name TEXT,
+ underlying TEXT,
+ strategy TEXT,
+ score INTEGER DEFAULT 0,
+ expected_move_pct REAL,
+ skip_reason TEXT DEFAULT 'no_profile',
+ skip_detail TEXT,
+ asset_class TEXT,
+ created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%S', 'now'))
+ )""")
+ try:
+ c.execute("CREATE INDEX IF NOT EXISTS idx_skipped_date ON skipped_trades(created_at DESC)")
+ except Exception:
+ pass
+
try:
c.execute("CREATE INDEX IF NOT EXISTS idx_kb_category ON knowledge_base(category, status)")
c.execute("CREATE INDEX IF NOT EXISTS idx_rs_version ON reasoning_state(version DESC)")
@@ -1014,6 +1033,17 @@ def log_trade_entries(run_id: str, scored_patterns: List[Dict[str, Any]], quotes
if matched is None:
skipped_no_profile += 1
_log.debug(f"[TradeLog] SKIP {underlying} score={eff_score} gain={exp_move:.0f}% — no profile match")
+ _trade_ac = trade.get("asset_class") or sp.get("asset_class") or _orig.get("asset_class") or ""
+ try:
+ log_skipped_trade(
+ run_id=run_id, pattern_id=pid, pattern_name=pattern_name,
+ underlying=underlying, strategy=strategy, score=eff_score,
+ expected_move_pct=exp_move,
+ skip_detail=f"profiles checked: {len(profiles)}, best: score>={eff_score} gain>={exp_move:.0f}%",
+ asset_class=_trade_ac,
+ )
+ except Exception:
+ pass
continue
ev_gross, ev_net, trade_score = _compute_trade_score(eff_score, exp_move)
@@ -1205,6 +1235,35 @@ def get_closed_trades(days: int = 180) -> List[Dict[str, Any]]:
return [dict(r) for r in rows]
+def get_skipped_trades(days: int = 30) -> List[Dict[str, Any]]:
+ conn = get_conn()
+ rows = conn.execute(
+ """SELECT * FROM skipped_trades
+ WHERE created_at >= date('now', ?)
+ ORDER BY created_at DESC, score DESC""",
+ (f"-{days} days",)
+ ).fetchall()
+ conn.close()
+ return [dict(r) for r in rows]
+
+
+def log_skipped_trade(run_id: str, pattern_id: str, pattern_name: str,
+ underlying: str, strategy: str, score: int,
+ expected_move_pct: float, skip_reason: str = "no_profile",
+ skip_detail: str = "", asset_class: str = "") -> None:
+ conn = get_conn()
+ conn.execute(
+ """INSERT INTO skipped_trades
+ (run_id, pattern_id, pattern_name, underlying, strategy, score,
+ expected_move_pct, skip_reason, skip_detail, asset_class)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
+ (run_id, pattern_id, pattern_name, underlying, strategy, score,
+ expected_move_pct, skip_reason, skip_detail, asset_class)
+ )
+ conn.commit()
+ conn.close()
+
+
def close_trade(trade_id: int, close_price: float, pnl_realized: float,
close_reason: str, close_note: str = "") -> bool:
conn = get_conn()
diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts
index 0520957..e0e8921 100644
--- a/frontend/src/hooks/useApi.ts
+++ b/frontend/src/hooks/useApi.ts
@@ -472,6 +472,13 @@ export const useTradeCheck = () =>
api.post('/journal/trade-check', body).then(r => r.data),
})
+export const useSkippedTrades = (days = 30) =>
+ useQuery({
+ queryKey: ['journal-skipped', days],
+ queryFn: () => api.get(`/journal/skipped-trades?days=${days}`).then(r => r.data),
+ staleTime: 60_000,
+ })
+
// ── Risk Profiles ─────────────────────────────────────────────────────────────
export const useRiskProfiles = () =>
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index 5893819..23b6172 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -4,8 +4,10 @@ import {
useCalendar, useAiStatus, usePortfolioSummary, useAddPosition,
useScorePatterns, useLastScores, useAllPatterns, useMacroRegime,
usePortfolioPositions, useTradeMtm, useRiskProfiles, useRiskDashboard,
+ useSimPortfolioRisk, useCycleStatus, useKnowledgeState,
} from '../hooks/useApi'
-import { Target, Clock, Brain, Globe, Plus, RefreshCw, ChevronDown, ChevronUp, CheckCircle2, ShieldAlert, LayoutGrid, List, Terminal } from 'lucide-react'
+import { Target, Clock, Brain, Globe, Plus, RefreshCw, ChevronDown, ChevronUp, CheckCircle2, ShieldAlert, LayoutGrid, List, Terminal, ArrowUpRight } from 'lucide-react'
+import { Link } from 'react-router-dom'
import clsx from 'clsx'
import type { Quote } from '../types'
import { format } from 'date-fns'
@@ -780,6 +782,9 @@ export default function Dashboard() {
const { data: tradeMtmData } = useTradeMtm(30)
const { data: riskProfilesData } = useRiskProfiles()
const { data: riskDashboard } = useRiskDashboard()
+ const { data: simRisk } = useSimPortfolioRisk()
+ const { data: cycleStatusData } = useCycleStatus()
+ const { data: knowledgeState } = useKnowledgeState()
const { mutate: scorePatterns, isPending: scoring } = useScorePatterns()
const { mutate: addPos } = useAddPosition()
@@ -1101,6 +1106,212 @@ export default function Dashboard() {
+ {/* ── Command Center: Résumé Opérationnel ── */}
+
+
+ {/* PnL Simulé */}
+ {(() => {
+ const trades: any[] = (tradeMtmData as any)?.trades ?? []
+ 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
+ return (
+
+
+
= 0 ? 'text-emerald-400' : 'text-red-400')}>
+ {avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'}
+
+
+ {trades.length} trades · {winners}✓{' '}
+ {losers}✗
+
+
+ )
+ })()}
+
+ {/* Risque Simulé */}
+ {(() => {
+ const risk = simRisk as any
+ const alertCount: number = risk?.alerts?.length ?? 0
+ const conflictCount: number = risk?.conflicts?.length ?? 0
+ const openCount: number = risk?.open_count ?? 0
+ return (
+
+
+
0 ? 'text-red-400' : 'text-emerald-400')}>
+ {alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
+
+
+ {openCount} positions
+ {conflictCount > 0 && · {conflictCount} conflit{conflictCount > 1 ? 's' : ''}}
+
+
+ )
+ })()}
+
+ {/* Dernier Cycle */}
+ {(() => {
+ const last = (cycleStatusData as any)?.last_cycle
+ const ts = last?.ts
+ ? new Date(last.ts.endsWith('Z') ? last.ts : last.ts + 'Z')
+ : null
+ const elapsed = ts ? Math.round((Date.now() - ts.getTime()) / 60_000) : null
+ const elapsedStr = elapsed === null ? '—'
+ : elapsed < 60 ? `${elapsed}min`
+ : elapsed < 1440 ? `${Math.round(elapsed / 60)}h`
+ : `${Math.round(elapsed / 1440)}j`
+ return (
+
+
+
{elapsedStr}
+
+ {last
+ ? `${last.patterns_added ?? 0} loggés · géo ${last.geo_score ?? '—'}`
+ : 'Aucun cycle enregistré'}
+
+
+ )
+ })()}
+
+ {/* Régime Macro */}
+ {(() => {
+ const dom = macroInfo?.dominant
+ const colorClass = dom === 'growth' ? 'text-emerald-400'
+ : dom === 'stagflation' || dom === 'recession' ? 'text-red-400'
+ : dom === 'deflation' ? 'text-blue-300'
+ : 'text-slate-400'
+ return (
+
+
+
+ {macroInfo ? `${macroInfo.emoji} ${macroInfo.label}` : '—'}
+
+
+ {macroInfo
+ ? Object.entries(macroInfo.assetBias).slice(0, 2).map(([k, v]) => `${k}→${v}`).join(' · ')
+ : 'Chargement...'}
+
+
+ )
+ })()}
+
+
+ {/* ── Command Center: Intelligence & Contexte ── */}
+
+
+ {/* Super Contexte IA */}
+ {(() => {
+ const state = (knowledgeState as any)?.state
+ const synthesis = state?.synthesis
+ const insights = (synthesis?.regime_insights?.length ?? 0)
+ + (synthesis?.pattern_insights?.length ?? 0)
+ + (synthesis?.recurring_mistakes?.length ?? 0)
+ const priority = synthesis?.strategic_priorities?.[0]
+ const excerpt = typeof priority === 'string' ? priority
+ : typeof state?.narrative === 'string' ? state.narrative.slice(0, 90)
+ : null
+ return (
+
+
+
+ {state ? `${insights} insights actifs` : 'Aucune synthèse'}
+
+
+ {excerpt ?? 'Lancer une synthèse dans Super Contexte'}
+
+
+ )
+ })()}
+
+ {/* Signaux Géo */}
+ {(() => {
+ const topRisks: Array<[string, number]> = riskScore?.top_risks ?? []
+ return (
+
+
+
+ {riskScore?.score ?? '—'}
+
+
+ {topRisks.slice(0, 2).map(([cat, val]) => (
+
+ {cat.replace(/_/g, ' ')}
+ {Math.round(val * 100)}%
+
+ ))}
+
+
+ )
+ })()}
+
+ {/* Meilleur Pattern scoré */}
+ {(() => {
+ const best = [...allPatterns]
+ .map(p => ({ p, sp: scoreMap[p.id] }))
+ .filter(x => x.sp?.score != null)
+ .sort((a, b) => (b.sp.score ?? 0) - (a.sp.score ?? 0))[0]
+ const score = best?.sp?.score ?? null
+ const name = best?.p?.name ?? null
+ const ticker = best?.sp?.recommended_trade?.underlying ?? null
+ return (
+
+
+
+ {score ?? '—'}
+
+
+ {name ?? 'Aucun scoré'}{ticker ? ` · ${ticker}` : ''}
+
+
+ )
+ })()}
+
+ {/* Patterns Actifs */}
+ {(() => {
+ const total = allPatterns.length
+ const scored = allPatterns.filter(p => scoreMap[p.id]).length
+ const unscored = total - scored
+ return (
+
+
+
{total}
+
+ {scored} scorés
+ {unscored > 0 && · {unscored} à scorer}
+
+
+ )
+ })()}
+
+
{/* ── Trade ideas scorées par IA ── */}
{/* Toolbar */}
diff --git a/frontend/src/pages/JournalDeBord.tsx b/frontend/src/pages/JournalDeBord.tsx
index 0fc5d05..91ff6f9 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, Lock, ShieldAlert, PieChart } from 'lucide-react'
import clsx from 'clsx'
-import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useClosedTrades, useCloseTrade, useUpdateExitParams, useExitDefaults, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, useSimPortfolioRisk, api } from '../hooks/useApi'
+import { useJournalSummary, useMacroHistory, useGeoHistory, useTradeMtm, useClosedTrades, useCloseTrade, useUpdateExitParams, useExitDefaults, useCycleHistory, useCycleStatus, useTriggerCycle, useTradePostmortem, useAnalyzePostmortem, useIvForTrade, useKellySizing, useSimPortfolioRisk, useSkippedTrades, api } from '../hooks/useApi'
import { useQueryClient } from '@tanstack/react-query'
import { format } from 'date-fns'
import { fr } from 'date-fns/locale'
@@ -1444,10 +1444,104 @@ const TABS = [
{ key: 'mtm', label: 'Ouverts', icon: TrendingUp },
{ key: 'closed', label: 'Fermés', icon: Lock },
{ key: 'geo', label: 'Alertes Géo', icon: AlertTriangle },
+ { key: 'skipped', label: 'Non loggés', icon: XCircle },
] as const
+function SkippedTradesSection({ days }: { days: number }) {
+ const { data, isLoading } = useSkippedTrades(days)
+ const trades: any[] = (data as any)?.trades ?? []
+
+ const ASSET_CLASS_COLORS: 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',
+ indices: 'bg-blue-900/30 text-blue-300 border-blue-700/30',
+ forex: 'bg-purple-900/30 text-purple-300 border-purple-700/30',
+ rates: 'bg-slate-800/60 text-slate-300 border-slate-600/30',
+ equities: 'bg-cyan-900/30 text-cyan-300 border-cyan-700/30',
+ }
+
+ if (isLoading) return
+
+ if (trades.length === 0) {
+ return (
+
+
+ Aucune suggestion non loggée sur {days}j — tous les trades scorés passent au moins un profil de risque.
+
+ )
+ }
+
+ 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
+
+ (score ou gain insuffisant pour tout profil actif)
+
+
+
+ {Object.entries(byReason).map(([reason, count]) => (
+
+ {reason}: {count}
+
+ ))}
+
+
+
+
+
+ | 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}
+ |
+
+ ))}
+
+
+
+
+
+ )
+}
+
export default function JournalDeBord() {
- const [tab, setTab] = useState<'cycles' | 'macro' | 'mtm' | 'closed' | 'geo'>('cycles')
+ const [tab, setTab] = useState<'cycles' | 'macro' | 'mtm' | 'closed' | 'geo' | 'skipped'>('cycles')
const [days, setDays] = useState(15)
const [confirmReset, setConfirmReset] = useState(false)
const [resetting, setResetting] = useState(false)
@@ -1605,6 +1699,7 @@ export default function JournalDeBord() {
{tab === 'mtm' && }
{tab === 'closed' && }
{tab === 'geo' && }
+ {tab === 'skipped' && }
)
}