diff --git a/backend/routers/risk.py b/backend/routers/risk.py
index e36139c..69b33a0 100644
--- a/backend/routers/risk.py
+++ b/backend/routers/risk.py
@@ -1,3 +1,4 @@
+import math
from fastapi import APIRouter, Query
from services.database import (
get_portfolio_exposure,
@@ -11,6 +12,17 @@ from services.database import (
router = APIRouter(prefix="/api/risk", tags=["risk"])
+def _sanitize(obj):
+ """Replace NaN/Inf with None recursively for JSON compliance."""
+ if isinstance(obj, dict):
+ return {k: _sanitize(v) for k, v in obj.items()}
+ if isinstance(obj, list):
+ return [_sanitize(v) for v in obj]
+ if isinstance(obj, float) and (math.isnan(obj) or math.isinf(obj)):
+ return None
+ return obj
+
+
@router.get("/exposure")
def portfolio_exposure():
"""Exposure by asset class + risk factor for open positions."""
@@ -49,3 +61,10 @@ def kelly_sizing(
def risk_dashboard():
"""Full portfolio risk snapshot: concentration, diversification, expected drawdown, recommendation."""
return get_risk_dashboard()
+
+
+@router.get("/radar")
+def risk_radar():
+ """5-axis risk radar (Concentration/Volatility/Correlation/Exposure/Drawdown) for the real portfolio."""
+ from services.portfolio_risk import compute_real_portfolio_risk_radar
+ return _sanitize(compute_real_portfolio_risk_radar())
diff --git a/backend/services/portfolio_risk.py b/backend/services/portfolio_risk.py
index 5e1695f..e00380b 100644
--- a/backend/services/portfolio_risk.py
+++ b/backend/services/portfolio_risk.py
@@ -232,29 +232,43 @@ def _compute_max_drawdown_pct(snapshots: List[Dict[str, Any]]) -> Optional[float
return round(max_dd, 2)
-def compute_portfolio_risk_radar() -> Dict[str, Any]:
- """5-axis risk radar for the Cockpit's Risk card (replaces the old asset-class donut,
- which now lives separately as the allocation breakdown). Axes, each scaled 0-100:
+def _compute_max_drawdown_eur(curve: List[Dict[str, Any]]) -> Optional[float]:
+ """Max peak-to-trough drop (in €) across an ascending cumulative-realized-PnL curve
+ (e.g. the closed-positions equity curve from services.database.get_positions('closed'))."""
+ if len(curve) < 2:
+ return None
+ peak = curve[0].get("cumulative", 0.0) or 0.0
+ max_dd = 0.0
+ for pt in curve:
+ v = pt.get("cumulative", 0.0) or 0.0
+ peak = max(peak, v)
+ max_dd = max(max_dd, peak - v)
+ return round(max_dd, 2)
+
+
+def _build_risk_radar_axes(trades: List[Dict[str, Any]], drawdown_pct: Optional[float]) -> Dict[str, Any]:
+ """Shared 5-axis computation (Concentration/Volatility/Correlation/Exposure/Drawdown),
+ each scaled 0-100, given a list of open trades (needs 'underlying' + a capital/price
+ field) and a pre-computed drawdown_pct. Used by both the simulated-portfolio and
+ real-portfolio radars — only the trade source and the drawdown source differ.
- Concentration: capital-weighted share of the single largest underlying.
- Volatility: capital-weighted average 20d realized vol of open positions.
- Correlation: average pairwise return correlation across open positions'
underlyings (only positive correlation counts as risk — negative correlation is
diversification, not danger).
- Exposure: open position count against a soft target of 10 concurrent trades —
- a proxy, NOT true margin leverage: trade_entry_prices has no notional/contract-size
- column to compute real leverage from, so this measures "how spread thin" instead.
- - Drawdown: max peak-to-trough drop in the simulated portfolio's total P&L %,
- from services.var_service's snapshot history.
+ a proxy, NOT true margin leverage (no notional/contract-size column to compute
+ real leverage from), so this measures "how spread thin" instead.
+ - Drawdown: max peak-to-trough drop, pre-computed by the caller from whichever
+ equity-curve source applies to that portfolio.
"""
from services.data_fetcher import get_quote_with_volatility
- from services.var_service import get_pnl_snapshots
- trades = get_open_simulation_trades()
open_count = len(trades)
if not trades:
return {"axes": [], "open_count": 0}
- weights = [max(t.get("capital_invested") or t.get("entry_price") or 0, 0) for t in trades]
+ weights = [max(t.get("capital_invested") or t.get("entry_price") or t.get("entry_underlying_price") or 0, 0) for t in trades]
total_w = sum(weights) or 1.0
by_underlying_w: Dict[str, float] = {}
@@ -286,8 +300,6 @@ def compute_portfolio_risk_radar() -> Dict[str, Any]:
exposure_score = min(100.0, open_count / 10 * 100)
- drawdown_pct = _compute_max_drawdown_pct(get_pnl_snapshots(200))
-
def _scale(v: Optional[float], cap: float) -> Optional[float]:
return round(min(100.0, max(0.0, v / cap * 100)), 1) if v is not None else None
@@ -301,6 +313,42 @@ def compute_portfolio_risk_radar() -> Dict[str, Any]:
return {"axes": axes, "open_count": open_count}
+def compute_portfolio_risk_radar() -> Dict[str, Any]:
+ """5-axis risk radar for the simulated trade log (trade_entry_prices). Drawdown comes
+ from services.var_service's periodic P&L snapshot history."""
+ from services.var_service import get_pnl_snapshots
+
+ trades = get_open_simulation_trades()
+ drawdown_pct = _compute_max_drawdown_pct(get_pnl_snapshots(200))
+ return _build_risk_radar_axes(trades, drawdown_pct)
+
+
+def compute_real_portfolio_risk_radar() -> Dict[str, Any]:
+ """5-axis risk radar for the REAL portfolio (services.database `portfolio` table).
+ Same axes/scaling as compute_portfolio_risk_radar(), but the Drawdown axis comes from
+ the realized equity curve of closed positions (the real portfolio has no periodic
+ unrealized-PnL snapshot yet), normalized to a % of currently invested capital.
+ """
+ from services.database import get_positions
+
+ trades = get_positions("open")
+
+ closed = sorted(get_positions("closed"), key=lambda p: p.get("close_date") or "")
+ cumulative = 0.0
+ curve: List[Dict[str, Any]] = []
+ for p in closed:
+ if p.get("close_value") is not None:
+ pnl = (p["close_value"] - p["capital_invested"]
+ - (p.get("ib_fees_entry") or 0) - (p.get("ib_fees_exit") or 0))
+ cumulative += pnl
+ curve.append({"cumulative": cumulative})
+ max_dd_eur = _compute_max_drawdown_eur(curve)
+ total_capital = sum(max(t.get("capital_invested") or 0, 0) for t in trades)
+ drawdown_pct = round(max_dd_eur / total_capital * 100, 2) if max_dd_eur is not None and total_capital > 0 else None
+
+ return _build_risk_radar_axes(trades, drawdown_pct)
+
+
def check_new_trade(underlying: str, strategy: str, asset_class: str) -> Dict[str, Any]:
"""Pre-entry check: would this new trade create conflicts or concentration issues?"""
open_trades = get_open_simulation_trades()
diff --git a/frontend/src/components/TradeRankList.tsx b/frontend/src/components/TradeRankList.tsx
index f461cae..089460a 100644
--- a/frontend/src/components/TradeRankList.tsx
+++ b/frontend/src/components/TradeRankList.tsx
@@ -5,15 +5,17 @@ import clsx from 'clsx'
export default function TradeRankList({ trades, mode, title, linkTo, toggle }: {
trades: any[]
- mode: 'top' | 'worst'
+ mode: 'top' | 'worst' | 'recent'
title: string
linkTo: string
toggle?: ReactNode
}) {
const sorted = [...trades]
- .sort((a, b) => mode === 'top'
- ? (b.pnl_pct ?? -999) - (a.pnl_pct ?? -999)
- : (a.pnl_pct ?? 999) - (b.pnl_pct ?? 999))
+ .sort((a, b) => mode === 'recent'
+ ? (b.entry_date ?? '').localeCompare(a.entry_date ?? '')
+ : mode === 'top'
+ ? (b.pnl_pct ?? -999) - (a.pnl_pct ?? -999)
+ : (a.pnl_pct ?? 999) - (b.pnl_pct ?? 999))
.slice(0, 5)
return (
diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts
index 3484229..c95ad4b 100644
--- a/frontend/src/hooks/useApi.ts
+++ b/frontend/src/hooks/useApi.ts
@@ -773,6 +773,15 @@ export const usePortfolioRiskRadar = () =>
staleTime: 5 * 60_000,
})
+// Same 5-axis shape as usePortfolioRiskRadar above, computed on the REAL portfolio
+// (services/database.py `portfolio` table) instead of the simulated trade log.
+export const useRiskRadar = () =>
+ useQuery({
+ queryKey: ['risk-radar'],
+ queryFn: () => api.get('/risk/radar').then(r => r.data),
+ staleTime: 5 * 60_000,
+ })
+
export const useTradeCheck = () =>
useMutation({
mutationFn: (body: { underlying: string; strategy: string; asset_class: string }) =>
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index 48f1fb1..8e5fe5c 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -2,9 +2,9 @@ import { useMemo, useState, useRef, useLayoutEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
import {
useGeoRiskScore, useAllQuotes,
- useEcoCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime,
- useTradeMtm, useRiskDashboard, useGeoNews,
- useSimPortfolioRisk, usePortfolioRiskRadar, useCycleStatus, useClosedTrades,
+ useEcoCalendar, usePortfolioSummary, usePortfolioPositions, usePnlHistory, useLastScores, useAllPatterns, useMacroRegime,
+ useRiskDashboard, useRiskRadar, useGeoNews,
+ useCycleStatus,
useInstrumentsWatchlist, useInstrumentsWatchlistQuotes, useWatchlistHistory, useInstrumentCatalogIds, useSaxoIvWatchlist, useLatestCycleReport,
useWaveletWatchlistSignals,
} from '../hooks/useApi'
@@ -17,7 +17,7 @@ import { fr } from 'date-fns/locale'
import {
RadarChart, PolarGrid, PolarAngleAxis, Radar, ResponsiveContainer,
AreaChart, Area, XAxis, YAxis, Tooltip, CartesianGrid,
- PieChart, Pie, Cell, BarChart, Bar, ReferenceLine,
+ Cell, BarChart, Bar, ReferenceLine,
} from 'recharts'
import { scoreColor } from '../components/TradeIdeas'
import { ASSET_CLASS_COLORS } from '../constants/assetColors'
@@ -67,45 +67,6 @@ const formatTimeShort = (isoStr: string) => {
return d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
}
-function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onChange: (v: 'simulated' | 'portfolio') => void }) {
- const click = (v: 'simulated' | 'portfolio') => (e: React.MouseEvent) => {
- e.preventDefault(); e.stopPropagation(); onChange(v)
- }
- return (
-
-
- Simulated
-
-
- Portfolio
-
-
- )
-}
-
-function TradesViewToggle({ value, onChange }: { value: 'top' | 'worst'; onChange: (v: 'top' | 'worst') => void }) {
- const click = (v: 'top' | 'worst') => (e: React.MouseEvent) => {
- e.preventDefault(); e.stopPropagation(); onChange(v)
- }
- return (
-
-
- Best
-
-
- Worst
-
-
- )
-}
function QuoteRow({ q }: { q: Quote }) {
if (!q.price) return null
@@ -153,11 +114,9 @@ export default function Dashboard() {
const { data: lastScoresData } = useLastScores()
const { data: allPatternsData } = useAllPatterns()
const { data: macroData } = useMacroRegime()
- const { data: tradeMtmData } = useTradeMtm(30)
- const { data: closedTradesData } = useClosedTrades(90)
+ const { data: openPositionsData } = usePortfolioPositions('open')
const { data: riskDashboard } = useRiskDashboard()
- const { data: simRisk } = useSimPortfolioRisk()
- const { data: riskRadarData } = usePortfolioRiskRadar()
+ const { data: riskRadarData } = useRiskRadar()
const { data: cycleStatusData } = useCycleStatus()
const { data: geoNews } = useGeoNews()
const { data: watchlistItems } = useInstrumentsWatchlist()
@@ -175,13 +134,8 @@ export default function Dashboard() {
const { data: waveletSignalsData } = useWaveletWatchlistSignals()
const { data: saxoIvWatchlistData } = useSaxoIvWatchlist()
- // Historical PnL curve + latest VaR snapshot
- const { data: pnlHistoryData } = useQuery({
- queryKey: ['dash-pnl-history'],
- queryFn: () => fetch('/api/var/pnl/snapshots?limit=200').then(r => r.ok ? r.json() : { snapshots: [] }),
- staleTime: 120_000,
- retry: 1,
- })
+ // Historical PnL curve (realized, from closed portfolio positions) + latest VaR snapshot
+ const { data: pnlHistoryData } = usePnlHistory()
const { data: latestVarData } = useQuery({
queryKey: ['dash-var-latest'],
queryFn: () => fetch('/api/var/latest').then(r => r.ok ? r.json() : { snapshot: null }),
@@ -189,27 +143,6 @@ export default function Dashboard() {
retry: 1,
})
- const [pnlView, setPnlView] = useState<'simulated' | 'portfolio'>(() =>
- (localStorage.getItem('dash_pnl_view') as any) ?? 'simulated'
- )
- const [riskView, setRiskView] = useState<'simulated' | 'portfolio'>(() =>
- (localStorage.getItem('dash_risk_view') as any) ?? 'simulated'
- )
-
- const setPnlViewPersist = (v: 'simulated' | 'portfolio') => {
- setPnlView(v); localStorage.setItem('dash_pnl_view', v)
- }
- const setRiskViewPersist = (v: 'simulated' | 'portfolio') => {
- setRiskView(v); localStorage.setItem('dash_risk_view', v)
- }
-
- const [tradesView, setTradesView] = useState<'top' | 'worst'>(() =>
- (localStorage.getItem('dash_trades_view') as any) ?? 'top'
- )
- const setTradesViewPersist = (v: 'top' | 'worst') => {
- setTradesView(v); localStorage.setItem('dash_trades_view', v)
- }
-
// Row height is dictated by the config-driven watchlist cards (Watchlist Radar for row 1,
// Options Lab for row 2) — the other cards in each row are capped to match, with internal scroll.
const watchlistCardRef = useRef(null)
@@ -262,7 +195,7 @@ export default function Dashboard() {
const gauge = riskScore ? riskGauge(riskScore.score) : null
const lastCycle = (cycleStatusData as any)?.last_cycle ?? null
- const mtmTrades: any[] = (tradeMtmData as any)?.trades ?? []
+ const openPositions: any[] = (openPositionsData as any) ?? []
// Patterns from the last cycle only (filter by created_at >= cycle started_at)
const cyclePatterns = useMemo(() => {
@@ -750,37 +683,8 @@ export default function Dashboard() {
{/* ── Command Center: Résumé Opérationnel ── height capped to Options Lab's natural size (config-driven) ── */}
- {/* PnL */}
+ {/* PnL — real portfolio (services/database.py `portfolio` table), mark-to-market via Black-Scholes */}
{(() => {
- // Unrealized — open trades only (get_trade_entry_prices excludes closed)
- const openTrades = mtmTrades
- 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 — from dedicated closed-trades endpoint (pnl_realized in %, capital for EUR)
- const closedTrades = (closedTradesData as any)?.trades ?? []
- const closedCapital = closedTrades.reduce((s: number, t: any) => s + (t.capital_invested ?? t.entry_price ?? 0), 0)
- const closedProfitEur = closedTrades.reduce((s: number, t: any) => {
- const cap = t.capital_invested ?? t.entry_price ?? 0
- return s + (cap * (t.pnl_realized ?? 0) / 100)
- }, 0)
- const closedWithPnl = closedTrades.filter((t: any) => t.pnl_realized != null)
- const avgClosedPct = closedWithPnl.length > 0
- ? closedWithPnl.reduce((s: number, t: any) => s + t.pnl_realized, 0) / closedWithPnl.length
- : null
- const closedWinners = closedTrades.filter((t: any) => (t.pnl_realized ?? 0) > 0).length
- const closedLosers = closedTrades.filter((t: any) => (t.pnl_realized ?? 0) < 0).length
-
- const isEstimated = openTrades.some((t: any) => t.capital_invested == null && t.entry_price != null)
- const totalCapital = openCapital + closedCapital
- const totalProfit = openProfit + closedProfitEur
-
const pf = portfolio as any
const pfPnlPct = pf?.unrealized_pnl_pct ?? null
const pfPnl = pf?.unrealized_pnl ?? null
@@ -793,10 +697,7 @@ export default function Dashboard() {
style={row2Height ? { height: row2Height } : undefined}>
{/* ── Side-by-side: Unrealized | Realized ── */}
@@ -804,118 +705,64 @@ export default function Dashboard() {
{/* Left: Unrealized */}
Open
- {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)}€` : '—'}
-
-
- {openTrades.length} trades · {openWinners}✓ {' '}
- {openLosers}✗
-
- >
- ) : (
- <>
-
= 0 ? 'text-emerald-400' : 'text-red-400')}>
- {pfPnlPct !== null ? `${pfPnlPct >= 0 ? '+' : ''}${pfPnlPct.toFixed(1)}%` : '—'}
-
-
= 0 ? 'text-emerald-500' : 'text-red-400')}>
- {pfPnl != null ? `${pfPnl >= 0 ? '+' : ''}${pfPnl.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)}%` : '—'}
+
+
= 0 ? 'text-emerald-500' : 'text-red-400')}>
+ {pfPnl != null ? `${pfPnl >= 0 ? '+' : ''}${pfPnl.toFixed(0)}€` : '—'}
+
+
+ {pf?.open_positions ?? 0} positions
+
{/* Right: Realized */}
Realized
- {pnlView === 'simulated' ? (
- <>
-
= 0 ? 'text-emerald-400' : 'text-red-400')}>
- {avgClosedPct !== null
- ? `${avgClosedPct >= 0 ? '+' : ''}${avgClosedPct.toFixed(2)}%`
- : closedTrades.length === 0 ? '—' : '+0.00%'}
-
-
= 0 ? 'text-emerald-500' : 'text-red-400')}>
- {closedCapital > 0
- ? `${closedProfitEur >= 0 ? '+' : ''}${closedProfitEur.toFixed(0)}€`
- : closedTrades.length > 0 ? 'no capital' : ''}
-
-
- {closedTrades.length} closed · {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} closed
-
- >
+
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} closed
+
{/* Total line */}
- Invested{isEstimated ? ' ~' : ''} {pnlView === 'simulated'
- ? (totalCapital > 0 ? `${totalCapital.toFixed(0)}€` : '—')
- : (pfInvest != null ? `${pfInvest.toFixed(0)}€` : '—')}
+ Invested {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)}€` : '—')}
+ = 0 ? 'text-emerald-400' : 'text-red-400')}>
+ Total {pfNet != null ? `${pfNet >= 0 ? '+' : ''}${pfNet.toFixed(0)}€` : '—'}
- {/* PnL historical sparkline */}
+ {/* Realized PnL curve — cumulative, from closed portfolio positions */}
{(() => {
- const snaps: any[] = [...(pnlHistoryData?.snapshots ?? [])].reverse()
- if (snaps.length < 2) return null
- const chartData = snaps.map(s => ({
- date: s.snapped_at?.slice(5, 10),
- pnl: s.total_pnl_pct ?? 0,
+ const curve: any[] = (pnlHistoryData as any) ?? []
+ if (curve.length < 2) return null
+ const chartData = curve.map((s: any) => ({
+ date: s.date?.slice(5, 10),
+ pnl: s.cumulative ?? 0,
}))
- const minVal = Math.min(...chartData.map(d => d.pnl))
- const maxVal = Math.max(...chartData.map(d => d.pnl))
+ const minVal = Math.min(...chartData.map((d: any) => d.pnl))
+ const maxVal = Math.max(...chartData.map((d: any) => d.pnl))
const isPositive = chartData[chartData.length - 1]?.pnl >= 0
const strokeColor = isPositive ? '#34d399' : '#f87171'
const gradId = 'pnlGrad'
return (
-
Historical PnL curve ({snaps.length} pts)
+
Realized PnL curve ({chartData.length} closed)
@@ -927,11 +774,11 @@ export default function Dashboard() {
-
+
[`${v >= 0 ? '+' : ''}${Number(v).toFixed(3)}%`, 'PnL']}
+ formatter={(v: any) => [`${v >= 0 ? '+' : ''}${Number(v).toFixed(0)}€`, 'Cumulative PnL']}
/>
@@ -944,16 +791,17 @@ export default function Dashboard() {
)
})()}
- {/* Risk — radar of 5 risk factors (Concentration/Volatility/Correlation/Exposure/
- Drawdown), asset-class allocation breakdown unchanged below it */}
+ {/* Risk — real portfolio. 5-axis radar (Concentration/Volatility/Correlation/
+ Exposure/Drawdown) from services/portfolio_risk.py `compute_real_portfolio_risk_radar()`
+ (`/api/risk/radar`), concentration alerts + asset-class breakdown from
+ `get_risk_dashboard()` (`/api/risk/dashboard`) — both built on the `portfolio` table. */}
{(() => {
- 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
- const concentration: Record = risk?.concentration ?? {}
- const pieData = Object.entries(concentration)
- .map(([cls, data]: [string, any]) => ({ name: cls, value: data.pct, bullish: data.bullish, bearish: data.bearish }))
+ const risk = riskDashboard as any
+ const alertCount: number = risk?.concentration_alerts?.length ?? 0
+ const openCount: number = risk?.open_trades ?? 0
+ const byClass: Record = risk?.exposure_by_class ?? {}
+ const pieData = Object.entries(byClass)
+ .map(([cls, data]: [string, any]) => ({ name: cls, value: data.pct_of_portfolio ?? 0 }))
.filter(d => d.value > 0)
.sort((a, b) => b.value - a.value)
const radarAxes = ((riskRadarData as any)?.axes ?? []).map((a: any) => ({ ...a, value: a.value ?? 0 }))
@@ -967,7 +815,6 @@ export default function Dashboard() {
0 ? 'text-red-400' : 'text-emerald-400')}>
{alertCount > 0 ? `${alertCount} alert${alertCount > 1 ? 's' : ''}` : 'OK'}
- {conflictCount > 0 && {conflictCount} conflict{conflictCount > 1 ? 's' : ''} }
{radarAxes.length > 0 && (
@@ -985,25 +832,19 @@ export default function Dashboard() {
{pieData.length > 0 ? (
<>
0 && 'mt-1 pt-1 border-t border-slate-700/30')}>
- {pieData.map(d => {
- const bias = d.bullish > d.bearish ? 'bullish' : d.bearish > d.bullish ? 'bearish' : 'neutral'
- return (
-
-
- {d.name}
-
- {bias === 'bullish' ? '↑' : bias === 'bearish' ? '↓' : '—'}
-
- {d.value}%
-
- )
- })}
+ {pieData.map(d => (
+
+
+ {d.name}
+ {d.value}%
+
+ ))}
- {(risk?.alerts ?? []).length > 0 && (
+ {(risk?.concentration_alerts ?? []).length > 0 && (
- {(risk.alerts as any[]).map((a: any, i: number) => (
-
-
{a.level === 'danger' ? '●' : '▲'}
+ {(risk.concentration_alerts as any[]).map((a: any, i: number) => (
+
+ {a.level === 'high' ? '●' : '▲'}
{a.message}
))}
@@ -1159,8 +1000,7 @@ export default function Dashboard() {
{/* ── Command Center: Intelligence & Contexte ── */}
- } />
+
{/* Wavelets Signal — latest per-ticker signal from the watchlist scan (computed each
cycle). Single click on the card = Wavelets Simulation overview; double-click a