From 8f15dff72119910cd70b1b2f6b529c09b4177253 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Fri, 19 Jun 2026 18:26:10 +0200 Subject: [PATCH] fix: sim portfolio asset_class fallback + consolidate risk into Risk Dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - portfolio_risk.py: add _infer_asset_class() with ticker→asset_class map covering energy/metals/agri/indices/forex/rates futures, ETFs, forex pairs, exchange prefixes (NSE:). Fallback applied when JOIN finds no match (orphaned pattern_id after re-seed). Fixes "unknown 100%" shown in screenshot. - RiskDashboard.tsx: add Portefeuille Réel / Simulé toggle at top. New SimRiskPanel component with KPI row + concentration bars + conflict cards + AI recommendations — all visible inline in Risk Dashboard. Red badge on Simulé tab when danger alerts exist. - JournalDeBord.tsx: remove standalone Risque Sim. tab (moved to Risk Dashboard). Replace with a red banner in summary cards when conflicts are detected, pointing user to Risk Dashboard → Simulé. Co-Authored-By: Claude Sonnet 4.6 --- backend/services/portfolio_risk.py | 59 ++++++- frontend/src/pages/JournalDeBord.tsx | 20 ++- frontend/src/pages/RiskDashboard.tsx | 231 ++++++++++++++++++++++++++- 3 files changed, 296 insertions(+), 14 deletions(-) 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 =>
)}