refactor(ui): standardize history ledger surfaces
This commit is contained in:
parent
0c8d3cf912
commit
ff17c635e3
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user