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