learning_ai_invt_trdg/web/src/tabs/HistoryTab.tsx

676 lines
35 KiB
TypeScript

import { useEffect, useState, useMemo } from 'react';
import { useAuth } from '../components/AuthContext';
import { aggregateHistoryLedger, buildHistoryLedger } from '../lib/tradeHistoryLedger';
import {
type LifecycleOrderRow
} from '../lib/orderLifecycleLedger';
import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle';
import { fetchTradeHistory } from '../lib/tradeHistoryApi';
import { fetchPositionsBootstrap } from '../lib/positionsApi';
interface TradeRecord {
id: string;
timestamp: number | string;
symbol: string;
side: string;
size?: number;
entry_price: number;
exit_price: number;
pnl: number;
pnl_percent: number;
reason: string;
profile_id?: string;
stop_loss?: number;
take_profit?: number;
rules_metadata?: any;
created_at?: string;
trade_id?: string;
source?: 'BOT' | 'MANUAL';
sub_tag?: string;
subTag?: string;
}
interface Profile {
id: string;
name: string;
allocated_capital: number;
}
interface OrderLifecycleRecord extends LifecycleOrderRow {
trade_id?: string;
profile_id?: string;
sub_tag?: string;
timestamp?: number | string;
filled_at?: string;
created_at?: string;
}
// ─── NEW HISTORY LEDGER SIMPLIFIED ───────────────────────────────────────────────
interface HistoryTabProps {
botState?: any;
}
export interface HistoryDateBounds {
from: number | null;
to: number | null;
}
export const toTimestampMs = (value?: number | string, fallbackCreatedAt?: string): number => {
if (typeof value === 'number') return value;
if (typeof value === 'string') {
const parsedValue = new Date(value).getTime();
if (Number.isFinite(parsedValue)) return parsedValue;
}
if (fallbackCreatedAt) {
const parsedCreatedAt = new Date(fallbackCreatedAt).getTime();
if (Number.isFinite(parsedCreatedAt)) return parsedCreatedAt;
}
return 0;
};
export const parseDateStart = (value: string): number | null => {
if (!value) return null;
const parsed = new Date(`${value}T00:00:00`).getTime();
return Number.isFinite(parsed) ? parsed : null;
};
export const parseDateEnd = (value: string): number | null => {
if (!value) return null;
const parsed = new Date(`${value}T23:59:59.999`).getTime();
return Number.isFinite(parsed) ? parsed : null;
};
export const filterHistoryByProfileAndDate = (
history: TradeRecord[],
selectedProfileId: string,
bounds: HistoryDateBounds
) =>
history.filter((record) => {
if (selectedProfileId !== 'all' && record.profile_id !== selectedProfileId) return false;
const recordTimestamp = toTimestampMs(record.timestamp, record.created_at);
if (bounds.from !== null && recordTimestamp < bounds.from) return false;
if (bounds.to !== null && recordTimestamp > bounds.to) return false;
return true;
});
export const sortHistoryByTimestamp = (
records: TradeRecord[],
historySortDirection: 'desc' | 'asc'
) => {
const direction = historySortDirection === 'asc' ? 1 : -1;
return [...records].sort((a, b) => {
const aTimestamp = toTimestampMs(a.timestamp, a.created_at);
const bTimestamp = toTimestampMs(b.timestamp, b.created_at);
if (aTimestamp !== bTimestamp) {
return direction * (aTimestamp - bTimestamp);
}
return direction * String(a.id).localeCompare(String(b.id));
});
};
export const paginateHistory = (records: TradeRecord[], page: number, pageSize: number) => {
const startIndex = (page - 1) * pageSize;
return records.slice(startIndex, startIndex + pageSize);
};
interface ExpectancyMetrics {
tradeCount: number;
avgWin: number;
avgLossAbs: number;
profitFactor: number | null;
expectancyPerTrade: number;
}
const computeExpectancyMetrics = (records: TradeRecord[]): ExpectancyMetrics => {
const byLifecycle = new Map<string, number>();
for (const record of records) {
const tradeId = String(record.trade_id || '').trim();
const profileId = String(record.profile_id || '').trim() || 'global';
const lifecycleKey = tradeId
? `trade:${profileId}:${tradeId}`
: `row:${record.id}`;
const pnl = Number(record.pnl || 0);
const next = (byLifecycle.get(lifecycleKey) || 0) + (Number.isFinite(pnl) ? pnl : 0);
byLifecycle.set(lifecycleKey, next);
}
const tradePnls = Array.from(byLifecycle.values());
const wins = tradePnls.filter((value) => value > 0);
const losses = tradePnls.filter((value) => value < 0);
const grossWin = wins.reduce((sum, value) => sum + value, 0);
const grossLossAbs = Math.abs(losses.reduce((sum, value) => sum + value, 0));
const tradeCount = tradePnls.length;
const avgWin = wins.length > 0 ? grossWin / wins.length : 0;
const avgLossAbs = losses.length > 0 ? grossLossAbs / losses.length : 0;
const profitFactor = grossLossAbs > 0
? (grossWin / grossLossAbs)
: (grossWin > 0 ? Number.POSITIVE_INFINITY : null);
const expectancyPerTrade = tradeCount > 0
? (tradePnls.reduce((sum, value) => sum + value, 0) / tradeCount)
: 0;
return {
tradeCount,
avgWin,
avgLossAbs,
profitFactor,
expectancyPerTrade
};
};
const compactTag = (value?: string): string => {
const token = String(value || '').trim();
if (!token) return '-';
return token.length > 24 ? `${token.slice(0, 12)}...${token.slice(-8)}` : token;
};
export const HistoryTab = ({ botState }: HistoryTabProps) => {
const { user, profile } = useAuth();
const [selectedProfileId, setSelectedProfileId] = useState<string>('all');
const [historyDateFrom, setHistoryDateFrom] = useState<string>('');
const [historyDateTo, setHistoryDateTo] = useState<string>('');
const [historySortDirection, setHistorySortDirection] = useState<'desc' | 'asc'>('desc');
const [historyPage, setHistoryPage] = useState(1);
const {
snapshot: canonicalSnapshot,
loading: canonicalLoading,
error: canonicalError
} = useCanonicalLifecycle(selectedProfileId === 'all' ? undefined : selectedProfileId);
const [dbHistory, setDbHistory] = useState<TradeRecord[]>([]);
const [dbLifecycleOrders, setDbLifecycleOrders] = useState<OrderLifecycleRecord[]>([]);
const [profiles, setProfiles] = useState<Profile[]>([]);
const [loading, setLoading] = useState(true);
const HISTORY_PAGE_SIZE = 20;
const orderSubTagByTrade = useMemo(() => {
const byTrade = new Map<string, { tag: string; ts: number }>();
const setIfLatest = (tradeIdRaw: unknown, profileIdRaw: unknown, tagRaw: unknown, tsRaw: unknown) => {
const tradeId = String(tradeIdRaw || '').trim();
const profileId = String(profileIdRaw || '').trim();
const tag = String(tagRaw || '').trim();
if (!tradeId || !tag) return;
const timestampCandidate = typeof tsRaw === 'number'
? tsRaw
: new Date(String(tsRaw || '')).getTime();
const timestamp = Number.isFinite(timestampCandidate) ? timestampCandidate : 0;
const scopedKey = `${profileId || 'global'}|${tradeId}`;
const globalKey = `global|${tradeId}`;
const scopedExisting = byTrade.get(scopedKey);
if (!scopedExisting || timestamp >= scopedExisting.ts) {
byTrade.set(scopedKey, { tag, ts: timestamp });
}
const globalExisting = byTrade.get(globalKey);
if (!globalExisting || timestamp >= globalExisting.ts) {
byTrade.set(globalKey, { tag, ts: timestamp });
}
};
for (const row of dbLifecycleOrders) {
setIfLatest(
row.trade_id,
row.profile_id,
row.sub_tag,
row.filled_at ?? row.timestamp ?? row.created_at
);
}
for (const row of (canonicalSnapshot?.lifecycleRows || [])) {
setIfLatest(
row.tradeId,
row.profileId,
row.subTag,
row.lastEventAt
);
}
for (const order of (botState?.orders || [])) {
setIfLatest(
(order as any).trade_id || (order as any).tradeId,
(order as any).profileId || (order as any).profile_id,
(order as any).subTag || (order as any).sub_tag || (order as any).subtag,
(order as any).timestamp
);
}
return byTrade;
}, [canonicalSnapshot?.lifecycleRows, dbLifecycleOrders, botState?.orders]);
const canonicalLifecycleTrades = useMemo(() => {
if (canonicalSnapshot && !canonicalSnapshot.diagnostics?.truncated) {
return canonicalSnapshot.realizedTrades.map((trade) => ({
id: trade.id,
tradeId: trade.tradeId,
profileId: trade.profileId,
symbol: trade.symbol,
side: trade.side,
size: Number(trade.size || 0),
entryPrice: Number(trade.entryPrice || 0),
exitPrice: Number(trade.exitPrice || 0),
pnl: Number(trade.pnl || 0),
pnlPercent: Number(trade.pnlPercent || 0),
closedAtMs: Number(trade.closedAt || 0)
}));
}
return [];
}, [canonicalSnapshot]);
const canonicalHistory = useMemo<TradeRecord[]>(() => (
canonicalLifecycleTrades.map((trade) => ({
id: trade.id,
timestamp: trade.closedAtMs,
symbol: trade.symbol,
side: trade.side,
size: trade.size,
entry_price: trade.entryPrice,
exit_price: trade.exitPrice,
pnl: trade.pnl,
pnl_percent: trade.pnlPercent,
reason: 'Exchange Filled Lifecycle',
profile_id: trade.profileId,
created_at: trade.closedAtMs > 0 ? new Date(trade.closedAtMs).toISOString() : undefined,
trade_id: trade.tradeId,
source: 'BOT'
}))
), [canonicalLifecycleTrades]);
const hasCanonicalLifecycle = Boolean(canonicalSnapshot && canonicalSnapshot.lifecycleRows.length > 0);
// Merge DB history with real-time bot history
const history = useMemo(() => {
if (canonicalHistory.length > 0) {
return canonicalHistory.map((row) => {
const scopedTradeKey = `${row.profile_id || 'global'}|${row.trade_id || ''}`;
const globalTradeKey = `global|${row.trade_id || ''}`;
const derivedSubTag = row.trade_id
? (orderSubTagByTrade.get(scopedTradeKey)?.tag || orderSubTagByTrade.get(globalTradeKey)?.tag)
: undefined;
return {
...row,
sub_tag: derivedSubTag
};
});
}
return dbHistory.map((row) => {
const tradeId = String(row.trade_id || '').trim();
const profileId = String(row.profile_id || '').trim();
const scopedTradeKey = `${profileId || 'global'}|${tradeId}`;
const globalTradeKey = `global|${tradeId}`;
const derivedSubTag = tradeId
? (orderSubTagByTrade.get(scopedTradeKey)?.tag || orderSubTagByTrade.get(globalTradeKey)?.tag)
: undefined;
return ({
...row,
sub_tag: row.sub_tag || derivedSubTag
});
});
}, [canonicalHistory, dbHistory, orderSubTagByTrade]);
useEffect(() => {
if (!user) return;
const fetchData = async () => {
setLoading(true);
try {
const scope = profile?.role === 'admin' ? 'all' : 'user';
const [historyRows, positionsBootstrap] = await Promise.all([
fetchTradeHistory({ scope, limit: 5000 }),
fetchPositionsBootstrap({ scope, limit: 5000 }),
]);
const profData = (positionsBootstrap.profiles || []).map((item: any) => ({
id: String(item.id),
name: String(item.name || item.id || 'Unnamed Profile'),
allocated_capital: Number(item.allocated_capital || 0)
}));
setDbHistory((historyRows as TradeRecord[]) || []);
setDbLifecycleOrders((positionsBootstrap.orders as OrderLifecycleRecord[]) || []);
setProfiles((profData as Profile[]) || []);
} catch (err) {
console.error("Unexpected error:", err);
}
setLoading(false);
};
fetchData();
}, [user, profile?.role]);
const historyDateBounds = useMemo(() => ({
from: parseDateStart(historyDateFrom),
to: parseDateEnd(historyDateTo)
}), [historyDateFrom, historyDateTo]);
const filteredHistory = useMemo(() => {
return filterHistoryByProfileAndDate(history, selectedProfileId, historyDateBounds);
}, [history, historyDateBounds, selectedProfileId]);
const sortedHistory = useMemo(() => {
return sortHistoryByTimestamp(filteredHistory, historySortDirection);
}, [filteredHistory, historySortDirection]);
useEffect(() => {
setHistoryPage(1);
}, [selectedProfileId, historyDateFrom, historyDateTo]);
const historyTotalPages = useMemo(
() => Math.max(1, Math.ceil(sortedHistory.length / HISTORY_PAGE_SIZE)),
[sortedHistory.length, HISTORY_PAGE_SIZE]
);
useEffect(() => {
setHistoryPage((current) => Math.min(current, historyTotalPages));
}, [historyTotalPages]);
const paginatedHistory = useMemo(() => {
return paginateHistory(sortedHistory, historyPage, HISTORY_PAGE_SIZE);
}, [sortedHistory, historyPage, HISTORY_PAGE_SIZE]);
// Aggregate Metrics for the metrics bar
const metrics = useMemo(() => {
const aggregate = aggregateHistoryLedger(
buildHistoryLedger({
dbRows: filteredHistory,
includeRealtime: false
})
);
const expectancy = computeExpectancyMetrics(filteredHistory);
return {
total: aggregate.tradeCount,
winRate: aggregate.winRate,
netPnl: aggregate.totalPnl,
expectancy
};
}, [filteredHistory]);
if (loading) {
return (
<div className="flex items-center justify-center py-40">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#00ff88]"></div>
</div>
);
}
return (
<div className="history-tab max-w-[1400px] mx-auto px-6 space-y-8 pb-20 animate-in fade-in duration-500">
{/* Header Section */}
<header className="pt-8 space-y-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="space-y-1">
<h1 className="text-4xl font-black text-white tracking-tight uppercase">Audit Logs</h1>
<p className="text-gray-400 font-medium text-[10px] uppercase tracking-widest opacity-60">Verified Execution & Performance History</p>
</div>
<div className="flex gap-2 bg-white/5 p-1 rounded-xl shadow-inner border border-white/5">
<button
onClick={() => setSelectedProfileId('all')}
className={`px-6 py-2 text-[10px] font-black uppercase tracking-widest rounded-lg transition-all ${selectedProfileId === 'all'
? 'bg-[#00ff88] text-black shadow-[0_0_20px_rgba(0,255,136,0.2)]'
: 'text-gray-400 hover:text-white hover:bg-white/5'
}`}
>
Global
</button>
{profiles.map(p => (
<button
key={p.id}
onClick={() => setSelectedProfileId(p.id)}
className={`px-6 py-2 text-[10px] font-black uppercase tracking-widest rounded-lg transition-all ${selectedProfileId === p.id
? 'bg-[#00ff88] text-black shadow-[0_0_20px_rgba(0,255,136,0.2)]'
: 'text-gray-400 hover:text-white hover:bg-white/5'
}`}
>
{p.name}
</button>
))}
</div>
</div>
{!hasCanonicalLifecycle && (
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 text-amber-300 text-xs px-3 py-2">
Canonical lifecycle is unavailable{canonicalLoading ? ' (loading)' : ''}. History is using fallback ledger sources.
{canonicalError ? ` ${canonicalError}` : ''}
</div>
)}
{canonicalSnapshot?.diagnostics?.truncated && (
<div className="rounded-xl border border-red-500/30 bg-red-500/10 text-red-300 text-xs px-3 py-2">
Canonical lifecycle snapshot is truncated ({canonicalSnapshot.diagnostics.orderRows} rows). Review a narrower scope for exact audit totals.
</div>
)}
{/* 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-[#00ff88]">{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>
</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">
<select
value={selectedProfileId}
onChange={(e) => setSelectedProfileId(e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded px-2 py-1 text-[10px] font-semibold text-gray-200 focus:outline-none focus:ring-2 focus:ring-[#00ff88]/40"
>
<option value="all">All Profiles</option>
{profiles.map((profileOption) => (
<option key={profileOption.id} value={profileOption.id}>
{profileOption.name}
</option>
))}
</select>
</th>
<th className="px-6 py-2">
<div className="flex flex-col gap-1">
<input
type="date"
value={historyDateFrom}
onChange={(e) => setHistoryDateFrom(e.target.value)}
className="bg-white/5 border border-white/10 rounded px-2 py-1 text-[10px] text-gray-200 focus:outline-none focus:ring-2 focus:ring-[#00ff88]/40"
/>
<input
type="date"
value={historyDateTo}
onChange={(e) => setHistoryDateTo(e.target.value)}
className="bg-white/5 border border-white/10 rounded px-2 py-1 text-[10px] text-gray-200 focus:outline-none focus:ring-2 focus:ring-[#00ff88]/40"
/>
<div className="flex items-center gap-2">
<button
onClick={() => {
setHistoryDateFrom('');
setHistoryDateTo('');
}}
className="px-2 py-1 rounded border border-white/10 text-[9px] font-bold uppercase tracking-wider text-gray-300 hover:bg-white/5 transition-colors"
>
Clear
</button>
<button
onClick={() => setHistorySortDirection((current) => current === 'desc' ? 'asc' : 'desc')}
className="px-2 py-1 rounded border border-white/10 text-[9px] font-bold uppercase tracking-wider text-gray-300 hover:bg-white/5 transition-colors"
>
{historySortDirection === 'desc' ? 'Newest' : 'Oldest'}
</button>
</div>
</div>
</th>
<th colSpan={9} className="px-6 py-2 text-[10px] text-gray-500 font-medium">
Filters and sorting are applied directly from table headers.
</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{sortedHistory.length === 0 ? (
<tr>
<td colSpan={11} className="px-6 py-12 text-center text-gray-500 italic text-sm">
No transaction journal found.
</td>
</tr>
) : (
paginatedHistory.map((t) => {
const pnlValue = Number(t.pnl ?? 0);
const pnlPercentValue = Number.isFinite(Number(t.pnl_percent)) ? Number(t.pnl_percent) : 0;
const isLoss = pnlValue < 0;
const normalizedSize = Number(t.size || 0);
const normalizedEntry = Number(t.entry_price || 0);
const capitalUsed = (normalizedSize > 0 && normalizedEntry > 0)
? Math.abs(normalizedSize * normalizedEntry)
: null;
return (
<tr
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`}
>
<td className="px-6 py-4">
<span className={`px-2 py-0.5 rounded text-[10px] font-black tracking-tighter ${t.source === 'BOT' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/20' : 'bg-orange-500/20 text-orange-400 border border-orange-500/20'}`}>
{t.source === 'BOT'
? (t.profile_id ? (profiles.find(p => p.id === t.profile_id)?.name || 'BOT') : 'BOT')
: 'MANUAL'}
</span>
</td>
<td className="px-6 py-4 text-[10px] text-gray-500 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">
{t.trade_id || '-'}
</td>
<td className="px-6 py-4 text-[9px] text-gray-500 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">
<span className={`text-[10px] font-black ${t.side === 'BUY' ? 'text-green-400' : t.side === 'SELL' ? 'text-red-400' : 'text-gray-400'}`}>
{t.side}
</span>
</td>
<td className="px-6 py-4 text-right text-xs font-mono text-cyan-300">
{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">
{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">
{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">
<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">
${pnlValue.toFixed(2)}
</div>
{isLoss && (
<div className="mt-1 inline-flex items-center px-1.5 py-0.5 rounded border border-red-500/40 bg-red-500/20 text-[8px] font-black uppercase tracking-wider text-red-200">
Loss Alert
</div>
)}
</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">
{t.reason}
</span>
</td>
</tr>
)})
)}
</tbody>
<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">
<span>
Showing {sortedHistory.length === 0 ? 0 : ((historyPage - 1) * HISTORY_PAGE_SIZE) + 1}
-{Math.min(historyPage * HISTORY_PAGE_SIZE, sortedHistory.length)} of {sortedHistory.length}
</span>
<div className="flex items-center gap-2">
<button
onClick={() => setHistoryPage((current) => Math.max(1, current - 1))}
disabled={historyPage === 1}
className="px-3 py-1.5 rounded border border-white/10 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-white/5 transition-colors"
>
Prev
</button>
<span className="text-[10px] uppercase tracking-widest text-gray-500">
Page {historyPage} / {historyTotalPages}
</span>
<button
onClick={() => setHistoryPage((current) => Math.min(historyTotalPages, current + 1))}
disabled={historyPage >= historyTotalPages}
className="px-3 py-1.5 rounded border border-white/10 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-white/5 transition-colors"
>
Next
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
);
};