← back to Bertha
kalshi-dash/src/pages/Risk.jsx
756 lines
import React, { useState, useEffect, useMemo } from 'react';
import { api, post } from '../api';
import {
BarChart, Bar, AreaChart, Area,
XAxis, YAxis, Tooltip, ResponsiveContainer,
Cell, PieChart, Pie,
} from 'recharts';
// ── Strategy color palette ──
const STRAT_COLORS = {
aggressive: '#ef4444', conservative: '#3b82f6', weather: '#06b6d4', momentum: '#f59e0b',
contrarian: '#a855f7', ai_enhanced: '#ec4899', mc_validated: '#8b5cf6', scalper: '#10b981',
patient: '#6366f1', random: '#f97316', penny_pincher: '#84cc16', spray_pray: '#14b8a6',
nickel_slots: '#eab308', degen_lite: '#e11d48', politics_junkie: '#7c3aed', double_down: '#dc2626',
volatility_rider: '#0ea5e9', whale: '#1d4ed8', yolo: '#be123c', full_send: '#ea580c',
kelly_criterion: '#4f46e5', sharpe_sniper: '#059669', bayesian_blend: '#7c3aed', ensemble_lock: '#0891b2',
mean_revert: '#4338ca', info_ratio: '#b45309', anti_fomo: '#64748b', nightcrawler: '#334155',
heatseeker: '#dc2626', quant_core: '#2563eb',
};
const ETF_COLOR = '#F7931A';
function getColor(strategy) {
if (strategy.startsWith('etf_')) return ETF_COLOR;
return STRAT_COLORS[strategy] || '#6b7280';
}
// ── Mini win/loss bar ──
function MiniBar({ won, lost, total }) {
if (!total) return <span className="text-xs" style={{ color: 'var(--text-muted)' }}>no trades</span>;
const w = (won / total) * 100;
const l = (lost / total) * 100;
return (
<div className="flex h-1.5 rounded-full overflow-hidden w-full" style={{ background: 'var(--border)' }}>
{won > 0 && <div style={{ width: `${w}%`, background: 'var(--green)' }} />}
{lost > 0 && <div style={{ width: `${l}%`, background: 'var(--red)' }} />}
</div>
);
}
// ── Streak badge ──
function StreakBadge({ streak }) {
if (streak === 0) return <span style={{ color: 'var(--text-muted)', fontSize: '0.7rem' }}>—</span>;
const hot = streak > 0;
return (
<span
className="mono font-bold text-xs"
style={{ color: hot ? 'var(--green)' : 'var(--red)' }}
>
{hot ? '+' : ''}{streak}
</span>
);
}
// ── Risk level badge ──
function RiskBadge({ level }) {
if (!level) return null;
const map = {
low: 'bg-green',
medium: 'bg-orange',
high: 'bg-red',
};
return (
<span className={`badge ${map[level] || 'bg-blue'}`}>
{level.toUpperCase()}
</span>
);
}
// ── Trade direction helpers ──
const sideLabel = (dir) => dir === 'BUY_YES' ? 'YES' : 'NO';
const sideBadge = (dir) => dir === 'BUY_YES' ? 'bg-green' : 'bg-red';
const pnlColor = (cents) => cents >= 0 ? 'var(--green)' : 'var(--red)';
// ── Portfolio Trades Table (shown inside expanded card) ──
function PortfolioTradesTable({ trades, loading }) {
if (loading) {
return (
<div className="flex justify-center py-4">
<div className="w-5 h-5 rounded-full border-2 animate-spin" style={{ borderColor: 'var(--border)', borderTopColor: 'var(--cyan)' }} />
</div>
);
}
if (!trades || trades.length === 0) {
return (
<p className="text-xs text-center py-3" style={{ color: 'var(--text-muted)' }}>
No trades found for this portfolio
</p>
);
}
return (
<div className="overflow-x-auto" style={{ maxHeight: 240, overflowY: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.7rem' }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--border)' }}>
{['Market', 'Dir', 'Entry', 'Qty', 'Cost', 'P&L', 'Status'].map(h => (
<th key={h} style={{
padding: '4px 6px', textAlign: 'left', color: 'var(--text-muted)',
fontWeight: 600, fontFamily: 'var(--font-body)', fontSize: '0.65rem',
whiteSpace: 'nowrap',
}}>{h}</th>
))}
</tr>
</thead>
<tbody>
{trades.map((t, i) => {
const pnl = parseInt(t.pnl_cents) || 0;
const cost = parseInt(t.cost_cents) || 0;
const entry = parseInt(t.entry_price_cents) || 0;
const contracts = parseInt(t.contracts) || 0;
const status = t.status || 'open';
return (
<tr key={t.id || i} style={{
borderBottom: '1px solid var(--border)',
background: i % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)',
}}>
<td style={{
padding: '4px 6px', maxWidth: 140, overflow: 'hidden',
textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: 'var(--text-dim)',
fontFamily: 'var(--font-body)',
}} title={t.market_title || t.market_id}>
{(t.market_title || t.market_id || '').substring(0, 28)}{(t.market_title || t.market_id || '').length > 28 ? '...' : ''}
</td>
<td style={{ padding: '4px 6px' }}>
<span className={`badge ${sideBadge(t.direction)}`} style={{ fontSize: '0.6rem', padding: '1px 5px' }}>
{sideLabel(t.direction)}
</span>
</td>
<td style={{ padding: '4px 6px', fontFamily: "'JetBrains Mono', monospace", color: 'var(--orange)' }}>
{entry}¢
</td>
<td style={{ padding: '4px 6px', fontFamily: "'JetBrains Mono', monospace", color: 'var(--text-dim)' }}>
{contracts}
</td>
<td style={{ padding: '4px 6px', fontFamily: "'JetBrains Mono', monospace", color: 'var(--text-dim)' }}>
${(cost / 100).toFixed(2)}
</td>
<td style={{
padding: '4px 6px', fontFamily: "'JetBrains Mono', monospace", fontWeight: 700,
color: pnlColor(pnl),
}}>
{pnl !== 0 ? `${pnl >= 0 ? '+' : ''}$${(pnl / 100).toFixed(2)}` : '—'}
</td>
<td style={{ padding: '4px 6px' }}>
<span className={`badge ${status === 'open' ? 'bg-blue' : status === 'closed' ? 'bg-green' : 'bg-orange'}`}
style={{ fontSize: '0.6rem', padding: '1px 5px' }}
>
{status}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
// ── Portfolio Card ──
function PortfolioCard({ p, dailyPnl, expanded, onToggle, pfTrades, pfTradesLoading }) {
const pnl = (p.total_returned_cents - p.total_invested_cents) / 100;
const totalTrades = p.trades_won + p.trades_lost + p.trades_expired;
const invested = p.total_invested_cents / 100;
const returned = p.total_returned_cents / 100;
const active = totalTrades > 0;
const color = getColor(p.strategy);
// Allocation as % of daily budget vs a synthetic max
const allocPct = Math.min(100, (p.daily_budget_cents / Math.max(1, 100000)) * 100);
return (
<div
className="card cursor-pointer transition-all"
style={
expanded
? {
borderColor: 'var(--cyan)',
boxShadow: '0 0 20px var(--cyan-glow)',
opacity: 1,
}
: active
? { opacity: 1 }
: { opacity: 0.5 }
}
onClick={onToggle}
>
{/* Header row */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2 min-w-0">
<div
className="w-2.5 h-2.5 rounded-full flex-shrink-0"
style={{ background: color }}
/>
<span
className="heading font-semibold text-sm truncate"
style={{ color: 'var(--text)' }}
>
{p.name}
</span>
<span className="mono text-xs flex-shrink-0" style={{ color: 'var(--text-muted)' }}>
#{p.id}
</span>
{totalTrades > 0 && (
<span className="text-xs flex-shrink-0 px-1.5 py-0.5 rounded"
style={{ background: 'var(--surface-el)', color: 'var(--text-muted)', fontSize: '0.65rem' }}>
{totalTrades} trade{totalTrades !== 1 ? 's' : ''}
</span>
)}
</div>
<span
className="mono text-sm font-bold flex-shrink-0 ml-2"
style={{ color: pnl >= 0 ? 'var(--green)' : 'var(--red)' }}
>
{pnl >= 0 ? '+' : ''}${pnl.toFixed(2)}
</span>
</div>
{/* Strategy tag + streak */}
<div className="flex items-center gap-2 mb-3">
<span
className="mono px-1.5 py-0.5 rounded text-xs flex-shrink-0"
style={{
background: `${color}15`,
color,
border: `1px solid ${color}30`,
}}
>
{p.strategy}
</span>
<StreakBadge streak={p.streak} />
<div className="flex-1" />
<span
className="badge flex-shrink-0"
style={
active
? { background: 'var(--green-dim)', color: 'var(--green)' }
: { background: 'var(--border)', color: 'var(--text-muted)' }
}
>
{active ? 'Active' : 'Dormant'}
</span>
</div>
{/* Win/loss bar */}
<div className="mb-3">
<MiniBar won={p.trades_won} lost={p.trades_lost} total={totalTrades} />
<div className="flex justify-between mt-1">
<span className="text-xs" style={{ color: 'var(--text-muted)' }}>
{p.trades_won}W / {p.trades_lost}L
</span>
<span className="mono text-xs" style={{ color: 'var(--text-dim)' }}>
{p.win_rate || 0}%
</span>
</div>
</div>
{/* Allocation bar */}
<div className="mb-3">
<div className="flex justify-between text-xs mb-1" style={{ color: 'var(--text-muted)' }}>
<span>Daily Budget</span>
<span className="mono" style={{ color: 'var(--text-dim)' }}>
${(p.daily_budget_cents / 100).toFixed(2)}
</span>
</div>
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${allocPct}%`, background: `linear-gradient(90deg, ${color}, ${color}99)` }}
/>
</div>
</div>
{/* Stats grid */}
<div className="grid grid-cols-2 gap-x-3 gap-y-1" style={{ fontSize: '0.7rem' }}>
<div className="flex justify-between">
<span style={{ color: 'var(--text-muted)' }}>Invested</span>
<span className="mono" style={{ color: 'var(--text-dim)' }}>${invested.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span style={{ color: 'var(--text-muted)' }}>Returned</span>
<span className="mono" style={{ color: 'var(--text-dim)' }}>${returned.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span style={{ color: 'var(--text-muted)' }}>Trades Today</span>
<span className="mono" style={{ color: 'var(--cyan)' }}>{p.trades_today}</span>
</div>
<div className="flex justify-between">
<span style={{ color: 'var(--text-muted)' }}>Total</span>
<span className="mono" style={{ color: 'var(--text-dim)' }}>{totalTrades}</span>
</div>
</div>
{/* Expanded sparkline */}
{expanded && dailyPnl && dailyPnl.length > 0 && (
<div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--border)' }}>
<p className="text-xs mb-1" style={{ color: 'var(--text-muted)' }}>Cumulative P&L</p>
<ResponsiveContainer width="100%" height={80}>
<AreaChart data={dailyPnl}>
<XAxis dataKey="date" hide />
<YAxis hide domain={['dataMin', 'dataMax']} />
<Tooltip
contentStyle={{
background: 'var(--surface-el)',
border: '1px solid var(--border)',
borderRadius: 8,
fontSize: 11,
fontFamily: 'var(--font-mono)',
}}
formatter={v => [`$${(v / 100).toFixed(2)}`, 'P&L']}
labelFormatter={l => l}
/>
<Area
type="monotone"
dataKey="cumulative"
stroke={color}
fill={color}
fillOpacity={0.1}
strokeWidth={1.5}
dot={false}
/>
</AreaChart>
</ResponsiveContainer>
{p.description && (
<p className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}>{p.description}</p>
)}
{/* Portfolio Trades Drill-Down */}
<div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--border)' }}>
<p className="text-xs mb-2 font-semibold" style={{ color: 'var(--cyan)' }}>
Individual Trades
</p>
<PortfolioTradesTable trades={pfTrades} loading={pfTradesLoading} />
</div>
</div>
)}
{/* If expanded but no dailyPnl chart, still show trades */}
{expanded && (!dailyPnl || dailyPnl.length === 0) && (
<div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--border)' }}>
<p className="text-xs mb-2 font-semibold" style={{ color: 'var(--cyan)' }}>
Individual Trades
</p>
<PortfolioTradesTable trades={pfTrades} loading={pfTradesLoading} />
</div>
)}
</div>
);
}
// ── Spinner ──
function Spinner() {
return (
<div className="flex items-center justify-center h-64">
<div
className="w-9 h-9 rounded-full border-2 animate-spin"
style={{ borderColor: 'var(--border)', borderTopColor: 'var(--cyan)' }}
/>
</div>
);
}
export default function Risk() {
const [portfolios, setPortfolios] = useState([]);
const [dailyData, setDailyData] = useState({});
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('all'); // all, active, dormant, etf, core
const [sort, setSort] = useState('pnl'); // pnl, winrate, invested, name, id
const [expanded, setExpanded] = useState(null);
const [pfTrades, setPfTrades] = useState({}); // { portfolioId: [...trades] }
const [pfTradesLoading, setPfTradesLoading] = useState(null); // portfolioId currently loading
useEffect(() => { load(); }, []);
async function load() {
try {
const [pf, hist] = await Promise.all([
api('/portfolios'),
api('/portfolio-trades?include_history=true').catch(() => ({})),
]);
setPortfolios(pf || []);
if (hist?.byPortfolio) setDailyData(hist.byPortfolio);
} catch {}
setLoading(false);
}
// Load trades for a portfolio when expanded
async function loadPortfolioTrades(portfolioId) {
if (pfTrades[portfolioId]) return; // already loaded
setPfTradesLoading(portfolioId);
try {
const data = await api(`/portfolio-trades?portfolio_id=${portfolioId}`);
setPfTrades(prev => ({ ...prev, [portfolioId]: Array.isArray(data) ? data : [] }));
} catch {
setPfTrades(prev => ({ ...prev, [portfolioId]: [] }));
}
setPfTradesLoading(null);
}
function handleToggle(id) {
if (expanded === id) {
setExpanded(null);
} else {
setExpanded(id);
loadPortfolioTrades(id);
}
}
const filtered = useMemo(() => {
let list = [...portfolios];
if (filter === 'active') list = list.filter(p => p.trades_won + p.trades_lost > 0);
if (filter === 'dormant') list = list.filter(p => p.trades_won + p.trades_lost === 0);
if (filter === 'etf') list = list.filter(p => p.strategy.startsWith('etf_'));
if (filter === 'core') list = list.filter(p => !p.strategy.startsWith('etf_'));
if (sort === 'pnl')
list.sort((a, b) =>
(b.total_returned_cents - b.total_invested_cents) -
(a.total_returned_cents - a.total_invested_cents)
);
if (sort === 'winrate') list.sort((a, b) => (b.win_rate || 0) - (a.win_rate || 0));
if (sort === 'invested') list.sort((a, b) => b.total_invested_cents - a.total_invested_cents);
if (sort === 'name') list.sort((a, b) => a.name.localeCompare(b.name));
if (sort === 'id') list.sort((a, b) => a.id - b.id);
return list;
}, [portfolios, filter, sort]);
const stats = useMemo(() => {
const totalInvested = portfolios.reduce((s, p) => s + p.total_invested_cents, 0);
const totalReturned = portfolios.reduce((s, p) => s + p.total_returned_cents, 0);
const totalWins = portfolios.reduce((s, p) => s + p.trades_won, 0);
const totalLosses = portfolios.reduce((s, p) => s + p.trades_lost, 0);
const active = portfolios.filter(p => p.trades_won + p.trades_lost > 0).length;
const dormant = portfolios.length - active;
const bestPf = [...portfolios].sort(
(a, b) =>
(b.total_returned_cents - b.total_invested_cents) -
(a.total_returned_cents - a.total_invested_cents)
)[0];
const worstPf = [...portfolios].sort(
(a, b) =>
(a.total_returned_cents - a.total_invested_cents) -
(b.total_returned_cents - b.total_invested_cents)
)[0];
return { totalInvested, totalReturned, totalWins, totalLosses, active, dormant, bestPf, worstPf };
}, [portfolios]);
const barData = useMemo(() =>
[...portfolios]
.map(p => ({ name: p.name, pnl: (p.total_returned_cents - p.total_invested_cents) / 100, strategy: p.strategy }))
.filter(p => p.pnl !== 0)
.sort((a, b) => b.pnl - a.pnl)
.slice(0, 20),
[portfolios]
);
const pieData = useMemo(() => {
const cats = {};
portfolios.forEach(p => {
const cat = p.strategy.startsWith('etf_') ? 'ETF Baskets' : p.strategy;
if (!cats[cat]) cats[cat] = { name: cat, value: 0 };
cats[cat].value += p.total_invested_cents;
});
return Object.values(cats).filter(c => c.value > 0).sort((a, b) => b.value - a.value);
}, [portfolios]);
if (loading) return <Spinner />;
const pnl = (stats.totalReturned - stats.totalInvested) / 100;
const totalTrades = stats.totalWins + stats.totalLosses;
return (
<div className="space-y-6 fade-in">
{/* ── Page Header ── */}
<div className="flex items-center justify-between">
<div>
<h1 className="heading text-3xl" style={{ color: 'var(--text)' }}>
Portfauxlios
<span className="badge bg-cyan ml-3 align-middle text-base">
{portfolios.length}
</span>
</h1>
<p className="section-subtitle mt-1">
All {portfolios.length} portfolios — performance, P&L, and strategy breakdown
</p>
</div>
<button onClick={load} className="btn btn-o btn-sm">Refresh</button>
</div>
{/* ── Summary Stats ── */}
<div className="grid grid-cols-3 md:grid-cols-6 gap-3">
{[
{
label: 'Total P&L',
value: `${pnl >= 0 ? '+' : ''}$${pnl.toFixed(2)}`,
color: pnl >= 0 ? 'var(--green)' : 'var(--red)',
},
{
label: 'Invested',
value: `$${(stats.totalInvested / 100).toFixed(2)}`,
color: 'var(--orange)',
},
{
label: 'Returned',
value: `$${(stats.totalReturned / 100).toFixed(2)}`,
color: 'var(--blue)',
},
{
label: 'Win Rate',
value: totalTrades ? `${((stats.totalWins / totalTrades) * 100).toFixed(1)}%` : '0%',
color: 'var(--purple)',
},
{ label: 'Active', value: stats.active, color: 'var(--green)' },
{ label: 'Dormant', value: stats.dormant, color: 'var(--text-muted)' },
].map(s => (
<div key={s.label} className="card text-center" style={{ padding: '1rem' }}>
<div className="stat-v" style={{ color: s.color, fontSize: '1.4rem' }}>
{s.value}
</div>
<div className="stat-label mt-1">{s.label}</div>
</div>
))}
</div>
{/* ── Charts Row ── */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* P&L Bar Chart */}
<div className="card md:col-span-2">
<h2 className="heading text-sm mb-3" style={{ color: 'var(--text-dim)' }}>
P&L by Portfolio
</h2>
{barData.length > 0 ? (
<ResponsiveContainer width="100%" height={220}>
<BarChart data={barData} margin={{ left: 10, right: 10 }}>
<XAxis
dataKey="name"
tick={{ fill: 'var(--text-muted)', fontSize: 9, fontFamily: 'var(--font-mono)' }}
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis
tick={{ fill: 'var(--text-muted)', fontSize: 10, fontFamily: 'var(--font-mono)' }}
tickFormatter={v => `$${v}`}
/>
<Tooltip
contentStyle={{
background: 'var(--surface-el)',
border: '1px solid var(--border)',
borderRadius: 8,
fontSize: 11,
fontFamily: 'var(--font-mono)',
}}
formatter={v => [`$${v.toFixed(2)}`, 'P&L']}
/>
<Bar dataKey="pnl" radius={[4, 4, 0, 0]}>
{barData.map((d, i) => (
<Cell
key={i}
fill={d.pnl >= 0 ? 'var(--green)' : 'var(--red)'}
fillOpacity={0.8}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
) : (
<div className="text-center py-10 text-sm" style={{ color: 'var(--text-muted)' }}>
No P&L data yet
</div>
)}
</div>
{/* Allocation Pie */}
<div className="card">
<h2 className="heading text-sm mb-3" style={{ color: 'var(--text-dim)' }}>
Capital Allocation
</h2>
{pieData.length > 0 ? (
<ResponsiveContainer width="100%" height={220}>
<PieChart>
<Pie
data={pieData}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
innerRadius={40}
outerRadius={75}
paddingAngle={2}
strokeWidth={0}
>
{pieData.map((d, i) => (
<Cell
key={i}
fill={getColor(d.name === 'ETF Baskets' ? 'etf_' : d.name)}
fillOpacity={0.85}
/>
))}
</Pie>
<Tooltip
contentStyle={{
background: 'var(--surface-el)',
border: '1px solid var(--border)',
borderRadius: 8,
fontSize: 11,
fontFamily: 'var(--font-mono)',
}}
formatter={v => [`$${(v / 100).toFixed(2)}`, 'Invested']}
/>
</PieChart>
</ResponsiveContainer>
) : (
<div className="text-center py-10 text-sm" style={{ color: 'var(--text-muted)' }}>
No allocations yet
</div>
)}
</div>
</div>
{/* ── Best / Worst Performers ── */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{stats.bestPf && (
<div
className="card"
style={{ borderColor: 'rgba(0,195,137,0.25)', background: 'var(--green-dim)' }}
>
<div className="flex items-center gap-2 mb-2">
<span
className="badge"
style={{ background: 'var(--green-dim)', color: 'var(--green)' }}
>
Best Performer
</span>
</div>
<div className="flex items-center justify-between">
<span className="heading font-bold" style={{ color: 'var(--text)' }}>
{stats.bestPf.name}
</span>
<span className="mono font-bold text-green">
+${((stats.bestPf.total_returned_cents - stats.bestPf.total_invested_cents) / 100).toFixed(2)}
</span>
</div>
<span className="text-xs" style={{ color: 'var(--text-muted)' }}>
{stats.bestPf.strategy} — {stats.bestPf.win_rate || 0}% win rate
</span>
</div>
)}
{stats.worstPf && (
<div
className="card"
style={{ borderColor: 'rgba(255,92,92,0.25)', background: 'var(--red-dim)' }}
>
<div className="flex items-center gap-2 mb-2">
<span
className="badge"
style={{ background: 'var(--red-dim)', color: 'var(--red)' }}
>
Worst Performer
</span>
</div>
<div className="flex items-center justify-between">
<span className="heading font-bold" style={{ color: 'var(--text)' }}>
{stats.worstPf.name}
</span>
<span className="mono font-bold text-red">
${((stats.worstPf.total_returned_cents - stats.worstPf.total_invested_cents) / 100).toFixed(2)}
</span>
</div>
<span className="text-xs" style={{ color: 'var(--text-muted)' }}>
{stats.worstPf.strategy} — {stats.worstPf.win_rate || 0}% win rate
</span>
</div>
)}
</div>
{/* ── Filter & Sort Controls ── */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap gap-2">
{['all', 'active', 'dormant', 'etf', 'core'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className="px-3 py-1.5 rounded-lg text-xs font-semibold transition"
style={
filter === f
? {
background: 'var(--cyan-dim)',
color: 'var(--cyan)',
border: '1px solid rgba(0,240,255,0.3)',
}
: {
background: 'var(--surface-el)',
color: 'var(--text-muted)',
border: '1px solid var(--border)',
}
}
>
{f === 'all'
? `All (${portfolios.length})`
: f === 'active'
? `Active (${stats.active})`
: f === 'dormant'
? `Dormant (${stats.dormant})`
: f === 'etf'
? 'ETF Baskets'
: 'Core Strategies'}
</button>
))}
</div>
<div className="flex items-center gap-2">
<span className="text-xs" style={{ color: 'var(--text-muted)' }}>Sort:</span>
{['pnl', 'winrate', 'invested', 'name', 'id'].map(s => (
<button
key={s}
onClick={() => setSort(s)}
className="px-2 py-1 rounded text-xs font-medium transition"
style={
sort === s
? { color: 'var(--cyan)', background: 'var(--cyan-dim)' }
: { color: 'var(--text-muted)' }
}
>
{s === 'pnl' ? 'P&L' : s === 'winrate' ? 'Win%' : s.charAt(0).toUpperCase() + s.slice(1)}
</button>
))}
</div>
</div>
{/* ── Portfolio Card Grid ── */}
{filtered.length === 0 ? (
<div
className="text-center py-16 text-sm"
style={{ color: 'var(--text-muted)' }}
>
No portfolios match this filter
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{filtered.map(p => (
<PortfolioCard
key={p.id}
p={p}
dailyPnl={dailyData[p.id]?.daily || []}
expanded={expanded === p.id}
onToggle={() => handleToggle(p.id)}
pfTrades={pfTrades[p.id] || null}
pfTradesLoading={pfTradesLoading === p.id}
/>
))}
</div>
)}
</div>
);
}