diff --git a/backend/services/portfolio_risk.py b/backend/services/portfolio_risk.py
index 06f9526..97ab74a 100644
--- a/backend/services/portfolio_risk.py
+++ b/backend/services/portfolio_risk.py
@@ -5,6 +5,55 @@ from services.database import get_conn
_BEARISH_KEYWORDS = {"bear", "put", "short", "sell", "vente", "baissier"}
+# Ticker → asset_class mapping for common instruments (fallback when pattern not in DB)
+_TICKER_AC: Dict[str, str] = {
+ # Energy
+ "CL=F": "energy", "BZ=F": "energy", "NG=F": "energy", "RB=F": "energy",
+ "HO=F": "energy", "USO": "energy", "XLE": "energy", "XOP": "energy", "OIL": "energy",
+ # Metals
+ "GC=F": "metals", "SI=F": "metals", "HG=F": "metals", "PL=F": "metals",
+ "PA=F": "metals", "GLD": "metals", "SLV": "metals", "GDX": "metals", "GDXJ": "metals",
+ # Agriculture
+ "ZW=F": "agriculture", "ZC=F": "agriculture", "ZS=F": "agriculture",
+ "CT=F": "agriculture", "KC=F": "agriculture", "SB=F": "agriculture", "CC=F": "agriculture",
+ "WEAT": "agriculture", "CORN": "agriculture", "SOYB": "agriculture",
+ # Equity indices
+ "^GSPC": "indices", "^DJI": "indices", "^NDX": "indices", "^RUT": "indices",
+ "^VIX": "indices", "^FTSE": "indices", "^GDAXI": "indices", "^FCHI": "indices",
+ "^N225": "indices", "^HSI": "indices", "^NSEI": "indices", "^BSESN": "indices",
+ "^STOXX50E": "indices", "^IBEX": "indices",
+ "SPY": "indices", "QQQ": "indices", "IWM": "indices", "DIA": "indices",
+ "VXX": "indices", "UVXY": "indices", "SVXY": "indices",
+ # Forex
+ "EURUSD=X": "forex", "GBPUSD=X": "forex", "USDJPY=X": "forex",
+ "AUDUSD=X": "forex", "USDCAD=X": "forex", "USDCHF=X": "forex",
+ "NZDUSD=X": "forex", "EURGBP=X": "forex", "EURJPY=X": "forex",
+ "GBPJPY=X": "forex", "USDCNH=X": "forex",
+ "FXE": "forex", "UUP": "forex", "FXB": "forex", "FXY": "forex",
+ # Rates
+ "ZB=F": "rates", "ZN=F": "rates", "ZF=F": "rates", "ZT=F": "rates",
+ "TLT": "rates", "IEF": "rates", "SHY": "rates", "HYG": "rates",
+ "LQD": "rates", "EMB": "rates",
+}
+
+
+def _infer_asset_class(ticker: str) -> str:
+ """Derive asset_class from ticker when not stored in DB."""
+ t = (ticker or "").upper().strip()
+ if t in _TICKER_AC:
+ return _TICKER_AC[t]
+ if ":" in t: # NSE:RELIANCE, BSE:TCS, etc.
+ return "equities"
+ if t.endswith("=X") and len(t) >= 7: # forex pairs like EURUSD=X
+ return "forex"
+ if t.endswith("=F"): # generic futures
+ return "energy" # most unknown futures are commodities
+ if t.startswith("^"): # index
+ return "indices"
+ if t.isalpha() and len(t) <= 5: # short alpha = equity
+ return "equities"
+ return "unknown"
+
def _direction(strategy: str) -> str:
s = (strategy or "").lower()
@@ -12,7 +61,7 @@ def _direction(strategy: str) -> str:
def get_open_simulation_trades() -> List[Dict[str, Any]]:
- """Open trades enriched with asset_class from joined pattern table."""
+ """Open trades enriched with asset_class — via stored column, JOIN fallback, then ticker inference."""
conn = get_conn()
rows = conn.execute("""
SELECT tep.*,
@@ -23,7 +72,13 @@ def get_open_simulation_trades() -> List[Dict[str, Any]]:
ORDER BY tep.entry_date DESC
""").fetchall()
conn.close()
- return [dict(r) for r in rows]
+ result = []
+ for r in rows:
+ d = dict(r)
+ if not d.get("asset_class"):
+ d["asset_class"] = _infer_asset_class(d.get("underlying", ""))
+ result.append(d)
+ return result
def analyze_simulation_portfolio() -> Dict[str, Any]:
diff --git a/frontend/src/pages/JournalDeBord.tsx b/frontend/src/pages/JournalDeBord.tsx
index 0cc065d..0fc5d05 100644
--- a/frontend/src/pages/JournalDeBord.tsx
+++ b/frontend/src/pages/JournalDeBord.tsx
@@ -1441,14 +1441,13 @@ function SimPortfolioRiskPanel() {
const TABS = [
{ key: 'cycles', label: 'Cycles IA', icon: Zap },
{ key: 'macro', label: 'Régimes Macro', icon: Activity },
- { key: 'risk', label: 'Risque Sim.', icon: ShieldAlert },
{ key: 'mtm', label: 'Ouverts', icon: TrendingUp },
{ key: 'closed', label: 'Fermés', icon: Lock },
{ key: 'geo', label: 'Alertes Géo', icon: AlertTriangle },
] as const
export default function JournalDeBord() {
- const [tab, setTab] = useState<'cycles' | 'macro' | 'risk' | 'mtm' | 'closed' | 'geo'>('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)
@@ -1572,6 +1571,17 @@ export default function JournalDeBord() {
{s.trade_entries_logged ?? 0}
trades logués
+ {riskAlertCount > 0 && (
+
+
+
+
+ {riskAlertCount} conflit{riskAlertCount > 1 ? 's' : ''} directionnel{riskAlertCount > 1 ? 's' : ''} détecté{riskAlertCount > 1 ? 's' : ''}
+
+ — voir Risk Dashboard → Simulé
+
+
+ )}
)}
@@ -1585,11 +1595,6 @@ export default function JournalDeBord() {
})}>
{label}
- {key === 'risk' && riskAlertCount > 0 && (
-
- {riskAlertCount}
-
- )}
))}
@@ -1597,7 +1602,6 @@ export default function JournalDeBord() {
{/* Content */}
{tab === 'cycles' && }
{tab === 'macro' && }
- {tab === 'risk' && }
{tab === 'mtm' && }
{tab === 'closed' && }
{tab === 'geo' && }
diff --git a/frontend/src/pages/RiskDashboard.tsx b/frontend/src/pages/RiskDashboard.tsx
index 50a9435..8a0a583 100644
--- a/frontend/src/pages/RiskDashboard.tsx
+++ b/frontend/src/pages/RiskDashboard.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react'
-import { useRiskDashboard, usePatternCorrelations, usePnlTimeline, useRiskExposure } from '../hooks/useApi'
+import { useRiskDashboard, usePatternCorrelations, usePnlTimeline, useRiskExposure, useSimPortfolioRisk } from '../hooks/useApi'
import clsx from 'clsx'
-import { ShieldAlert, TrendingUp, GitBranch, AlertTriangle, CheckCircle, Activity } from 'lucide-react'
+import { ShieldAlert, TrendingUp, GitBranch, AlertTriangle, CheckCircle, Activity, Brain, RefreshCw, PieChart } from 'lucide-react'
// ── Gauge component ──────────────────────────────────────────────────────────
function ConcentrationGauge({ label, pct, threshold = 50 }: { label: string; pct: number; threshold?: number }) {
@@ -131,11 +131,210 @@ function RecommendationCard({ rec }: { rec: any }) {
}
// ── Main page ────────────────────────────────────────────────────────────────
+const ASSET_CLASS_COLORS: Record = {
+ energy: '#f97316', metals: '#eab308', agriculture: '#84cc16',
+ equities: '#22c55e', indices: '#3b82f6', forex: '#8b5cf6', rates: '#06b6d4',
+ unknown: '#64748b',
+}
+
+function SimRiskPanel() {
+ const { data, isLoading, refetch } = useSimPortfolioRisk()
+ const risk = data as any
+
+ if (isLoading) return (
+
+ {[1, 2, 3].map(i =>
)}
+
+ )
+
+ if (!risk || risk.open_count === 0) return (
+
+
+
Aucune position ouverte dans le portefeuille simulé
+
Les trades logués apparaîtront ici après le prochain cycle IA
+
+ )
+
+ const concentration: Record = risk.concentration ?? {}
+ const alerts: any[] = risk.alerts ?? []
+ const conflicts: any[] = risk.conflicts ?? []
+ const aiMonitor: any = risk.ai_monitor
+ const aiTs: string = risk.ai_monitor_ts
+ const dangers = alerts.filter((a: any) => a.level === 'danger')
+ const warnings = alerts.filter((a: any) => a.level === 'warning')
+
+ return (
+
+ {/* KPIs */}
+
+
+
{risk.open_count}
+
Positions simulées
+
+
+
0 ? 'text-red-400' : 'text-emerald-400')}>
+ {dangers.length}
+
+
Conflits directionnels
+
+
+
0 ? 'text-amber-400' : 'text-emerald-400')}>
+ {warnings.length}
+
+
Alertes concentration
+
+
+
+ {Object.keys(concentration).filter(ac => ac !== 'unknown').length}
+
+
Classes d'actifs
+
+
+
+ {/* Concentration + Conflicts side by side */}
+
+ {/* Asset class bars */}
+
+
+
+ Répartition par classe d'actif
+
+
+
+
+ {Object.entries(concentration)
+ .sort(([, a]: any, [, b]: any) => b.pct - a.pct)
+ .map(([ac, data]: [string, any]) => {
+ const color = ASSET_CLASS_COLORS[ac] ?? ASSET_CLASS_COLORS.unknown
+ const exp = risk.direction_exposure?.[ac] ?? {}
+ const isOver = data.pct >= 35
+ return (
+
+
+
+ {ac}
+
+ {data.tickers?.slice(0, 3).join(', ')}{data.tickers?.length > 3 ? '…' : ''}
+
+
+
+ {exp.bullish > 0 && ▲{exp.bullish}}
+ {exp.bearish > 0 && ▼{exp.bearish}}
+
+ {data.pct}%
+
+ ({data.count})
+
+
+
+
+ )
+ })}
+
+
+
+ {/* Conflicts & alerts */}
+
+ {conflicts.length === 0 && warnings.length === 0 ? (
+
+
+
Portefeuille équilibré — aucun conflit ni sur-concentration détectés
+
+ ) : null}
+
+ {conflicts.map((c: any, i: number) => (
+
+
+ Conflit directionnel — {c.underlying}
+
+
+ {c.trades.map((t: any) => (
+
+
+ {t.direction === 'bullish' ? '▲' : '▼'}#{t.id}
+
+ {t.strategy}
+ {t.entry_date}
+ {t.pattern_name}
+
+ ))}
+
+
+ ))}
+
+ {warnings.length > 0 && (
+
+
+
Alertes de concentration
+
+
+ {warnings.map((a: any, i: number) => (
+
+ •{a.message}
+
+ ))}
+
+
+ )}
+
+
+
+ {/* AI recommendations */}
+ {aiMonitor && (
+
+
+
+ Recommandations IA — Moniteur de portefeuille simulé
+
+ {aiTs &&
{aiTs.slice(0, 16).replace('T', ' ')}}
+
+ {aiMonitor.assessment &&
{aiMonitor.assessment}
}
+ {aiMonitor.actions?.length > 0 && (
+
+ {aiMonitor.actions.map((action: any, i: number) => (
+
+
+ {action.type === 'close_trade' ? '🔒' : action.type === 'rebalance' ? '⚖️' : '👁'}
+
+
+ {action.underlying && {action.underlying}}
+ {action.trade_id && #{action.trade_id}}
+ {action.reason}
+
+
+ ))}
+
+ )}
+ {aiMonitor.rebalance_suggestion && (
+
+ ⚖️ {aiMonitor.rebalance_suggestion}
+
+ )}
+
+ )}
+
+ )
+}
+
export default function RiskDashboard() {
const { data: dashboard, isLoading } = useRiskDashboard()
const { data: corrData } = usePatternCorrelations()
const [tlDays, setTlDays] = useState(90)
const { data: tlData } = usePnlTimeline(tlDays)
+ const [mode, setMode] = useState<'real' | 'sim'>('real')
+ const { data: simRisk } = useSimPortfolioRisk()
+ const simConflicts = (simRisk as any)?.alerts?.filter((a: any) => a.level === 'danger').length ?? 0
const d: any = dashboard ?? {}
const pairs: any[] = corrData?.pairs ?? []
@@ -149,12 +348,36 @@ export default function RiskDashboard() {
Risk Dashboard
- Concentration · Clusters · Corrélations · Position Sizing
+ {mode === 'real'
+ ? 'Concentration · Clusters · Corrélations · Position Sizing — Portefeuille IBKR'
+ : 'Conflits directionnels · Concentration · Recommandations IA — Trades loggés'}
+ {/* Mode toggle */}
+
+
+
+
- {isLoading ? (
+ {mode === 'sim' ? : isLoading ? (
{[1, 2, 3, 4].map(i =>
)}