feat: cockpit
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex gap-0.5 bg-dark-800 rounded p-0.5 text-[9px]">
|
||||
<button onClick={click('simulated')}
|
||||
className={clsx('px-1.5 py-0.5 rounded transition-colors',
|
||||
value === 'simulated' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
|
||||
Simulated
|
||||
</button>
|
||||
<button onClick={click('portfolio')}
|
||||
className={clsx('px-1.5 py-0.5 rounded transition-colors',
|
||||
value === 'portfolio' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
|
||||
Portfolio
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex gap-0.5 bg-dark-800 rounded p-0.5 text-[9px]">
|
||||
<button onClick={click('top')}
|
||||
className={clsx('px-1.5 py-0.5 rounded transition-colors',
|
||||
value === 'top' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
|
||||
Best
|
||||
</button>
|
||||
<button onClick={click('worst')}
|
||||
className={clsx('px-1.5 py-0.5 rounded transition-colors',
|
||||
value === 'worst' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
|
||||
Worst
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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<HTMLDivElement>(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) ── */}
|
||||
<div className="grid grid-cols-4 gap-3 items-start">
|
||||
|
||||
{/* 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}>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="section-title mb-0 text-[10px]">📊 P&L</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ViewToggle value={pnlView} onChange={setPnlViewPersist} />
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
|
||||
{/* ── Side-by-side: Unrealized | Realized ── */}
|
||||
@@ -804,118 +705,64 @@ export default function Dashboard() {
|
||||
{/* Left: Unrealized */}
|
||||
<div className="bg-dark-700/40 rounded px-2.5 py-2 border border-slate-700/30">
|
||||
<div className="text-[9px] text-slate-600 uppercase tracking-wide mb-1">Open</div>
|
||||
{pnlView === 'simulated' ? (
|
||||
<>
|
||||
<div className={clsx('text-xl font-bold font-mono leading-none',
|
||||
openPnlPct === null ? 'text-slate-600' : openPnlPct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{openPnlPct !== null ? `${openPnlPct >= 0 ? '+' : ''}${openPnlPct.toFixed(1)}%` : '—'}
|
||||
</div>
|
||||
<div className={clsx('text-xs font-mono mt-0.5',
|
||||
openProfit >= 0 ? 'text-emerald-500' : 'text-red-400')}>
|
||||
{openCapital > 0 ? `${openProfit >= 0 ? '+' : ''}${openProfit.toFixed(0)}€` : '—'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{openTrades.length} trades · <span className="text-emerald-500">{openWinners}✓</span>{' '}
|
||||
<span className="text-red-400">{openLosers}✗</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={clsx('text-xl font-bold font-mono leading-none',
|
||||
pfPnlPct === null ? 'text-slate-600' : pfPnlPct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{pfPnlPct !== null ? `${pfPnlPct >= 0 ? '+' : ''}${pfPnlPct.toFixed(1)}%` : '—'}
|
||||
</div>
|
||||
<div className={clsx('text-xs font-mono mt-0.5', (pfPnl ?? 0) >= 0 ? 'text-emerald-500' : 'text-red-400')}>
|
||||
{pfPnl != null ? `${pfPnl >= 0 ? '+' : ''}${pfPnl.toFixed(0)}€` : '—'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{pf?.open_positions ?? 0} positions
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{(targetHit > 0 || stopHit > 0) && (
|
||||
<div className="mt-1 text-[9px] space-x-1">
|
||||
{targetHit > 0 && <span className="text-emerald-400">🎯{targetHit}</span>}
|
||||
{stopHit > 0 && <span className="text-red-400">⛔{stopHit}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className={clsx('text-xl font-bold font-mono leading-none',
|
||||
pfPnlPct === null ? 'text-slate-600' : pfPnlPct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{pfPnlPct !== null ? `${pfPnlPct >= 0 ? '+' : ''}${pfPnlPct.toFixed(1)}%` : '—'}
|
||||
</div>
|
||||
<div className={clsx('text-xs font-mono mt-0.5', (pfPnl ?? 0) >= 0 ? 'text-emerald-500' : 'text-red-400')}>
|
||||
{pfPnl != null ? `${pfPnl >= 0 ? '+' : ''}${pfPnl.toFixed(0)}€` : '—'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{pf?.open_positions ?? 0} positions
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Realized */}
|
||||
<div className="bg-dark-700/40 rounded px-2.5 py-2 border border-slate-700/30">
|
||||
<div className="text-[9px] text-slate-600 uppercase tracking-wide mb-1">Realized</div>
|
||||
{pnlView === 'simulated' ? (
|
||||
<>
|
||||
<div className={clsx('text-xl font-bold font-mono leading-none',
|
||||
avgClosedPct === null ? 'text-slate-600'
|
||||
: avgClosedPct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{avgClosedPct !== null
|
||||
? `${avgClosedPct >= 0 ? '+' : ''}${avgClosedPct.toFixed(2)}%`
|
||||
: closedTrades.length === 0 ? '—' : '+0.00%'}
|
||||
</div>
|
||||
<div className={clsx('text-xs font-mono mt-0.5',
|
||||
closedProfitEur >= 0 ? 'text-emerald-500' : 'text-red-400')}>
|
||||
{closedCapital > 0
|
||||
? `${closedProfitEur >= 0 ? '+' : ''}${closedProfitEur.toFixed(0)}€`
|
||||
: closedTrades.length > 0 ? 'no capital' : ''}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{closedTrades.length} closed · <span className="text-emerald-500">{closedWinners}✓</span>{' '}
|
||||
<span className="text-red-400">{closedLosers}✗</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={clsx('text-xl font-bold font-mono leading-none',
|
||||
pfReal == null || pfReal === 0 ? 'text-slate-600'
|
||||
: pfReal > 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{pfReal != null && pfReal !== 0
|
||||
? `${pfReal >= 0 ? '+' : ''}${pfReal.toFixed(0)}€`
|
||||
: '—'}
|
||||
</div>
|
||||
{pfNet != null && (
|
||||
<div className={clsx('text-xs font-mono mt-0.5', pfNet >= 0 ? 'text-emerald-500/70' : 'text-red-400/70')}>
|
||||
net {pfNet >= 0 ? '+' : ''}{pfNet.toFixed(0)}€
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{pf?.closed_positions ?? 0} closed
|
||||
</div>
|
||||
</>
|
||||
<div className={clsx('text-xl font-bold font-mono leading-none',
|
||||
pfReal == null || pfReal === 0 ? 'text-slate-600'
|
||||
: pfReal > 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{pfReal != null && pfReal !== 0
|
||||
? `${pfReal >= 0 ? '+' : ''}${pfReal.toFixed(0)}€`
|
||||
: '—'}
|
||||
</div>
|
||||
{pfNet != null && (
|
||||
<div className={clsx('text-xs font-mono mt-0.5', pfNet >= 0 ? 'text-emerald-500/70' : 'text-red-400/70')}>
|
||||
net {pfNet >= 0 ? '+' : ''}{pfNet.toFixed(0)}€
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{pf?.closed_positions ?? 0} closed
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total line */}
|
||||
<div className="mt-1.5 flex items-center justify-between text-[10px] flex-shrink-0">
|
||||
<span className="text-slate-600">
|
||||
Invested{isEstimated ? ' ~' : ''} {pnlView === 'simulated'
|
||||
? (totalCapital > 0 ? `${totalCapital.toFixed(0)}€` : '—')
|
||||
: (pfInvest != null ? `${pfInvest.toFixed(0)}€` : '—')}
|
||||
Invested {pfInvest != null ? `${pfInvest.toFixed(0)}€` : '—'}
|
||||
</span>
|
||||
<span className={clsx('font-mono font-bold',
|
||||
(pnlView === 'simulated' ? totalProfit : (pfNet ?? 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)}€` : '—')}
|
||||
<span className={clsx('font-mono font-bold', (pfNet ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
Total {pfNet != null ? `${pfNet >= 0 ? '+' : ''}${pfNet.toFixed(0)}€` : '—'}
|
||||
</span>
|
||||
</div>
|
||||
{/* 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 (
|
||||
<div className="mt-1.5 pt-1.5 border-t border-slate-700/30">
|
||||
<div className="text-[9px] text-slate-600 mb-0.5">Historical PnL curve ({snaps.length} pts)</div>
|
||||
<div className="text-[9px] text-slate-600 mb-0.5">Realized PnL curve ({chartData.length} closed)</div>
|
||||
<ResponsiveContainer width="100%" height={58}>
|
||||
<AreaChart data={chartData} margin={{ top: 2, right: 0, left: -38, bottom: 0 }}>
|
||||
<defs>
|
||||
@@ -927,11 +774,11 @@ export default function Dashboard() {
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 8, fill: '#475569' }}
|
||||
interval={Math.max(0, Math.floor(chartData.length / 5) - 1)} />
|
||||
<YAxis domain={[minVal - 0.1, maxVal + 0.1]} tick={{ fontSize: 8, fill: '#475569' }} unit="%" />
|
||||
<YAxis domain={[minVal - 1, maxVal + 1]} tick={{ fontSize: 8, fill: '#475569' }} unit="€" />
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#0f172a', border: '1px solid #334155', borderRadius: 6, fontSize: 10, padding: '4px 8px' }}
|
||||
labelStyle={{ color: '#64748b' }}
|
||||
formatter={(v: any) => [`${v >= 0 ? '+' : ''}${Number(v).toFixed(3)}%`, 'PnL']}
|
||||
formatter={(v: any) => [`${v >= 0 ? '+' : ''}${Number(v).toFixed(0)}€`, 'Cumulative PnL']}
|
||||
/>
|
||||
<Area type="monotone" dataKey="pnl" stroke={strokeColor} strokeWidth={1.5}
|
||||
fill={`url(#${gradId})`} dot={false} />
|
||||
@@ -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<string, any> = 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<string, any> = 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() {
|
||||
</div>
|
||||
<div className={clsx('text-lg font-bold mt-0.5 flex items-center gap-2', alertCount > 0 ? 'text-red-400' : 'text-emerald-400')}>
|
||||
{alertCount > 0 ? `${alertCount} alert${alertCount > 1 ? 's' : ''}` : 'OK'}
|
||||
{conflictCount > 0 && <span className="text-[10px] text-red-400 font-normal">{conflictCount} conflict{conflictCount > 1 ? 's' : ''}</span>}
|
||||
</div>
|
||||
{radarAxes.length > 0 && (
|
||||
<ResponsiveContainer width="100%" height={110}>
|
||||
@@ -985,25 +832,19 @@ export default function Dashboard() {
|
||||
{pieData.length > 0 ? (
|
||||
<>
|
||||
<div className={clsx('space-y-0.5', radarAxes.length > 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 (
|
||||
<div key={d.name} className="flex items-center gap-1.5 text-[9px]">
|
||||
<span className="w-2 h-2 rounded-full shrink-0" style={{ background: ASSET_CLASS_COLORS[d.name] ?? ASSET_CLASS_COLORS.unknown }} />
|
||||
<span className="text-slate-500 capitalize flex-1 truncate">{d.name}</span>
|
||||
<span className={clsx('text-[8px] w-3', bias === 'bullish' ? 'text-emerald-500' : bias === 'bearish' ? 'text-red-400' : 'text-slate-700')}>
|
||||
{bias === 'bullish' ? '↑' : bias === 'bearish' ? '↓' : '—'}
|
||||
</span>
|
||||
<span className="text-slate-600 font-mono w-9 text-right">{d.value}%</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{pieData.map(d => (
|
||||
<div key={d.name} className="flex items-center gap-1.5 text-[9px]">
|
||||
<span className="w-2 h-2 rounded-full shrink-0" style={{ background: ASSET_CLASS_COLORS[d.name] ?? ASSET_CLASS_COLORS.unknown }} />
|
||||
<span className="text-slate-500 capitalize flex-1 truncate">{d.name}</span>
|
||||
<span className="text-slate-600 font-mono w-9 text-right">{d.value}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{(risk?.alerts ?? []).length > 0 && (
|
||||
{(risk?.concentration_alerts ?? []).length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t border-slate-700/30 space-y-1">
|
||||
{(risk.alerts as any[]).map((a: any, i: number) => (
|
||||
<div key={i} className={clsx('flex items-start gap-1 text-[9px] leading-snug', a.level === 'danger' ? 'text-red-400' : 'text-amber-400')}>
|
||||
<span className="shrink-0 mt-[3px]">{a.level === 'danger' ? '●' : '▲'}</span>
|
||||
{(risk.concentration_alerts as any[]).map((a: any, i: number) => (
|
||||
<div key={i} className={clsx('flex items-start gap-1 text-[9px] leading-snug', a.level === 'high' ? 'text-red-400' : 'text-amber-400')}>
|
||||
<span className="shrink-0 mt-[3px]">{a.level === 'high' ? '●' : '▲'}</span>
|
||||
<span className="line-clamp-2">{a.message}</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -1159,8 +1000,7 @@ export default function Dashboard() {
|
||||
{/* ── Command Center: Intelligence & Contexte ── */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
|
||||
<TradeRankList trades={mtmTrades} mode={tradesView} title="🏆 Trades Overview" linkTo="/journal"
|
||||
toggle={<TradesViewToggle value={tradesView} onChange={setTradesViewPersist} />} />
|
||||
<TradeRankList trades={openPositions} mode="recent" title="🏆 Trades Overview" linkTo="/portfolio" />
|
||||
|
||||
{/* Wavelets Signal — latest per-ticker signal from the watchlist scan (computed each
|
||||
cycle). Single click on the card = Wavelets Simulation overview; double-click a
|
||||
|
||||
Reference in New Issue
Block a user