← back to Ken
kalshi-dash/src/pages/Positions.jsx
970 lines
import React, { useState, useEffect, useCallback } from 'react';
import { api, post } from '../api';
import MiniCandlestick, { genCandles } from '../components/MiniCandlestick';
import {
BarChart, Bar, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
} from 'recharts';
// ── Helpers ──
const fmt = (cents) => `$${(cents / 100).toFixed(2)}`;
const fmtPnl = (cents) => {
const val = cents / 100;
const prefix = val >= 0 ? '+' : '';
return `${prefix}$${val.toFixed(2)}`;
};
const pnlColor = (cents) => cents >= 0 ? 'text-green' : 'text-red';
const sideBadge = (dir) => dir === 'BUY_YES' ? 'bg-green' : 'bg-red';
const sideLabel = (dir) => dir === 'BUY_YES' ? 'YES' : 'NO';
const fmtCents = (cents) => `${cents}¢`;
const calcExitPrice = (entryPriceCents, pnlCents, contracts) => {
if (!contracts || contracts === 0) return null;
return Math.round(entryPriceCents + (pnlCents / contracts));
};
const kalshiLink = (ticker) =>
`https://kalshi.com/search?query=${encodeURIComponent((ticker || '').split('-')[0])}`;
const timeAgo = (d) => {
if (!d) return '';
const ms = Date.now() - new Date(d).getTime();
if (ms < 60000) return 'just now';
if (ms < 3600000) return `${Math.floor(ms / 60000)}m ago`;
if (ms < 86400000) return `${Math.floor(ms / 3600000)}h ago`;
return `${Math.floor(ms / 86400000)}d ago`;
};
/* ── Chart 6: P&L Distribution Histogram ── */
function PnlDistributionChart({ positions }) {
if (!positions || !positions.length) return null;
/* Define buckets */
const BUCKETS = [
{ label: '<-$50', min: -Infinity, max: -5000, color: '#FF5C5C' },
{ label: '-$20-50', min: -5000, max: -2000, color: '#FF7A7A' },
{ label: '-$1-20', min: -2000, max: -100, color: '#FF9999' },
{ label: '$0', min: -100, max: 100, color: '#6B7280' },
{ label: '$1-20', min: 100, max: 2000, color: '#88DDBB' },
{ label: '$20-50', min: 2000, max: 5000, color: '#44CC99' },
{ label: '>$50', min: 5000, max: Infinity, color: '#00C389' },
];
const bucketed = BUCKETS.map(b => {
const count = positions.filter(p => {
const v = p.realized_pnl_cents || 0;
return v > b.min && v <= b.max;
}).length;
return { ...b, count };
}).filter(b => b.count > 0);
if (!bucketed.length) return null;
const CustomTooltip = ({ active, payload, label }) => {
if (!active || !payload?.length) return null;
const d = payload[0]?.payload || {};
return (
<div style={{
background: '#1C1C24', border: '1px solid #2A2A35', borderRadius: 8,
padding: '8px 12px', fontSize: 12, fontFamily: "'Inter', sans-serif",
}}>
<p style={{ color: '#E0E0E0', fontWeight: 700, marginBottom: 4 }}>{label}</p>
<p style={{ color: d.color, fontFamily: "'JetBrains Mono', monospace", fontWeight: 700 }}>
{d.count} position{d.count !== 1 ? 's' : ''}
</p>
</div>
);
};
return (
<div className="card">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
<h2 className="section-title" style={{ fontSize: '1.05rem' }}>P&L Distribution</h2>
<span style={{ fontSize: 11, color: '#6B7280' }}>{positions.length} positions total</span>
</div>
<ResponsiveContainer width="100%" height={180}>
<BarChart data={bucketed} margin={{ top: 4, right: 12, left: 0, bottom: 4 }}>
<CartesianGrid
strokeDasharray="3 3"
stroke="#2A2A35"
strokeOpacity={0.6}
vertical={false}
/>
<XAxis
dataKey="label"
stroke="#2A2A35"
tick={{ fill: '#6B7280', fontSize: 10, fontFamily: "'Inter', sans-serif" }}
tickLine={false}
axisLine={{ stroke: '#2A2A35' }}
/>
<YAxis
stroke="#2A2A35"
tick={{ fill: '#6B7280', fontSize: 11, fontFamily: "'Inter', sans-serif" }}
tickLine={false}
axisLine={false}
allowDecimals={false}
width={30}
/>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'rgba(0,240,255,0.04)' }} />
<Bar dataKey="count" radius={[4, 4, 0, 0]} maxBarSize={48}>
{bucketed.map((entry, i) => (
<Cell key={i} fill={entry.color} fillOpacity={0.85} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
);
}
// ── Loading 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>
);
}
// ── Trade Sub-table ──
function TradeSubTable({ p, trades, loading }) {
return (
<div style={{ background: 'var(--bg)', borderTop: '1px solid var(--border)', borderBottom: '1px solid var(--border)' }} className="px-4 py-3">
<div className="flex items-center justify-between mb-3">
<span className="mono text-xs font-bold" style={{ color: 'var(--cyan)' }}>
Trade History — {p.ticker}
</span>
<div className="flex gap-4 text-xs" style={{ color: 'var(--text-muted)' }}>
<span>Open: <b style={{ color: 'var(--blue)' }}>{p.open_trades}</b></span>
<span>Closed: <b style={{ color: 'var(--text-dim)' }}>{p.closed_trades}</b></span>
<span>
Entry Range:{' '}
<b style={{ color: 'var(--orange)' }}>
{p.min_entry}¢ – {p.max_entry}¢
</b>
</span>
<span>First: <b>{new Date(p.first_trade_at).toLocaleDateString()}</b></span>
</div>
</div>
{loading ? (
<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>
) : trades.length > 0 ? (
<div className="overflow-x-auto" style={{ maxHeight: 200, overflowY: 'auto' }}>
<table className="text-xs">
<thead>
<tr>
<th>Time</th>
<th>Direction</th>
<th>Contracts</th>
<th>Entry</th>
<th>Exit</th>
<th>Cost</th>
<th>Profit</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{trades.map((t, i) => {
const entryC = parseInt(t.entry_price_cents) || 0;
const pnlC = parseInt(t.pnl_cents) || 0;
const ctrs = parseInt(t.contracts) || 0;
const isClosed = t.status === 'closed' || t.status === 'expired';
const exitPrice = isClosed && ctrs > 0 ? calcExitPrice(entryC, pnlC, ctrs) : null;
return (
<tr key={i}>
<td className="mono" style={{ color: 'var(--text-muted)' }}>
{new Date(t.created_at).toLocaleString()}
</td>
<td>
<span className={`badge ${sideBadge(t.direction)}`}>{sideLabel(t.direction)}</span>
</td>
<td className="mono">{t.contracts}</td>
<td className="mono" style={{ color: 'var(--orange)', fontFamily: "'JetBrains Mono', monospace" }}>
{entryC}¢
</td>
<td className="mono" style={{ fontFamily: "'JetBrains Mono', monospace", color: exitPrice != null ? 'var(--text-dim)' : 'var(--text-muted)' }}>
{exitPrice != null ? `${exitPrice}¢` : '—'}
</td>
<td className="mono" style={{ color: 'var(--text-dim)' }}>{fmt(t.cost_cents || 0)}</td>
<td className="mono" style={{
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 700,
color: pnlC > 0 ? 'var(--green)' : pnlC < 0 ? 'var(--red)' : 'var(--text-muted)',
}}>
{t.pnl_cents != null && isClosed ? fmtPnl(pnlC) : '—'}
</td>
<td>
<span className={`badge ${t.status === 'open' ? 'bg-blue' : t.status === 'closed' ? 'bg-green' : 'bg-orange'}`}>
{t.status}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : (
<p className="text-center py-3 text-xs" style={{ color: 'var(--text-muted)' }}>
No trade details available
</p>
)}
</div>
);
}
export default function Positions() {
const [positions, setPositions] = useState([]);
const [grouped, setGrouped] = useState([]);
const [summary, setSummary] = useState({});
const [loading, setLoading] = useState(true);
const [expandedGroup, setExpandedGroup] = useState(null);
const [expandedPos, setExpandedPos] = useState(null);
const [trades, setTrades] = useState({});
const [tradesLoading, setTradesLoading] = useState(null);
const [viewMode, setViewMode] = useState('grouped'); // grouped | flat | pnl
const [sortBy, setSortBy] = useState('cost'); // cost | pnl | contracts
const load = useCallback(async () => {
try {
const r = await api('/positions');
if (r.success) {
setPositions(r.positions || []);
setGrouped(r.grouped || []);
setSummary(r.summary || {});
}
} catch {}
setLoading(false);
}, []);
useEffect(() => {
load();
const i = setInterval(load, 30000);
return () => clearInterval(i);
}, [load]);
async function loadTrades(ticker, direction) {
const key = `${ticker}_${direction}`;
if (trades[key]) return;
setTradesLoading(key);
try {
const r = await post('/positions/trades', { ticker, direction, limit: 50 });
if (r.success) {
setTrades(prev => ({ ...prev, [key]: r.trades || [] }));
}
} catch {}
setTradesLoading(null);
}
function toggleGroup(event) {
setExpandedGroup(prev => (prev === event ? null : event));
setExpandedPos(null);
}
function togglePos(ticker, direction) {
const key = `${ticker}_${direction}`;
if (expandedPos === key) {
setExpandedPos(null);
} else {
setExpandedPos(key);
loadTrades(ticker, direction);
}
}
const sortedPositions = [...positions].sort((a, b) => {
if (sortBy === 'pnl') return b.realized_pnl_cents - a.realized_pnl_cents;
if (sortBy === 'contracts') return b.open_contracts - a.open_contracts;
return b.open_cost_cents - a.open_cost_cents;
});
const winners = positions
.filter(p => p.realized_pnl_cents > 0)
.sort((a, b) => b.realized_pnl_cents - a.realized_pnl_cents);
const losers = positions
.filter(p => p.realized_pnl_cents < 0)
.sort((a, b) => a.realized_pnl_cents - b.realized_pnl_cents);
const flat = positions.filter(p => p.realized_pnl_cents === 0);
const winRate =
positions.length > 0 && winners.length + losers.length > 0
? `${Math.round((winners.length / (winners.length + losers.length)) * 100)}%`
: '—';
if (loading) return <Spinner />;
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)' }}>
Positions
<span className="badge bg-cyan ml-3 align-middle text-base">
{summary.positionCount || 0}
</span>
</h1>
<p className="section-subtitle mt-1">
{summary.positionCount || 0} active positions •{' '}
{summary.totalTrades?.toLocaleString() || 0} total trades
</p>
</div>
<div className="flex items-center gap-2">
{/* View mode toggle */}
<div
className="flex rounded-lg p-0.5 gap-0.5"
style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
>
{[['grouped', 'Grouped'], ['flat', 'All'], ['pnl', 'P&L']].map(([v, label]) => (
<button
key={v}
onClick={() => setViewMode(v)}
className="px-3 py-1 text-xs font-semibold rounded-md transition"
style={
viewMode === v
? { background: 'var(--cyan)', color: 'var(--bg)', fontFamily: 'var(--font-body)' }
: { color: 'var(--text-muted)', fontFamily: 'var(--font-body)' }
}
>
{label}
</button>
))}
</div>
<button onClick={load} className="btn btn-o btn-sm">Refresh</button>
</div>
</div>
{/* ── Summary Stat Cards ── */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
<div className="card card-accent-cyan">
<p className="stat-label">Positions</p>
<p className="stat-v mono mt-2" style={{ color: 'var(--cyan)' }}>{summary.positionCount || 0}</p>
</div>
<div className="card card-accent-orange">
<p className="stat-label">Total Cost</p>
<p className="stat-v mono mt-2" style={{ color: 'var(--text)' }}>{fmt(summary.totalCost || 0)}</p>
</div>
<div className={`card ${(summary.totalPnl || 0) >= 0 ? 'card-accent-green' : 'card-accent-red'}`}>
<p className="stat-label">Realized P&L</p>
<p className={`stat-v mono mt-2 ${(summary.totalPnl || 0) >= 0 ? 'pnl-positive' : 'pnl-negative'}`}>
{fmtPnl(summary.totalPnl || 0)}
</p>
</div>
<div className="card card-accent-blue">
<p className="stat-label">Contracts</p>
<p className="stat-v mono mt-2" style={{ color: 'var(--blue)' }}>
{(summary.totalContracts || 0).toLocaleString()}
</p>
</div>
<div className="card card-accent-green">
<p className="stat-label">Win Rate</p>
<p className="stat-v mono mt-2 text-green">{winRate}</p>
</div>
</div>
{/* ── P&L Breakdown Bar ── */}
{summary.totalPnl !== 0 && (
<div className="card py-3">
<div className="flex items-center justify-between text-xs mb-2">
<span className="font-semibold text-green">{winners.length} winners</span>
<span style={{ color: 'var(--text-muted)' }}>{flat.length} flat</span>
<span className="font-semibold text-red">{losers.length} losers</span>
</div>
<div className="progress-bar" style={{ height: 8 }}>
{(() => {
const totalAbs =
winners.reduce((s, p) => s + p.realized_pnl_cents, 0) +
Math.abs(losers.reduce((s, p) => s + p.realized_pnl_cents, 0));
if (!totalAbs) return null;
const winPct = (winners.reduce((s, p) => s + p.realized_pnl_cents, 0) / totalAbs) * 100;
return (
<div className="flex h-full w-full">
<div className="bar-win-glow" style={{ width: `${winPct}%`, background: 'var(--green)', borderRadius: '2px 0 0 2px' }} />
<div className="bar-loss-glow" style={{ width: `${100 - winPct}%`, background: 'var(--red)', borderRadius: '0 2px 2px 0' }} />
</div>
);
})()}
</div>
</div>
)}
{/* ── Chart 6: P&L Distribution Histogram ── */}
<PnlDistributionChart positions={positions} />
{/* ══════════════════════════════
GROUPED VIEW
══════════════════════════════ */}
{viewMode === 'grouped' && (
<div className="space-y-3">
{grouped.map((group) => {
const isExpanded = expandedGroup === group.event;
return (
<div
key={group.event}
className="card p-0 overflow-hidden"
style={{ padding: 0 }}
>
{/* Group Header */}
<button
onClick={() => toggleGroup(group.event)}
className="w-full flex items-center gap-3 p-4 text-left transition"
style={{ background: 'transparent' }}
onMouseEnter={e => (e.currentTarget.style.background = 'var(--cyan-glow)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
<span
className="text-xs inline-block transition-transform"
style={{
color: 'var(--cyan)',
transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
}}
>
▶
</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="mono text-sm font-bold" style={{ color: 'var(--orange)' }}>
{group.event}
</span>
<span className="badge bg-cyan">{group.positions.length}</span>
</div>
<p className="text-xs truncate mt-0.5" style={{ color: 'var(--text-muted)' }}>
{group.title}
</p>
</div>
<div className="flex items-center gap-6 text-right flex-shrink-0">
<div>
<p className="text-xs" style={{ color: 'var(--text-muted)' }}>Cost</p>
<p className="mono text-sm" style={{ color: 'var(--text)' }}>{fmt(group.totalCost)}</p>
</div>
<div>
<p className="text-xs" style={{ color: 'var(--text-muted)' }}>Contracts</p>
<p className="mono text-sm" style={{ color: 'var(--blue)' }}>
{group.totalContracts.toLocaleString()}
</p>
</div>
<div>
<p className="text-xs" style={{ color: 'var(--text-muted)' }}>Realized P&L</p>
<p className={`mono text-sm font-bold ${pnlColor(group.totalPnl)}`}>
{fmtPnl(group.totalPnl)}
</p>
</div>
</div>
</button>
{/* Expanded Positions Table */}
{isExpanded && (
<div style={{ borderTop: '1px solid var(--border)' }}>
<table>
<thead>
<tr>
<th></th>
<th>Ticker</th>
<th>Side</th>
<th>Contracts</th>
<th>Bought At</th>
<th>Sold At</th>
<th>Cost</th>
<th>Profit</th>
<th>Trades</th>
<th>Last Trade</th>
</tr>
</thead>
<tbody>
{group.positions.map((p) => {
const posKey = `${p.ticker}_${p.direction}`;
const isPosExpanded = expandedPos === posKey;
const posTrades = trades[posKey] || [];
const isLoadingTrades = tradesLoading === posKey;
const isWinning = (p.realized_pnl_cents || 0) > 0;
const isLosing = (p.realized_pnl_cents || 0) < 0;
// Deterministic seed from ticker string
const tickerSeed = p.ticker.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
const candleTrend = isWinning ? 'up' : isLosing ? 'down' : 'flat';
const candleData = genCandles(tickerSeed, 9, candleTrend);
const rowClass = isWinning ? 'cursor-pointer position-winning' : isLosing ? 'cursor-pointer position-losing' : 'cursor-pointer';
return (
<React.Fragment key={posKey}>
<tr
className={rowClass}
onClick={() => togglePos(p.ticker, p.direction)}
style={{ position: 'relative' }}
>
<td style={{ width: 24 }}>
<span
className="text-xs inline-block transition-transform"
style={{
color: 'var(--cyan)',
transform: isPosExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
}}
>
▶
</span>
</td>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<a
href={kalshiLink(p.ticker)}
target="_blank"
rel="noopener noreferrer"
className="mono text-sm hover:underline"
style={{ color: 'var(--orange)' }}
onClick={e => e.stopPropagation()}
>
{p.ticker}
</a>
<MiniCandlestick data={candleData} width={72} height={28} />
</div>
</td>
<td>
<span className={`badge ${sideBadge(p.direction)}`}>
{sideLabel(p.direction)}
</span>
</td>
<td className="mono">{p.open_contracts?.toLocaleString()}</td>
<td className="mono" style={{ color: 'var(--orange)', fontFamily: "'JetBrains Mono', monospace" }}>
{p.avg_entry_price}¢
</td>
<td className="mono" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
{(() => {
const closedCount = parseInt(p.closed_trades) || 0;
const openCount = parseInt(p.open_trades) || 0;
if (closedCount > 0 && openCount === 0) {
const exit = calcExitPrice(p.avg_entry_price, p.realized_pnl_cents, parseInt(p.open_contracts) || parseInt(p.total_trades) || 1);
return <span style={{ color: 'var(--text-dim)' }}>{exit}¢</span>;
}
return <span className="badge bg-blue" style={{ fontSize: '0.65rem' }}>Open</span>;
})()}
</td>
<td className="mono" style={{ color: 'var(--text)' }}>
{fmt(p.open_cost_cents)}
</td>
<td className="mono" style={{
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 700,
color: p.realized_pnl_cents > 0 ? 'var(--green)' : p.realized_pnl_cents < 0 ? 'var(--red)' : 'var(--text-muted)',
}}>
{p.realized_pnl_cents !== 0 ? fmtPnl(p.realized_pnl_cents) : '—'}
</td>
<td className="mono" style={{ color: 'var(--text-muted)' }}>
{p.total_trades?.toLocaleString()}
</td>
<td className="text-xs" style={{ color: 'var(--text-muted)' }}>
{timeAgo(p.last_trade_at)}
</td>
</tr>
{isPosExpanded && (
<tr>
<td colSpan={10} style={{ padding: 0 }}>
<TradeSubTable
p={p}
trades={posTrades}
loading={isLoadingTrades}
/>
</td>
</tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
})}
</div>
)}
{/* ══════════════════════════════
FLAT VIEW
══════════════════════════════ */}
{viewMode === 'flat' && (
<div className="card overflow-x-auto" style={{ padding: 0 }}>
{/* Sort controls */}
<div
className="flex items-center gap-3 px-4 py-3"
style={{ borderBottom: '1px solid var(--border)' }}
>
<span className="text-xs" style={{ color: 'var(--text-muted)' }}>Sort by:</span>
{[['cost', 'Cost'], ['pnl', 'P&L'], ['contracts', 'Contracts']].map(([v, label]) => (
<button
key={v}
onClick={() => setSortBy(v)}
className="text-xs font-semibold px-2.5 py-1 rounded-md transition"
style={
sortBy === v
? { background: 'var(--cyan-dim)', color: 'var(--cyan)' }
: { color: 'var(--text-muted)' }
}
>
{label}
</button>
))}
</div>
<table>
<thead>
<tr>
<th>#</th>
<th>Ticker</th>
<th>Market</th>
<th>Side</th>
<th>Contracts</th>
<th>Bought At</th>
<th>Sold At</th>
<th>Cost</th>
<th>Profit</th>
<th>Trades</th>
<th>Last Trade</th>
</tr>
</thead>
<tbody>
{sortedPositions.length === 0 ? (
<tr>
<td colSpan={11} className="text-center py-12" style={{ color: 'var(--text-muted)' }}>
No open positions
</td>
</tr>
) : (
sortedPositions.map((p, i) => {
const posKey = `${p.ticker}_${p.direction}`;
const isPosExpanded = expandedPos === posKey;
const posTrades = trades[posKey] || [];
const isLoadingTrades = tradesLoading === posKey;
const isWinningFlat = (p.realized_pnl_cents || 0) > 0;
const isLosingFlat = (p.realized_pnl_cents || 0) < 0;
const tickerSeedFlat = p.ticker.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
const candleTrendFlat = isWinningFlat ? 'up' : isLosingFlat ? 'down' : 'flat';
const candleDataFlat = genCandles(tickerSeedFlat + 100, 9, candleTrendFlat);
const flatRowClass = isWinningFlat ? 'cursor-pointer position-winning' : isLosingFlat ? 'cursor-pointer position-losing' : 'cursor-pointer';
return (
<React.Fragment key={posKey}>
<tr
className={flatRowClass}
onClick={() => togglePos(p.ticker, p.direction)}
style={{ position: 'relative' }}
>
<td style={{ color: 'var(--text-muted)' }}>{i + 1}</td>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<a
href={kalshiLink(p.ticker)}
target="_blank"
rel="noopener noreferrer"
className="mono text-sm hover:underline"
style={{ color: 'var(--orange)' }}
onClick={e => e.stopPropagation()}
>
{p.ticker}
</a>
<MiniCandlestick data={candleDataFlat} width={72} height={28} />
</div>
</td>
<td
className="text-xs max-w-[192px] truncate"
style={{ color: 'var(--text-dim)' }}
>
{p.title}
</td>
<td>
<span className={`badge ${sideBadge(p.direction)}`}>
{sideLabel(p.direction)}
</span>
</td>
<td className="mono">{p.open_contracts?.toLocaleString()}</td>
<td className="mono" style={{ color: 'var(--orange)', fontFamily: "'JetBrains Mono', monospace" }}>
{p.avg_entry_price}¢
</td>
<td className="mono" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
{(() => {
const closedCount = parseInt(p.closed_trades) || 0;
const openCount = parseInt(p.open_trades) || 0;
if (closedCount > 0 && openCount === 0) {
const exit = calcExitPrice(p.avg_entry_price, p.realized_pnl_cents, parseInt(p.open_contracts) || parseInt(p.total_trades) || 1);
return <span style={{ color: 'var(--text-dim)' }}>{exit}¢</span>;
}
return <span className="badge bg-blue" style={{ fontSize: '0.65rem' }}>Open</span>;
})()}
</td>
<td className="mono" style={{ color: 'var(--text)' }}>
{fmt(p.open_cost_cents)}
</td>
<td className="mono" style={{
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 700,
color: p.realized_pnl_cents > 0 ? 'var(--green)' : p.realized_pnl_cents < 0 ? 'var(--red)' : 'var(--text-muted)',
}}>
{p.realized_pnl_cents !== 0 ? fmtPnl(p.realized_pnl_cents) : '—'}
</td>
<td className="mono" style={{ color: 'var(--text-muted)' }}>
{p.total_trades?.toLocaleString()}
</td>
<td className="text-xs" style={{ color: 'var(--text-muted)' }}>
{timeAgo(p.last_trade_at)}
</td>
</tr>
{isPosExpanded && (
<tr>
<td colSpan={11} style={{ padding: 0 }}>
<TradeSubTable p={p} trades={posTrades} loading={isLoadingTrades} />
</td>
</tr>
)}
</React.Fragment>
);
})
)}
</tbody>
</table>
</div>
)}
{/* ══════════════════════════════
P&L VIEW
══════════════════════════════ */}
{viewMode === 'pnl' && (
<div className="space-y-4">
{/* Winners */}
<div className="card overflow-hidden" style={{ padding: 0 }}>
<div
className="flex items-center gap-3 px-4 py-3"
style={{
background: 'rgba(0,195,137,0.06)',
borderBottom: '1px solid rgba(0,195,137,0.2)',
borderLeft: '4px solid var(--green)',
}}
>
<span className="text-sm font-bold text-green" style={{ textShadow: '0 0 10px rgba(0,195,137,0.4)' }}>Winners</span>
<span className="badge bg-green">{winners.length}</span>
<div className="flex-1" />
<span className="mono font-bold pnl-positive">
{fmtPnl(winners.reduce((s, p) => s + p.realized_pnl_cents, 0))}
</span>
</div>
<table>
<thead>
<tr>
<th>Ticker</th>
<th>Market</th>
<th>Side</th>
<th>Contracts</th>
<th>Bought At</th>
<th>Sold At</th>
<th>Cost</th>
<th>Profit</th>
<th>ROI</th>
</tr>
</thead>
<tbody>
{winners.map(p => {
const roi =
p.open_cost_cents > 0
? `${((p.realized_pnl_cents / p.open_cost_cents) * 100).toFixed(0)}%`
: '∞';
const wTickerSeed = p.ticker.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
const wCandles = genCandles(wTickerSeed + 200, 9, 'up');
const closedCount = parseInt(p.closed_trades) || 0;
const openCount = parseInt(p.open_trades) || 0;
const exitPrice = (closedCount > 0 && openCount === 0)
? calcExitPrice(p.avg_entry_price, p.realized_pnl_cents, parseInt(p.open_contracts) || parseInt(p.total_trades) || 1)
: null;
return (
<tr key={`${p.ticker}_${p.direction}`} className="position-winning" style={{ position: 'relative' }}>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<a
href={kalshiLink(p.ticker)}
target="_blank"
rel="noopener"
className="mono text-sm hover:underline"
style={{ color: 'var(--orange)' }}
>
{p.ticker}
</a>
<MiniCandlestick data={wCandles} width={64} height={26} />
</div>
</td>
<td className="text-xs max-w-[192px] truncate" style={{ color: 'var(--text-dim)' }}>
{p.title}
</td>
<td>
<span className={`badge ${sideBadge(p.direction)}`}>
{sideLabel(p.direction)}
</span>
</td>
<td className="mono">{p.open_contracts?.toLocaleString()}</td>
<td className="mono" style={{ color: 'var(--orange)', fontFamily: "'JetBrains Mono', monospace" }}>
{p.avg_entry_price}¢
</td>
<td className="mono" style={{ fontFamily: "'JetBrains Mono', monospace", color: exitPrice != null ? 'var(--text-dim)' : 'var(--text-muted)' }}>
{exitPrice != null ? `${exitPrice}¢` : <span className="badge bg-blue" style={{ fontSize: '0.65rem' }}>Open</span>}
</td>
<td className="mono" style={{ color: 'var(--text-dim)' }}>
{fmt(p.open_cost_cents)}
</td>
<td className="mono font-bold pnl-positive" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
{fmtPnl(p.realized_pnl_cents)}
</td>
<td className="mono pnl-positive">{roi}</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* Losers */}
<div className="card overflow-hidden" style={{ padding: 0 }}>
<div
className="flex items-center gap-3 px-4 py-3"
style={{
background: 'rgba(255,92,92,0.06)',
borderBottom: '1px solid rgba(255,92,92,0.2)',
borderLeft: '4px solid var(--red)',
}}
>
<span className="text-sm font-bold text-red" style={{ textShadow: '0 0 10px rgba(255,92,92,0.4)' }}>Losers</span>
<span className="badge bg-red">{losers.length}</span>
<div className="flex-1" />
<span className="mono font-bold pnl-negative">
{fmtPnl(losers.reduce((s, p) => s + p.realized_pnl_cents, 0))}
</span>
</div>
<table>
<thead>
<tr>
<th>Ticker</th>
<th>Market</th>
<th>Side</th>
<th>Contracts</th>
<th>Bought At</th>
<th>Sold At</th>
<th>Cost</th>
<th>Profit</th>
<th>ROI</th>
</tr>
</thead>
<tbody>
{losers.map(p => {
const roi =
p.open_cost_cents > 0
? `${((p.realized_pnl_cents / p.open_cost_cents) * 100).toFixed(0)}%`
: '—';
const lTickerSeed = p.ticker.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
const lCandles = genCandles(lTickerSeed + 300, 9, 'down');
const closedCount = parseInt(p.closed_trades) || 0;
const openCount = parseInt(p.open_trades) || 0;
const exitPrice = (closedCount > 0 && openCount === 0)
? calcExitPrice(p.avg_entry_price, p.realized_pnl_cents, parseInt(p.open_contracts) || parseInt(p.total_trades) || 1)
: null;
return (
<tr key={`${p.ticker}_${p.direction}`} className="position-losing" style={{ position: 'relative' }}>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<a
href={kalshiLink(p.ticker)}
target="_blank"
rel="noopener"
className="mono text-sm hover:underline"
style={{ color: 'var(--orange)' }}
>
{p.ticker}
</a>
<MiniCandlestick data={lCandles} width={64} height={26} />
</div>
</td>
<td className="text-xs max-w-[192px] truncate" style={{ color: 'var(--text-dim)' }}>
{p.title}
</td>
<td>
<span className={`badge ${sideBadge(p.direction)}`}>
{sideLabel(p.direction)}
</span>
</td>
<td className="mono">{p.open_contracts?.toLocaleString()}</td>
<td className="mono" style={{ color: 'var(--orange)', fontFamily: "'JetBrains Mono', monospace" }}>
{p.avg_entry_price}¢
</td>
<td className="mono" style={{ fontFamily: "'JetBrains Mono', monospace", color: exitPrice != null ? 'var(--text-dim)' : 'var(--text-muted)' }}>
{exitPrice != null ? `${exitPrice}¢` : <span className="badge bg-blue" style={{ fontSize: '0.65rem' }}>Open</span>}
</td>
<td className="mono" style={{ color: 'var(--text-dim)' }}>
{fmt(p.open_cost_cents)}
</td>
<td className="mono font-bold pnl-negative" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
{fmtPnl(p.realized_pnl_cents)}
</td>
<td className="mono pnl-negative">{roi}</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* Flat */}
{flat.length > 0 && (
<div className="card overflow-hidden" style={{ padding: 0 }}>
<div
className="flex items-center gap-3 px-4 py-3"
style={{
background: 'var(--surface-el)',
borderBottom: '1px solid var(--border)',
}}
>
<span className="text-sm font-bold" style={{ color: 'var(--text-dim)' }}>
Flat (No Realized P&L)
</span>
<span className="badge bg-orange">{flat.length}</span>
</div>
<table>
<thead>
<tr>
<th>Ticker</th>
<th>Market</th>
<th>Side</th>
<th>Contracts</th>
<th>Cost</th>
</tr>
</thead>
<tbody>
{flat.map(p => (
<tr key={`${p.ticker}_${p.direction}`}>
<td>
<a
href={kalshiLink(p.ticker)}
target="_blank"
rel="noopener"
className="mono text-sm hover:underline"
style={{ color: 'var(--orange)' }}
>
{p.ticker}
</a>
</td>
<td className="text-xs max-w-[192px] truncate" style={{ color: 'var(--text-dim)' }}>
{p.title}
</td>
<td>
<span className={`badge ${sideBadge(p.direction)}`}>
{sideLabel(p.direction)}
</span>
</td>
<td className="mono">{p.open_contracts?.toLocaleString()}</td>
<td className="mono" style={{ color: 'var(--text-dim)' }}>
{fmt(p.open_cost_cents)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</div>
);
}