refactor(ui): standardize history ledger surfaces

This commit is contained in:
Saravana Achu Mac 2026-05-09 02:22:10 -07:00
parent 0c8d3cf912
commit ff17c635e3

View File

@ -7,7 +7,20 @@ import {
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
import { fetchTradeHistory } from '../lib/tradeHistoryApi';
import { fetchPositionsBootstrap } from '../lib/positionsApi';
import { AlertBanner, Badge, Button, Input, Select } from '../components/ui/Primitives';
import {
AlertBanner,
Badge,
Button,
DataTable,
DataTableBody,
DataTableCell,
DataTableHead,
DataTableHeader,
DataTableRow,
MetricCard,
Input,
Select,
} from '../components/ui/Primitives';
interface TradeRecord {
@ -449,63 +462,53 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
</AlertBanner>
)}
{/* Performance Metrics Bar */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-6 pt-4">
<div className="bg-white/[0.02] border border-white/5 p-6 rounded-2xl flex flex-col gap-1 backdrop-blur-sm">
<span className="text-gray-500 text-[10px] font-black uppercase tracking-widest">Total Trades</span>
<span className="text-3xl font-black text-white">{metrics.total}</span>
</div>
<div className="bg-white/[0.02] border border-white/5 p-6 rounded-2xl flex flex-col gap-1 backdrop-blur-sm">
<span className="text-gray-500 text-[10px] font-black uppercase tracking-widest">Win Rate</span>
<span className="text-3xl font-black text-[var(--bl-success)]">{metrics.winRate.toFixed(1)}%</span>
</div>
<div className="bg-white/[0.02] border border-white/5 p-6 rounded-2xl flex flex-col gap-1 backdrop-blur-sm">
<span className="text-gray-500 text-[10px] font-black uppercase tracking-widest">Realized P&L</span>
<span className={`text-3xl font-black ${metrics.netPnl >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{metrics.netPnl >= 0 ? '+' : '-'}${Math.abs(metrics.netPnl).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>
<div className="bg-white/[0.02] border border-white/5 p-6 rounded-2xl flex flex-col gap-1 backdrop-blur-sm">
<span className="text-gray-500 text-[10px] font-black uppercase tracking-widest">Profit Factor</span>
<span className={`text-3xl font-black ${metrics.expectancy.profitFactor === null ? 'text-gray-400' : metrics.expectancy.profitFactor >= 1 ? 'text-green-400' : 'text-red-400'}`}>
{metrics.expectancy.profitFactor === null
? '-'
: Number.isFinite(metrics.expectancy.profitFactor)
? metrics.expectancy.profitFactor.toFixed(2)
: '∞'}
</span>
</div>
<div className="bg-white/[0.02] border border-white/5 p-6 rounded-2xl flex flex-col gap-1 backdrop-blur-sm">
<span className="text-gray-500 text-[10px] font-black uppercase tracking-widest">Expectancy / Trade</span>
<span className={`text-3xl font-black ${metrics.expectancy.expectancyPerTrade >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{metrics.expectancy.expectancyPerTrade >= 0 ? '+' : '-'}${Math.abs(metrics.expectancy.expectancyPerTrade).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
<span className="text-[10px] text-gray-500">
Avg win ${metrics.expectancy.avgWin.toFixed(2)} / Avg loss ${metrics.expectancy.avgLossAbs.toFixed(2)}
</span>
</div>
<div className="grid grid-cols-1 gap-4 pt-4 md:grid-cols-5">
<MetricCard label="Total Trades" value={metrics.total} />
<MetricCard
label="Win Rate"
value={`${metrics.winRate.toFixed(1)}%`}
tone="success"
/>
<MetricCard
label="Realized P&L"
value={`${metrics.netPnl >= 0 ? '+' : '-'}$${Math.abs(metrics.netPnl).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`}
tone={metrics.netPnl >= 0 ? 'success' : 'danger'}
/>
<MetricCard
label="Profit Factor"
value={metrics.expectancy.profitFactor === null
? '-'
: Number.isFinite(metrics.expectancy.profitFactor)
? metrics.expectancy.profitFactor.toFixed(2)
: '∞'}
tone={metrics.expectancy.profitFactor === null ? 'neutral' : metrics.expectancy.profitFactor >= 1 ? 'success' : 'danger'}
/>
<MetricCard
label="Expectancy / Trade"
value={`${metrics.expectancy.expectancyPerTrade >= 0 ? '+' : '-'}$${Math.abs(metrics.expectancy.expectancyPerTrade).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`}
helper={`Avg win $${metrics.expectancy.avgWin.toFixed(2)} / Avg loss $${metrics.expectancy.avgLossAbs.toFixed(2)}`}
tone={metrics.expectancy.expectancyPerTrade >= 0 ? 'success' : 'danger'}
/>
</div>
</header>
{/* Trade Ledger Table - Matched exactly with Active Orders Style */}
<div className="table-container bg-black/20 border border-white/5 rounded-2xl overflow-hidden">
<table className="pro-table w-full">
<thead>
<tr className="bg-white/[0.02] text-left">
<th className="px-6 py-4 text-[10px] font-black uppercase tracking-widest text-gray-500">Source</th>
<th className="px-6 py-4 text-[10px] font-black uppercase tracking-widest text-gray-500">Time</th>
<th className="px-6 py-4 text-[10px] font-black uppercase tracking-widest text-gray-500">Trade ID</th>
<th className="px-6 py-4 text-[10px] font-black uppercase tracking-widest text-gray-500">Sub-tag</th>
<th className="px-6 py-4 text-[10px] font-black uppercase tracking-widest text-gray-500">Asset</th>
<th className="px-6 py-4 text-[10px] font-black uppercase tracking-widest text-gray-500">Side</th>
<th className="px-6 py-4 text-[10px] font-black uppercase tracking-widest text-gray-500 text-right">Capital Used ($)</th>
<th className="px-6 py-4 text-[10px] font-black uppercase tracking-widest text-gray-500 text-right">Entry</th>
<th className="px-6 py-4 text-[10px] font-black uppercase tracking-widest text-gray-500 text-right">Exit</th>
<th className="px-6 py-4 text-[10px] font-black uppercase tracking-widest text-gray-500 text-right">P/L (%)</th>
<th className="px-6 py-4 text-[10px] font-black uppercase tracking-widest text-gray-500">Reason</th>
</tr>
<tr className="bg-white/[0.015] text-left align-top">
<th className="px-6 py-2">
<DataTable className="min-w-[1180px]">
<DataTableHeader>
<DataTableRow>
<DataTableHead>Source</DataTableHead>
<DataTableHead>Time</DataTableHead>
<DataTableHead>Trade ID</DataTableHead>
<DataTableHead>Sub-tag</DataTableHead>
<DataTableHead>Asset</DataTableHead>
<DataTableHead>Side</DataTableHead>
<DataTableHead className="text-right">Capital Used ($)</DataTableHead>
<DataTableHead className="text-right">Entry</DataTableHead>
<DataTableHead className="text-right">Exit</DataTableHead>
<DataTableHead className="text-right">P/L (%)</DataTableHead>
<DataTableHead>Reason</DataTableHead>
</DataTableRow>
<DataTableRow className="align-top">
<DataTableHead className="py-2 normal-case tracking-normal">
<Select
value={selectedProfileId}
onChange={(e) => setSelectedProfileId(e.target.value)}
@ -519,8 +522,8 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
})),
]}
/>
</th>
<th className="px-6 py-2">
</DataTableHead>
<DataTableHead className="py-2 normal-case tracking-normal">
<div className="flex flex-col gap-1">
<Input
type="date"
@ -560,19 +563,19 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
</Button>
</div>
</div>
</th>
<th colSpan={9} className="px-6 py-2 text-[10px] text-gray-500 font-medium">
</DataTableHead>
<DataTableHead colSpan={9} className="py-2 text-xs font-medium normal-case tracking-normal">
Filters and sorting are applied directly from table headers.
</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
</DataTableHead>
</DataTableRow>
</DataTableHeader>
<DataTableBody>
{sortedHistory.length === 0 ? (
<tr>
<td colSpan={11} className="px-6 py-12 text-center text-gray-500 italic text-sm">
<DataTableRow>
<DataTableCell colSpan={11} className="py-12 text-center italic text-[var(--bl-text-secondary)]">
No transaction journal found.
</td>
</tr>
</DataTableCell>
</DataTableRow>
) : (
paginatedHistory.map((t) => {
const pnlValue = Number(t.pnl ?? 0);
@ -585,48 +588,48 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
: null;
return (
<tr
<DataTableRow
key={`${t.id}-${t.trade_id || ''}`}
className={`${isLoss ? 'bg-red-500/[0.06] border-l-2 border-l-red-500/50' : ''} hover:bg-white/[0.02] transition-colors`}
className={isLoss ? 'border-l-2 border-l-[var(--bl-danger)] bg-[color-mix(in_oklab,var(--bl-danger)_8%,transparent)]' : ''}
>
<td className="px-6 py-4">
<DataTableCell>
<Badge variant={historySourceBadgeVariant(t.source)} size="sm">
{t.source === 'BOT'
? (t.profile_id ? (profiles.find(p => p.id === t.profile_id)?.name || 'BOT') : 'BOT')
: 'MANUAL'}
</Badge>
</td>
<td className="px-6 py-4 text-[10px] text-gray-500 font-mono">
</DataTableCell>
<DataTableCell className="text-xs text-[var(--bl-text-secondary)] font-mono">
{(t.timestamp || t.created_at) ? new Date(t.timestamp || t.created_at!).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'}
</td>
<td className="px-6 py-4 text-[10px] text-gray-500 font-mono">
</DataTableCell>
<DataTableCell className="text-xs text-[var(--bl-text-secondary)] font-mono">
{t.trade_id || '-'}
</td>
<td className="px-6 py-4 text-[9px] text-gray-500 font-mono" title={t.sub_tag || t.subTag || ''}>
</DataTableCell>
<DataTableCell className="text-xs text-[var(--bl-text-secondary)] font-mono" title={t.sub_tag || t.subTag || ''}>
{compactTag(t.sub_tag || t.subTag)}
</td>
<td className="px-6 py-4 font-mono font-bold text-white">{t.symbol}</td>
<td className="px-6 py-4">
</DataTableCell>
<DataTableCell className="font-mono font-semibold">{t.symbol}</DataTableCell>
<DataTableCell>
<Badge variant={historySideBadgeVariant(t.side)} size="sm">
{t.side}
</Badge>
</td>
<td className="px-6 py-4 text-right text-xs font-mono text-cyan-300">
</DataTableCell>
<DataTableCell className="text-right text-xs font-mono text-[var(--bl-info,var(--bl-accent))]">
{capitalUsed !== null
? `$${capitalUsed.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
: '-'}
</td>
<td className="px-6 py-4 text-right text-xs font-mono text-gray-400">
</DataTableCell>
<DataTableCell className="text-right text-xs font-mono text-[var(--bl-text-secondary)]">
{Number.isFinite(Number(t.entry_price)) && Number(t.entry_price) > 0
? `$${Number(t.entry_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
: '-'}
</td>
<td className="px-6 py-4 text-right text-xs font-mono text-white font-bold">
</DataTableCell>
<DataTableCell className="text-right text-xs font-mono font-semibold">
{Number.isFinite(Number(t.exit_price)) && Number(t.exit_price) > 0
? `$${Number(t.exit_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
: '-'}
</td>
<td className="px-6 py-4 text-right">
</DataTableCell>
<DataTableCell className="text-right">
<div className={`text-xs font-mono font-black ${isLoss ? 'text-red-300' : 'text-green-400'}`}>
{pnlValue >= 0 ? '+' : ''}{pnlPercentValue.toFixed(2)}%
<div className="text-[10px] opacity-60">
@ -638,20 +641,20 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
</Badge>
)}
</div>
</td>
<td className="px-6 py-4">
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-tighter truncate max-w-[150px] inline-block">
</DataTableCell>
<DataTableCell>
<span className="inline-block max-w-[150px] truncate text-xs font-semibold text-[var(--bl-text-secondary)]">
{t.reason}
</span>
</td>
</tr>
</DataTableCell>
</DataTableRow>
)})
)}
</tbody>
</DataTableBody>
<tfoot>
<tr className="bg-white/[0.015]">
<td colSpan={11} className="px-6 py-3">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-3 text-xs text-gray-400">
<DataTableRow>
<DataTableCell colSpan={11}>
<div className="flex flex-col gap-3 text-xs text-[var(--bl-text-secondary)] md:flex-row md:items-center md:justify-between">
<span>
Showing {sortedHistory.length === 0 ? 0 : ((historyPage - 1) * HISTORY_PAGE_SIZE) + 1}
-{Math.min(historyPage * HISTORY_PAGE_SIZE, sortedHistory.length)} of {sortedHistory.length}
@ -680,11 +683,10 @@ export const HistoryTab = ({ botState }: HistoryTabProps) => {
</Button>
</div>
</div>
</td>
</tr>
</DataTableCell>
</DataTableRow>
</tfoot>
</table>
</div>
</DataTable>
</div>
);
};