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(); 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('all'); const [historyDateFrom, setHistoryDateFrom] = useState(''); const [historyDateTo, setHistoryDateTo] = useState(''); 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([]); const [dbLifecycleOrders, setDbLifecycleOrders] = useState([]); const [profiles, setProfiles] = useState([]); const [loading, setLoading] = useState(true); const HISTORY_PAGE_SIZE = 20; const orderSubTagByTrade = useMemo(() => { const byTrade = new Map(); 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(() => ( 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 (
); } return (
{/* Header Section */}

Audit Logs

Verified Execution & Performance History

{profiles.map(p => ( ))}
{!hasCanonicalLifecycle && (
Canonical lifecycle is unavailable{canonicalLoading ? ' (loading)' : ''}. History is using fallback ledger sources. {canonicalError ? ` ${canonicalError}` : ''}
)} {canonicalSnapshot?.diagnostics?.truncated && (
Canonical lifecycle snapshot is truncated ({canonicalSnapshot.diagnostics.orderRows} rows). Review a narrower scope for exact audit totals.
)} {/* Performance Metrics Bar */}
Total Trades {metrics.total}
Win Rate {metrics.winRate.toFixed(1)}%
Realized P&L = 0 ? 'text-green-400' : 'text-red-400'}`}> {metrics.netPnl >= 0 ? '+' : '-'}${Math.abs(metrics.netPnl).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
Profit Factor = 1 ? 'text-green-400' : 'text-red-400'}`}> {metrics.expectancy.profitFactor === null ? '-' : Number.isFinite(metrics.expectancy.profitFactor) ? metrics.expectancy.profitFactor.toFixed(2) : '∞'}
Expectancy / Trade = 0 ? 'text-green-400' : 'text-red-400'}`}> {metrics.expectancy.expectancyPerTrade >= 0 ? '+' : '-'}${Math.abs(metrics.expectancy.expectancyPerTrade).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} Avg win ${metrics.expectancy.avgWin.toFixed(2)} / Avg loss ${metrics.expectancy.avgLossAbs.toFixed(2)}
{/* Trade Ledger Table - Matched exactly with Active Orders Style */}
{sortedHistory.length === 0 ? ( ) : ( 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 ( )}) )}
Source Time Trade ID Sub-tag Asset Side Capital Used ($) Entry Exit P/L (%) Reason
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" /> 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" />
Filters and sorting are applied directly from table headers.
No transaction journal found.
{t.source === 'BOT' ? (t.profile_id ? (profiles.find(p => p.id === t.profile_id)?.name || 'BOT') : 'BOT') : 'MANUAL'} {(t.timestamp || t.created_at) ? new Date(t.timestamp || t.created_at!).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'} {t.trade_id || '-'} {compactTag(t.sub_tag || t.subTag)} {t.symbol} {t.side} {capitalUsed !== null ? `$${capitalUsed.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '-'} {Number.isFinite(Number(t.entry_price)) && Number(t.entry_price) > 0 ? `$${Number(t.entry_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '-'} {Number.isFinite(Number(t.exit_price)) && Number(t.exit_price) > 0 ? `$${Number(t.exit_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '-'}
{pnlValue >= 0 ? '+' : ''}{pnlPercentValue.toFixed(2)}%
${pnlValue.toFixed(2)}
{isLoss && (
Loss Alert
)}
{t.reason}
Showing {sortedHistory.length === 0 ? 0 : ((historyPage - 1) * HISTORY_PAGE_SIZE) + 1} -{Math.min(historyPage * HISTORY_PAGE_SIZE, sortedHistory.length)} of {sortedHistory.length}
Page {historyPage} / {historyTotalPages}
); };