import { useEffect, useMemo, useState } from 'react'; import type { ChangeEvent } from 'react'; import type { BotState } from '../hooks/useWebSocket'; import { getPlatformAccessToken } from '../lib/authSession'; import { tradingRuntime } from '../lib/runtime'; import { useAuth } from '../components/AuthContext'; import { createRequestId } from '../../../shared/request-id.js'; import { Layers, ListFilter, Link2, GitBranch, AlertTriangle, Lock, RefreshCw, CheckCircle, XCircle } from 'lucide-react'; import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle'; import { fetchPositionsBootstrap } from '../lib/positionsApi'; import { Button, Input, Select } from '../components/ui/Primitives'; interface PositionsTabProps { botState: BotState; onManageHolding?: (position: HybridPosition, action: 'open-plan' | 'create-exit-plan') => void; } type SimplePlanMeta = { entryId?: string; holdingMode: 'short_term' | 'long_term'; automationState?: string | null; }; interface HybridPosition { source: 'BOT' | 'MANUAL'; id: string; symbol: string; side: 'BUY' | 'SELL'; size: number; entryPrice: number; currentPrice?: number | null; pnl?: number | null; pnlPercent?: number | null; stopLoss?: number; takeProfit?: number; profileId?: string; profileName?: string; tradeId?: string; planMode?: 'short_term' | 'long_term'; planState?: string | null; planEntryId?: string; } function inferAutomationStateFromSimpleEvent(message: string): string | null { const normalized = String(message || '').trim().toLowerCase(); if (!normalized) return null; if (normalized.includes('entry submitted')) return 'entry_submitted'; if (normalized.includes('entry filled')) return 'holding_managed'; if (normalized.includes('exit submitted')) return 'exit_submitted'; if (normalized.includes('setup completed')) return 'closed'; if (normalized.includes('entry did not fill')) return 'armed'; if (normalized.includes('exit did not complete')) return 'holding_managed'; if (normalized.includes('exit partially filled')) return 'holding_managed'; return null; } interface Profile { id: string; name: string; } interface RawHistoryRecord { trade_id?: string; profile_id?: string; } type OrderSource = 'BOT' | 'MANUAL'; type OrderAction = 'ENTRY' | 'EXIT'; interface RawOrderRecord { id?: string; order_id?: string; profile_id?: string; profileId?: string; symbol?: string; type?: string; side?: string; qty?: number; quantity?: number; price?: number; status?: string; timestamp?: number | string; created_at?: string; trade_id?: string; tradeId?: string; action?: string; source?: OrderSource; stop_loss?: number; take_profit?: number; stopLoss?: number; takeProfit?: number; subTag?: string; subtag?: string; sub_tag?: string; } interface NormalizedOrder { id: string; orderId?: string; symbol: string; type: string; side: 'BUY' | 'SELL'; qty: number; price: number; status: string; timestamp: number; profileId?: string; tradeId?: string; action?: OrderAction; source: OrderSource; stopLoss?: number; takeProfit?: number; subTag?: string; } interface LifecycleTrace { traceKey: string; tradeId: string; profileId?: string; profileName: string; symbol: string; side: 'BUY' | 'SELL'; source: OrderSource; entryOrder?: NormalizedOrder; exitOrders: NormalizedOrder[]; orderedEvents: NormalizedOrder[]; entryFilledQty: number; exitFilledQty: number; openQty: number; entryAvgPrice: number; entryUsedUsd: number; state: 'OPEN' | 'PARTIAL_EXIT' | 'CLOSED' | 'ORPHAN_EXIT' | 'EXIT_PENDING'; stateReason: string; lastTimestamp: number; hasHistoryMatch: boolean; hasCancel: boolean; } export const normalizeSide = (side?: string): 'BUY' | 'SELL' => { const value = (side || '').toUpperCase(); return value === 'SELL' || value === 'SHORT' ? 'SELL' : 'BUY'; }; export const normalizeAction = (action?: string): OrderAction | undefined => { const value = (action || '').toUpperCase(); if (value === 'ENTRY' || value === 'EXIT') return value; return undefined; }; export const normalizeSource = (source?: string, profileId?: string): OrderSource => { const value = (source || '').toUpperCase(); if (value === 'BOT' || value === 'MANUAL') return value as OrderSource; return profileId ? 'BOT' : 'MANUAL'; }; export const toEpoch = (value?: number | string): number => { if (typeof value === 'number') return value; if (typeof value === 'string') { const parsed = new Date(value).getTime(); return Number.isFinite(parsed) ? parsed : 0; } 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 toPositiveNumber = (value: unknown): number => { const num = Number(value); return Number.isFinite(num) && num > 0 ? num : 0; }; export const normalizeLifecycleSymbolToken = (symbol?: string): string => { const raw = String(symbol || '').trim().toUpperCase(); if (!raw) return ''; const compact = raw.replace(/[^A-Z0-9]/g, ''); if (!compact) return ''; if (compact.endsWith('USDT')) { return `${compact.slice(0, -4)}USD`; } if (compact.endsWith('USDC')) { return `${compact.slice(0, -4)}USD`; } return compact; }; export const symbolsMatchForLifecycle = (left?: string, right?: string): boolean => { const leftToken = normalizeLifecycleSymbolToken(left); const rightToken = normalizeLifecycleSymbolToken(right); return !!leftToken && !!rightToken && leftToken === rightToken; }; export const formatDisplayQty = (value: number): string => { const qty = Number(value); if (!Number.isFinite(qty)) return '0'; const text = qty.toFixed(8).replace(/\.?0+$/, ''); return text || '0'; }; export const hasFiniteNumber = (value: unknown): value is number => { return typeof value === 'number' && Number.isFinite(value); }; export const isLifecycleFilledStatus = (status?: string): boolean => { const normalized = (status || '').toLowerCase().replace(/-/g, '_'); return normalized === 'filled' || normalized === 'partially_filled'; }; export const isPendingLikeStatus = (status?: string): boolean => { const normalized = (status || '').toLowerCase().replace(/-/g, '_'); return normalized === 'pending_new' || normalized === 'pending' || normalized === 'accepted' || normalized === 'new'; }; export const statusRank = (status?: string): number => { const normalized = (status || '').toLowerCase().replace(/-/g, '_'); if (normalized === 'filled') return 6; if (normalized === 'partially_filled') return 5; if (normalized === 'canceled' || normalized === 'cancelled' || normalized === 'rejected' || normalized === 'expired') return 4; if (normalized === 'pending_new' || normalized === 'pending' || normalized === 'accepted' || normalized === 'new') return 2; if (normalized === 'unknown') return 1; return 2; }; export const pickMostReliableStatus = (base: NormalizedOrder, incoming: NormalizedOrder): string => { const baseStatus = (base.status || '').toLowerCase(); const incomingStatus = (incoming.status || '').toLowerCase(); if (!baseStatus) return incomingStatus || 'unknown'; if (!incomingStatus) return baseStatus || 'unknown'; const baseRank = statusRank(baseStatus); const incomingRank = statusRank(incomingStatus); if (baseRank !== incomingRank) { return incomingRank > baseRank ? incomingStatus : baseStatus; } return incoming.timestamp >= base.timestamp ? incomingStatus : baseStatus; }; export const normalizeOrder = (record: RawOrderRecord, fallbackSource?: OrderSource): NormalizedOrder | null => { const orderId = record.order_id || record.id; const profileId = record.profileId || record.profile_id; const symbol = record.symbol || ''; if (!orderId || !symbol) return null; return { id: orderId, orderId, symbol, type: record.type || 'Market', side: normalizeSide(record.side), qty: toPositiveNumber(record.qty) || toPositiveNumber(record.quantity), price: toPositiveNumber(record.price), status: (record.status || 'unknown').toLowerCase(), timestamp: toEpoch(record.timestamp || record.created_at), profileId, tradeId: record.trade_id || record.tradeId, action: normalizeAction(record.action), source: normalizeSource(record.source || fallbackSource, profileId), stopLoss: toPositiveNumber(record.stop_loss) || toPositiveNumber(record.stopLoss), takeProfit: toPositiveNumber(record.take_profit) || toPositiveNumber(record.takeProfit), subTag: String(record.subTag || record.subtag || record.sub_tag || '').trim() || undefined }; }; const SourceTruthVariants: Record<'exchange' | 'db' | 'reconciled' | 'unknown', { label: string; className: string }> = { exchange: { label: 'Exchange', className: 'truth-pill exchange' }, db: { label: 'DB', className: 'truth-pill db' }, reconciled: { label: 'Reconciled', className: 'truth-pill reconciled' }, unknown: { label: 'Unknown', className: 'truth-pill unknown' } }; const getTruthSourceForPosition = (entryOrder?: NormalizedOrder, canonicalAvailable: boolean = true) => { if (!entryOrder) return SourceTruthVariants.db; const status = entryOrder.status || ''; if (status.includes('pending') || status.includes('new') || status.includes('accepted')) { return SourceTruthVariants.exchange; } if (!canonicalAvailable) { return SourceTruthVariants.db; } return SourceTruthVariants.reconciled; }; const getTruthSourceForOrder = (order: NormalizedOrder, historyKeys: Set, canonicalAvailable: boolean = true) => { const status = order.status || ''; if (status.includes('pending') || status.includes('new') || status.includes('accepted')) { return SourceTruthVariants.exchange; } if (!canonicalAvailable) { return SourceTruthVariants.db; } const scopedKey = `${order.profileId || 'global'}|${order.tradeId}`; if (order.tradeId && (historyKeys.has(scopedKey) || historyKeys.has(`global|${order.tradeId}`))) { return SourceTruthVariants.reconciled; } return SourceTruthVariants.db; }; export const orderKey = (order: NormalizedOrder): string => `id:${order.orderId || order.id}`; export const mergeOrders = (base: NormalizedOrder, incoming: NormalizedOrder): NormalizedOrder => { const newer = incoming.timestamp >= base.timestamp ? incoming : base; const older = newer === incoming ? base : incoming; return { id: newer.id || older.id, orderId: newer.orderId || older.orderId, symbol: newer.symbol || older.symbol, type: newer.type || older.type, side: newer.side || older.side, qty: newer.qty > 0 ? newer.qty : older.qty, price: newer.price > 0 ? newer.price : older.price, status: pickMostReliableStatus(base, incoming), timestamp: Math.max(base.timestamp, incoming.timestamp), profileId: newer.profileId || older.profileId, tradeId: newer.tradeId || older.tradeId, action: newer.action || older.action, source: newer.source || older.source, stopLoss: newer.stopLoss || older.stopLoss, takeProfit: newer.takeProfit || older.takeProfit, subTag: newer.subTag || older.subTag }; }; export const assignLifecycleTradeIds = ( orders: NormalizedOrder[], livePositions: BotState['positions'] ): NormalizedOrder[] => { const positionTradeIds = new Map(); for (const pos of livePositions || []) { if (!pos?.tradeId) continue; const key = `${pos.profileId || 'global'}|${pos.symbol}|${pos.side}`; positionTradeIds.set(key, pos.tradeId); } const queuesByProfileSymbol = new Map(); const entrySideByScopedTradeId = new Map(); const getQueue = (key: string) => { let queue = queuesByProfileSymbol.get(key); if (!queue) { queue = { BUY: [], SELL: [], counter: 0 }; queuesByProfileSymbol.set(key, queue); } return queue; }; const enriched = [...orders] .map((order) => ({ ...order })) .sort((a, b) => (a.timestamp - b.timestamp) || a.id.localeCompare(b.id)); for (const order of enriched) { const profileKey = order.profileId || 'global'; const queueKey = `${profileKey}|${order.symbol}`; const queue = getQueue(queueKey); const scopedTradeKey = (tradeId: string) => `${profileKey}|${tradeId}`; const side = order.side; const oppositeSide: 'BUY' | 'SELL' = side === 'BUY' ? 'SELL' : 'BUY'; const explicitAction = order.action; let resolvedAction: OrderAction | undefined = explicitAction; if (!order.tradeId) { const positionKey = `${profileKey}|${order.symbol}|${side}`; const positionTradeId = positionTradeIds.get(positionKey); if (positionTradeId && (explicitAction === 'ENTRY' || !explicitAction)) { order.tradeId = positionTradeId; resolvedAction = resolvedAction || 'ENTRY'; } } if (order.tradeId && !resolvedAction) { const scopedKey = scopedTradeKey(order.tradeId); const knownEntrySide = entrySideByScopedTradeId.get(scopedKey); if (knownEntrySide) { resolvedAction = side === knownEntrySide ? 'ENTRY' : 'EXIT'; } else { const queuedOnSameSide = queue[side].includes(order.tradeId); const queuedOnOppositeSide = queue[oppositeSide].includes(order.tradeId); resolvedAction = queuedOnOppositeSide && !queuedOnSameSide ? 'EXIT' : 'ENTRY'; } } if (!order.tradeId) { if (!resolvedAction) { resolvedAction = queue[oppositeSide].length > 0 ? 'EXIT' : 'ENTRY'; } if (resolvedAction === 'ENTRY') { order.tradeId = `TRD-LEGACY-${profileKey}-${order.symbol}-${order.timestamp}-${queue.counter++}`; } else { order.tradeId = queue[oppositeSide].length > 0 ? queue[oppositeSide][0] : `TRD-LEGACY-${profileKey}-${order.symbol}-EXIT-${order.timestamp}-${queue.counter++}`; } } if (!order.action && resolvedAction) { order.action = resolvedAction; } if (order.tradeId && order.action === 'ENTRY') { entrySideByScopedTradeId.set(scopedTradeKey(order.tradeId), order.side); } else if (order.tradeId && order.action === 'EXIT') { const scopedKey = scopedTradeKey(order.tradeId); if (!entrySideByScopedTradeId.has(scopedKey)) { entrySideByScopedTradeId.set(scopedKey, oppositeSide); } } if (order.action === 'ENTRY') { if (!queue[side].includes(order.tradeId)) { queue[side].push(order.tradeId); } } else if (order.action === 'EXIT') { const matchedIndex = queue[oppositeSide].findIndex((id) => id === order.tradeId); if (matchedIndex >= 0) { queue[oppositeSide].splice(matchedIndex, 1); } else if (queue[oppositeSide].length > 0) { queue[oppositeSide].shift(); } } } return enriched; }; export const PositionsTab = ({ botState, onManageHolding }: PositionsTabProps) => { const { user, profile } = useAuth(); const [manualPositions, setManualPositions] = useState([]); const [simplePlanMetaByTradeId, setSimplePlanMetaByTradeId] = useState>({}); const [dbOrders, setDbOrders] = useState([]); const [historyTradeKeys, setHistoryTradeKeys] = useState([]); const [profiles, setProfiles] = useState([]); const [selectedProfileId, setSelectedProfileId] = useState('all'); const { snapshot: canonicalSnapshot, loading: canonicalLoading, error: canonicalError } = useCanonicalLifecycle( selectedProfileId === 'all' ? undefined : selectedProfileId ); const [ordersDateFrom, setOrdersDateFrom] = useState(''); const [ordersDateTo, setOrdersDateTo] = useState(''); const [ordersSortDirection, setOrdersSortDirection] = useState<'desc' | 'asc'>('desc'); const [ordersPage, setOrdersPage] = useState(1); const [lifecycleDateFrom, setLifecycleDateFrom] = useState(''); const [lifecycleDateTo, setLifecycleDateTo] = useState(''); const [lifecycleSortDirection, setLifecycleSortDirection] = useState<'desc' | 'asc'>('desc'); const [lifecyclePage, setLifecyclePage] = useState(1); const ORDER_FETCH_LIMIT = 5000; const ORDER_ACTIVITY_PAGE_SIZE = 20; const TRACE_PAGE_SIZE = 10; const EPSILON = 1e-8; const REFRESH_INTERVAL_MS = 30_000; const hasCanonicalLifecycle = Boolean( canonicalSnapshot && !canonicalSnapshot.diagnostics?.truncated && canonicalSnapshot.lifecycleRows.length > 0 ); // 1. Fetch Data useEffect(() => { if (!user) return; let cancelled = false; const fetchData = async () => { if (cancelled) return; let posData: any[] = []; let ordData: RawOrderRecord[] | null = []; let histData: RawHistoryRecord[] | null = []; let profData: Array<{ id: string; name: string }> = []; let bootstrapError: Error | null = null; try { const bootstrap = await fetchPositionsBootstrap({ scope: profile?.role === 'admin' ? 'all' : 'user', limit: ORDER_FETCH_LIMIT }); posData = Array.isArray(bootstrap.entries) ? bootstrap.entries : []; ordData = Array.isArray(bootstrap.orders) ? (bootstrap.orders as RawOrderRecord[]) : []; histData = Array.isArray(bootstrap.historyTradeKeys) ? (bootstrap.historyTradeKeys as RawHistoryRecord[]) : []; profData = (Array.isArray(bootstrap.profiles) ? bootstrap.profiles : []).map((item: any) => ({ id: String(item.id), name: String(item.name || item.id || 'Unnamed Profile') })); } catch (error) { bootstrapError = error as Error; } if (bootstrapError) { console.error('[PositionsTab] Failed loading positions bootstrap:', bootstrapError.message); } const tradeKeys = Array.from(new Set( ((histData as RawHistoryRecord[] | null) || []) .map((row) => { const tradeId = String(row.trade_id || '').trim(); if (!tradeId) return ''; return `${row.profile_id || 'global'}|${tradeId}`; }) .filter(Boolean) )); if (posData) { const positions: HybridPosition[] = posData.map((entry: any) => ({ source: 'MANUAL' as const, id: entry.stock_instance_id, symbol: entry.symbol, side: 'BUY' as const, size: entry.quantity || 0, entryPrice: entry.buy_price || 0, currentPrice: null, pnl: null, pnlPercent: null, stopLoss: entry.drop_threshold_for_buy, takeProfit: entry.gain_threshold_for_sell })).filter((p: { size: number; entryPrice: number; }) => p.size > 0 && p.entryPrice > 0); setManualPositions(positions); const nextSimplePlanMetaByTradeId = Object.fromEntries( posData .filter((entry: any) => String(entry.workflow_type || '').trim().toLowerCase() === 'simple') .map((entry: any) => { const tradeId = String(entry.linked_trade_id || '').trim(); if (!tradeId) return null; const holdingMode = String(entry.holding_mode || '').trim().toLowerCase() === 'long_term' ? 'long_term' : 'short_term'; return [tradeId, { entryId: String(entry.stock_instance_id || '').trim() || undefined, holdingMode, automationState: String(entry.automation_state || '').trim() || null, }] as const; }) .filter(Boolean) as Array ); setSimplePlanMetaByTradeId(nextSimplePlanMetaByTradeId); } setDbOrders(ordData || []); setHistoryTradeKeys(tradeKeys); setProfiles((profData as Profile[]) || []); }; fetchData(); const refreshTimer = window.setInterval(fetchData, REFRESH_INTERVAL_MS); return () => { cancelled = true; window.clearInterval(refreshTimer); }; }, [user, profile?.role]); const runtimeSimplePlanMetaByTradeId = useMemo(() => { const merged: Record> = {}; const events = Array.isArray(botState.operationalEvents) ? botState.operationalEvents : []; for (const event of events) { if (!event || event.type !== 'SIMPLE_SETUP_UPDATE') continue; const tradeId = String(event.tradeId || '').trim(); if (!tradeId) continue; const nextAutomationState = inferAutomationStateFromSimpleEvent(event.message); const existing = merged[tradeId] || {}; merged[tradeId] = { entryId: String(event.setupId || '').trim() || existing.entryId, automationState: nextAutomationState ?? existing.automationState ?? null, }; } return merged; }, [botState.operationalEvents]); const effectiveSimplePlanMetaByTradeId = useMemo(() => { const merged: Record = { ...simplePlanMetaByTradeId }; for (const [tradeId, runtimeMeta] of Object.entries(runtimeSimplePlanMetaByTradeId)) { const existing = merged[tradeId]; merged[tradeId] = { entryId: runtimeMeta.entryId || existing?.entryId, holdingMode: existing?.holdingMode || 'short_term', automationState: runtimeMeta.automationState ?? existing?.automationState ?? null, }; } return merged; }, [runtimeSimplePlanMetaByTradeId, simplePlanMetaByTradeId]); // 2. Build bot positions from real-time Socket.IO data const botPositionsRaw: HybridPosition[] = useMemo(() => { const deduped = new Map(); const score = (position: HybridPosition): number => { const tradeScore = position.tradeId ? 4 : 0; const profileScore = position.profileId ? 3 : 0; const nameScore = position.profileName ? 1 : 0; const notional = Math.abs(Number(position.entryPrice || 0) * Number(position.size || 0)); return tradeScore + profileScore + nameScore + Math.min(notional, 100_000); }; for (const p of (botState.positions || [])) { const normalized: HybridPosition = { source: 'BOT' as const, id: p.id || `bot-${p.profileId}-${p.symbol}`, symbol: p.symbol, side: p.side as 'BUY' | 'SELL', size: p.size, entryPrice: p.entryPrice, currentPrice: p.currentPrice || 0, pnl: p.unrealizedPnl || 0, pnlPercent: p.unrealizedPnlPercent || 0, stopLoss: p.stopLoss, takeProfit: p.takeProfit, profileId: p.profileId, profileName: p.profileName, tradeId: p.tradeId, planMode: p.tradeId && effectiveSimplePlanMetaByTradeId[p.tradeId] ? effectiveSimplePlanMetaByTradeId[p.tradeId].holdingMode : undefined, planState: p.tradeId && effectiveSimplePlanMetaByTradeId[p.tradeId] ? effectiveSimplePlanMetaByTradeId[p.tradeId].automationState || null : null, planEntryId: p.tradeId && effectiveSimplePlanMetaByTradeId[p.tradeId] ? effectiveSimplePlanMetaByTradeId[p.tradeId].entryId : undefined, }; const tradeId = String(normalized.tradeId || '').trim(); const dedupeKey = tradeId ? `trade:${tradeId}` : `${normalized.profileId || 'global'}|${normalized.symbol}|${normalized.side}`; const existing = deduped.get(dedupeKey); if (!existing) { deduped.set(dedupeKey, normalized); continue; } const preferred = score(normalized) >= score(existing) ? normalized : existing; const fallback = preferred === normalized ? existing : normalized; deduped.set(dedupeKey, { ...fallback, ...preferred, profileId: preferred.profileId || fallback.profileId, profileName: preferred.profileName || fallback.profileName, tradeId: preferred.tradeId || fallback.tradeId }); } return Array.from(deduped.values()); }, [botState.positions, effectiveSimplePlanMetaByTradeId]); const managedSymbols = useMemo(() => { return new Set(Object.keys(botState.symbols || {}).map((symbol) => String(symbol).toUpperCase())); }, [botState.symbols]); const hasManagedSymbolScope = managedSymbols.size > 0; const managedSymbolTokens = useMemo(() => { return new Set( Array.from(managedSymbols) .map((symbol) => normalizeLifecycleSymbolToken(symbol)) .filter(Boolean) ); }, [managedSymbols]); // Filter by Profile const filteredBotPositions = selectedProfileId === 'all' ? botPositionsRaw : botPositionsRaw.filter(p => p.profileId === selectedProfileId); const filteredManualPositions = selectedProfileId === 'all' ? manualPositions : []; const resolvedOrders = useMemo(() => { const normalizedBotOrders = (botState.orders || []) .map((order) => normalizeOrder(order as RawOrderRecord, 'BOT')) .filter((order): order is NormalizedOrder => !!order); const normalizedDbOrders = dbOrders .map((order) => normalizeOrder(order, order.profile_id ? 'BOT' : 'MANUAL')) .filter((order): order is NormalizedOrder => !!order); const mergedByKey = new Map(); for (const order of normalizedDbOrders) { const key = orderKey(order); const existing = mergedByKey.get(key); if (!existing) { mergedByKey.set(key, order); continue; } mergedByKey.set(key, mergeOrders(existing, order)); } // Keep DB rows authoritative for lifecycle state; runtime rows only enrich missing metadata. for (const order of normalizedBotOrders) { const key = orderKey(order); const existing = mergedByKey.get(key); if (!existing) { mergedByKey.set(key, order); continue; } mergedByKey.set(key, { ...existing, profileId: existing.profileId || order.profileId, tradeId: existing.tradeId || order.tradeId, action: existing.action || order.action, stopLoss: existing.stopLoss || order.stopLoss, takeProfit: existing.takeProfit || order.takeProfit, subTag: existing.subTag || order.subTag }); } const withLifecycleIds = assignLifecycleTradeIds( Array.from(mergedByKey.values()), botState.positions || [] ); const historyTradeKeySet = new Set(historyTradeKeys); const staleResolvedOrders = withLifecycleIds.map((order) => { const isPendingLike = isPendingLikeStatus(order.status); if (!isPendingLike || order.action !== 'EXIT' || !order.tradeId) { return order; } const scopedTradeKey = `${order.profileId || 'global'}|${order.tradeId}`; const hasClosedHistory = historyTradeKeySet.has(scopedTradeKey) || historyTradeKeySet.has(`global|${order.tradeId}`); if (!hasClosedHistory) { return order; } return { ...order, status: 'canceled' }; }); return staleResolvedOrders .filter((order) => selectedProfileId === 'all' || order.profileId === selectedProfileId) .sort((a, b) => (b.timestamp - a.timestamp) || b.id.localeCompare(a.id)); }, [botState.orders, botState.positions, dbOrders, historyTradeKeys, selectedProfileId]); const ordersDateBounds = useMemo(() => ({ from: parseDateStart(ordersDateFrom), to: parseDateEnd(ordersDateTo) }), [ordersDateFrom, ordersDateTo]); const filteredOrdersForActivity = useMemo(() => { return resolvedOrders.filter((order) => { if (ordersDateBounds.from !== null && order.timestamp < ordersDateBounds.from) return false; if (ordersDateBounds.to !== null && order.timestamp > ordersDateBounds.to) return false; return true; }); }, [resolvedOrders, ordersDateBounds.from, ordersDateBounds.to]); const sortedOrdersForActivity = useMemo(() => { const direction = ordersSortDirection === 'asc' ? 1 : -1; return [...filteredOrdersForActivity].sort((a, b) => { if (a.timestamp !== b.timestamp) { return direction * (a.timestamp - b.timestamp); } return direction * a.id.localeCompare(b.id); }); }, [filteredOrdersForActivity, ordersSortDirection]); const ordersTotalPages = useMemo( () => Math.max(1, Math.ceil(sortedOrdersForActivity.length / ORDER_ACTIVITY_PAGE_SIZE)), [sortedOrdersForActivity.length, ORDER_ACTIVITY_PAGE_SIZE] ); useEffect(() => { setOrdersPage(1); }, [selectedProfileId, ordersDateFrom, ordersDateTo]); useEffect(() => { setOrdersPage((current) => Math.min(current, ordersTotalPages)); }, [ordersTotalPages]); const finalOrders = useMemo(() => { const startIndex = (ordersPage - 1) * ORDER_ACTIVITY_PAGE_SIZE; return sortedOrdersForActivity.slice(startIndex, startIndex + ORDER_ACTIVITY_PAGE_SIZE); }, [sortedOrdersForActivity, ordersPage, ORDER_ACTIVITY_PAGE_SIZE]); const historyTradeKeySet = useMemo(() => new Set(historyTradeKeys), [historyTradeKeys]); const activePositionTradeKeys = useMemo(() => { const keys = new Set(); for (const position of filteredBotPositions) { const tradeId = String(position.tradeId || '').trim(); if (!tradeId) continue; keys.add(`${position.profileId || 'global'}|${tradeId}`); keys.add(`global|${tradeId}`); } return keys; }, [filteredBotPositions]); const staleWarningOrders = useMemo(() => { return resolvedOrders.filter((order) => { if (!isPendingLikeStatus(order.status)) return false; const orderAge = order.timestamp ? Date.now() - order.timestamp : 0; if (orderAge <= 5 * 60 * 1000) return false; const tradeId = String(order.tradeId || '').trim(); if (tradeId) { const scopedTradeKey = `${order.profileId || 'global'}|${tradeId}`; if (activePositionTradeKeys.has(scopedTradeKey) || activePositionTradeKeys.has(`global|${tradeId}`)) { return false; } if (historyTradeKeySet.has(scopedTradeKey) || historyTradeKeySet.has(`global|${tradeId}`)) { return false; } } return true; }); }, [resolvedOrders, activePositionTradeKeys, historyTradeKeySet]); const entryOrdersLookup = useMemo(() => { const byScopedTrade = new Map(); const byTrade = new Map(); for (const order of resolvedOrders) { if (order.action !== 'ENTRY' || !order.tradeId) continue; const scopedKey = `${order.profileId || 'global'}|${order.tradeId}`; const scopedExisting = byScopedTrade.get(scopedKey); if (!scopedExisting || order.timestamp < scopedExisting.timestamp) { byScopedTrade.set(scopedKey, order); } const globalExisting = byTrade.get(order.tradeId); if (!globalExisting || order.timestamp < globalExisting.timestamp) { byTrade.set(order.tradeId, order); } } return { byScopedTrade, byTrade }; }, [resolvedOrders]); const resolveEntryOrder = (tradeId?: string, profileId?: string): NormalizedOrder | undefined => { if (!tradeId) return undefined; const scopedKey = `${profileId || 'global'}|${tradeId}`; return entryOrdersLookup.byScopedTrade.get(scopedKey) || entryOrdersLookup.byTrade.get(tradeId); }; const lifecycleTraces = useMemo(() => { if (canonicalSnapshot && canonicalSnapshot.lifecycleRows.length > 0) { return canonicalSnapshot.lifecycleRows .filter((row) => selectedProfileId === 'all' || row.profileId === selectedProfileId) .map((row) => { const entryOrder: NormalizedOrder | undefined = row.entryQty > EPSILON ? { id: `entry:${row.id}`, orderId: undefined, symbol: row.symbol, type: 'Market', side: row.side, qty: Number(row.entryQty || 0), price: Number(row.entryAvgPrice || row.openEntryAvgPrice || 0), status: 'filled', timestamp: Number(row.lastEventAt || 0), profileId: row.profileId, tradeId: row.tradeId, action: 'ENTRY', source: 'BOT', stopLoss: Number(row.stopLoss || 0) || undefined, takeProfit: Number(row.takeProfit || 0) || undefined, subTag: String(row.subTag || '').trim() || undefined } : undefined; const exitOrders: NormalizedOrder[] = Number(row.exitQty || 0) > EPSILON ? [{ id: `exit:${row.id}`, orderId: undefined, symbol: row.symbol, type: 'Market', side: row.side === 'BUY' ? 'SELL' : 'BUY', qty: Number(row.exitQty || 0), price: Number(row.exitAvgPrice || 0), status: 'filled', timestamp: Number(row.lastEventAt || 0), profileId: row.profileId, tradeId: row.tradeId, action: 'EXIT', source: 'BOT', subTag: String(row.subTag || '').trim() || undefined }] : []; const orderedEvents: NormalizedOrder[] = [ ...(entryOrder ? [entryOrder] : []), ...exitOrders ]; const stateReason = row.state === 'PARTIAL_EXIT' ? `Partial exit: ${Number(row.exitQty || 0).toFixed(4)} closed of ${Number(row.entryQty || 0).toFixed(4)}. Remaining quantity stays active in Open Positions.` : row.state === 'CLOSED' ? 'Entry and exit fills are fully matched.' : row.state === 'ORPHAN_EXIT' ? 'Exit fill found but entry leg is missing from current order data window.' : 'Entry filled and waiting for matching exit.'; return { traceKey: `${row.profileId}|${row.tradeId}`, tradeId: row.tradeId, profileId: row.profileId, profileName: row.profileName, symbol: row.symbol, side: row.side, source: 'BOT' as const, entryOrder, exitOrders, orderedEvents, entryFilledQty: Number(row.entryQty || 0), exitFilledQty: Number(row.exitQty || 0), openQty: Number(row.openQty || 0), entryAvgPrice: Number(row.entryAvgPrice || row.openEntryAvgPrice || 0), entryUsedUsd: Number((Number(row.entryQty || 0) * Number(row.entryAvgPrice || 0)).toFixed(8)), state: row.state as LifecycleTrace['state'], stateReason, lastTimestamp: Number(row.lastEventAt || 0), hasHistoryMatch: row.state === 'CLOSED', hasCancel: false } as LifecycleTrace; }) .sort((a, b) => b.lastTimestamp - a.lastTimestamp); } const grouped = new Map(); for (const order of resolvedOrders) { if (!order.tradeId) continue; const traceKey = `${order.profileId || 'global'}|${order.tradeId}`; if (!grouped.has(traceKey)) { grouped.set(traceKey, { tradeId: order.tradeId, profileId: order.profileId, orders: [] }); } grouped.get(traceKey)!.orders.push(order); } const traces: LifecycleTrace[] = []; for (const [traceKey, group] of grouped.entries()) { const orderedBase = [...group.orders].sort((a, b) => (a.timestamp - b.timestamp) || a.id.localeCompare(b.id)); const explicitEntry = orderedBase.find((event) => event.action === 'ENTRY'); const explicitExit = orderedBase.find((event) => event.action === 'EXIT'); const lifecycleSide: 'BUY' | 'SELL' = explicitEntry?.side || (explicitExit ? (explicitExit.side === 'BUY' ? 'SELL' : 'BUY') : orderedBase[0]?.side || 'BUY'); const orderedEvents = orderedBase.map((event) => ({ ...event, action: event.action || (event.side === lifecycleSide ? 'ENTRY' : 'EXIT') })); const entryOrders = orderedEvents.filter((o) => o.action === 'ENTRY'); const exitOrders = orderedEvents.filter((o) => o.action === 'EXIT'); const entryOrder = entryOrders[0]; const representative = entryOrder || orderedEvents[0]; if (!representative) continue; const entryFilledQty = entryOrders.reduce((sum, order) => ( isLifecycleFilledStatus(order.status) ? sum + (order.qty || 0) : sum ), 0); const exitFilledQty = exitOrders.reduce((sum, order) => ( isLifecycleFilledStatus(order.status) ? sum + (order.qty || 0) : sum ), 0); const openQty = Math.max(entryFilledQty - exitFilledQty, 0); const entryUsedUsd = entryOrders.reduce((sum, order) => ( isLifecycleFilledStatus(order.status) ? sum + ((order.qty || 0) * (order.price || 0)) : sum ), 0); const entryAvgPrice = entryFilledQty > EPSILON ? (entryUsedUsd > 0 ? (entryUsedUsd / entryFilledQty) : (entryOrder?.price || 0)) : (entryOrder?.price || 0); const hasScopedHistoryMatch = historyTradeKeySet.has(traceKey); const hasGlobalHistoryMatch = historyTradeKeySet.has(`global|${group.tradeId}`); const hasHistoryMatch = hasScopedHistoryMatch || hasGlobalHistoryMatch; const hasCancel = orderedEvents.some((event) => (event.status || '').includes('cancel')); let state: LifecycleTrace['state'] = 'OPEN'; if (entryOrders.length === 0 && exitOrders.length > 0) { if (hasHistoryMatch || exitFilledQty > EPSILON) { state = hasHistoryMatch ? 'CLOSED' : 'ORPHAN_EXIT'; } else { state = 'EXIT_PENDING'; } } else if (openQty <= EPSILON && (exitFilledQty > EPSILON || hasHistoryMatch)) { state = 'CLOSED'; } else if (exitFilledQty > EPSILON && openQty > EPSILON) { state = 'PARTIAL_EXIT'; } let stateReason = 'Entry filled and waiting for matching exit.'; if (state === 'PARTIAL_EXIT') { stateReason = `Partial exit: ${exitFilledQty.toFixed(4)} closed of ${entryFilledQty.toFixed(4)}. Remaining quantity stays active in Open Positions.`; } else if (state === 'CLOSED') { stateReason = hasHistoryMatch ? 'Lifecycle closed and recorded in trade history.' : 'Entry and exit fills are fully matched.'; } else if (state === 'ORPHAN_EXIT') { stateReason = 'Exit fill found but entry leg is missing from current order data window.'; } else if (state === 'EXIT_PENDING') { stateReason = 'Exit submitted but not filled yet. Waiting for exchange sync.'; } const profileName = representative.profileId ? (profiles.find((p) => p.id === representative.profileId)?.name || representative.profileId) : representative.source; traces.push({ traceKey, tradeId: group.tradeId, profileId: representative.profileId, profileName, symbol: representative.symbol, side: lifecycleSide, source: representative.source, entryOrder, exitOrders, orderedEvents, entryFilledQty, exitFilledQty, openQty, entryAvgPrice, entryUsedUsd, state, stateReason, lastTimestamp: orderedEvents[orderedEvents.length - 1]?.timestamp || representative.timestamp, hasHistoryMatch, hasCancel, }); } return traces .sort((a, b) => b.lastTimestamp - a.lastTimestamp); }, [hasCanonicalLifecycle, canonicalSnapshot, selectedProfileId, resolvedOrders, profiles, historyTradeKeySet, EPSILON]); const lifecycleDateBounds = useMemo(() => ({ from: parseDateStart(lifecycleDateFrom), to: parseDateEnd(lifecycleDateTo) }), [lifecycleDateFrom, lifecycleDateTo]); const filteredLifecycleTraces = useMemo(() => { return lifecycleTraces.filter((trace) => { if (lifecycleDateBounds.from !== null && trace.lastTimestamp < lifecycleDateBounds.from) return false; if (lifecycleDateBounds.to !== null && trace.lastTimestamp > lifecycleDateBounds.to) return false; return true; }); }, [lifecycleTraces, lifecycleDateBounds.from, lifecycleDateBounds.to]); const sortedLifecycleTraces = useMemo(() => { const direction = lifecycleSortDirection === 'asc' ? 1 : -1; return [...filteredLifecycleTraces].sort((a, b) => { if (a.lastTimestamp !== b.lastTimestamp) { return direction * (a.lastTimestamp - b.lastTimestamp); } return direction * a.traceKey.localeCompare(b.traceKey); }); }, [filteredLifecycleTraces, lifecycleSortDirection]); const lifecycleTotalPages = useMemo( () => Math.max(1, Math.ceil(sortedLifecycleTraces.length / TRACE_PAGE_SIZE)), [sortedLifecycleTraces.length, TRACE_PAGE_SIZE] ); useEffect(() => { setLifecyclePage(1); }, [selectedProfileId, lifecycleDateFrom, lifecycleDateTo]); useEffect(() => { setLifecyclePage((current) => Math.min(current, lifecycleTotalPages)); }, [lifecycleTotalPages]); const paginatedLifecycleTraces = useMemo(() => { const startIndex = (lifecyclePage - 1) * TRACE_PAGE_SIZE; return sortedLifecycleTraces.slice(startIndex, startIndex + TRACE_PAGE_SIZE); }, [sortedLifecycleTraces, lifecyclePage, TRACE_PAGE_SIZE]); const lifecycleSummary = useMemo(() => { const summary = { OPEN: 0, PARTIAL_EXIT: 0, CLOSED: 0, ORPHAN_EXIT: 0, EXIT_PENDING: 0 }; for (const trace of filteredLifecycleTraces) { summary[trace.state] += 1; } return summary; }, [filteredLifecycleTraces]); const allPositions = useMemo(() => { if (!hasCanonicalLifecycle) { const deduped = new Map(); for (const position of [...filteredBotPositions, ...filteredManualPositions]) { const symbol = String(position.symbol || '').trim(); if (!symbol) continue; if ( hasManagedSymbolScope && position.source === 'BOT' && !managedSymbolTokens.has(normalizeLifecycleSymbolToken(symbol)) ) { continue; } const key = position.tradeId ? `trade:${position.profileId || 'global'}|${position.tradeId}` : `${position.source}:${position.profileId || 'global'}|${symbol}|${position.side}`; if (!deduped.has(key)) { deduped.set(key, position); } } return Array.from(deduped.values()).sort((a, b) => { const sourceRankA = a.source === 'BOT' ? 0 : 1; const sourceRankB = b.source === 'BOT' ? 0 : 1; if (sourceRankA !== sourceRankB) return sourceRankA - sourceRankB; const notionalA = Math.abs(Number(a.entryPrice || 0) * Number(a.size || 0)); const notionalB = Math.abs(Number(b.entryPrice || 0) * Number(b.size || 0)); return notionalB - notionalA; }); } if (canonicalSnapshot && canonicalSnapshot.lifecycleRows.length > 0) { const canonicalOpen = canonicalSnapshot.openPositions .filter((position) => selectedProfileId === 'all' || position.profileId === selectedProfileId) .filter((position) => { const symbol = String(position.symbol || '').trim(); if (!symbol) return false; if (hasManagedSymbolScope && !managedSymbolTokens.has(normalizeLifecycleSymbolToken(symbol))) return false; return Number(position.size || 0) > EPSILON; }) .map((position) => ({ source: 'BOT' as const, id: position.id, symbol: position.symbol, side: position.side, size: Number(position.size || 0), entryPrice: Number(position.entryPrice || 0), currentPrice: Number(position.currentPrice || 0), pnl: Number(position.pnl || 0), pnlPercent: Number(position.pnlPercent || 0), stopLoss: Number(position.stopLoss || 0) || undefined, takeProfit: Number(position.takeProfit || 0) || undefined, profileId: position.profileId, profileName: position.profileName, tradeId: position.tradeId, planMode: position.tradeId && effectiveSimplePlanMetaByTradeId[position.tradeId] ? effectiveSimplePlanMetaByTradeId[position.tradeId].holdingMode : undefined, planState: position.tradeId && effectiveSimplePlanMetaByTradeId[position.tradeId] ? effectiveSimplePlanMetaByTradeId[position.tradeId].automationState || null : null, planEntryId: position.tradeId && effectiveSimplePlanMetaByTradeId[position.tradeId] ? effectiveSimplePlanMetaByTradeId[position.tradeId].entryId : undefined } as HybridPosition)); const deduped = new Map(); for (const position of [...canonicalOpen, ...filteredManualPositions]) { const symbol = String(position.symbol || '').trim(); if (!symbol) continue; const key = position.tradeId ? `trade:${position.profileId || 'global'}|${position.tradeId}` : `${position.source}:${position.profileId || 'global'}|${symbol}|${position.side}`; if (!deduped.has(key)) { deduped.set(key, position); } } return Array.from(deduped.values()).sort((a, b) => { const sourceRankA = a.source === 'BOT' ? 0 : 1; const sourceRankB = b.source === 'BOT' ? 0 : 1; if (sourceRankA !== sourceRankB) return sourceRankA - sourceRankB; const notionalA = Math.abs(Number(a.entryPrice || 0) * Number(a.size || 0)); const notionalB = Math.abs(Number(b.entryPrice || 0) * Number(b.size || 0)); return notionalB - notionalA; }); } return [...filteredManualPositions].sort((a, b) => { const sourceRankA = a.source === 'BOT' ? 0 : 1; const sourceRankB = b.source === 'BOT' ? 0 : 1; if (sourceRankA !== sourceRankB) return sourceRankA - sourceRankB; const notionalA = Math.abs(Number(a.entryPrice || 0) * Number(a.size || 0)); const notionalB = Math.abs(Number(b.entryPrice || 0) * Number(b.size || 0)); return notionalB - notionalA; }); }, [ hasCanonicalLifecycle, canonicalSnapshot, selectedProfileId, filteredBotPositions, filteredManualPositions, simplePlanMetaByTradeId, hasManagedSymbolScope, managedSymbolTokens, EPSILON ]); const positionMismatches = useMemo(() => { const runtimeMismatches = filteredBotPositions .filter((pos) => { const symbol = String(pos.symbol || '').trim(); if (!symbol) return false; if (hasManagedSymbolScope && !managedSymbolTokens.has(normalizeLifecycleSymbolToken(symbol))) return false; return true; }) .map((pos) => { if (!pos.tradeId) { return { id: `${pos.id}-missing-trade`, severity: 'warning' as const, profileName: pos.profileName || profiles.find((p) => p.id === pos.profileId)?.name || 'BOT', symbol: pos.symbol, tradeId: 'N/A', reason: 'Position has no trade ID. Lifecycle tracing is degraded.' }; } const scopedKey = `${pos.profileId || 'global'}|${pos.tradeId}`; const entryOrder = entryOrdersLookup.byScopedTrade.get(scopedKey) || entryOrdersLookup.byTrade.get(pos.tradeId); if (!entryOrder) { return { id: `${pos.id}-missing-entry`, severity: 'critical' as const, profileName: pos.profileName || profiles.find((p) => p.id === pos.profileId)?.name || 'BOT', symbol: pos.symbol, tradeId: pos.tradeId, reason: 'Position has trade ID but no matching ENTRY order.' }; } if (!symbolsMatchForLifecycle(entryOrder.symbol, pos.symbol)) { return { id: `${pos.id}-symbol-mismatch`, severity: 'critical' as const, profileName: pos.profileName || profiles.find((p) => p.id === pos.profileId)?.name || 'BOT', symbol: pos.symbol, tradeId: pos.tradeId, reason: `Trade ID resolves to ${entryOrder.symbol} entry, but position symbol is ${pos.symbol}.` }; } if ((entryOrder.profileId || 'global') !== (pos.profileId || 'global')) { return { id: `${pos.id}-profile-mismatch`, severity: 'critical' as const, profileName: pos.profileName || profiles.find((p) => p.id === pos.profileId)?.name || 'BOT', symbol: pos.symbol, tradeId: pos.tradeId, reason: 'Trade ID entry order belongs to a different profile scope.' }; } return null; }) .filter((issue): issue is NonNullable => !!issue); const canonicalMismatches = (canonicalSnapshot && canonicalSnapshot.lifecycleRows.length > 0) ? canonicalSnapshot.lifecycleRows .filter((row) => selectedProfileId === 'all' || row.profileId === selectedProfileId) .filter((row) => row.state === 'ORPHAN_EXIT' || (Number(row.openQty || 0) > EPSILON && Number(row.entryQty || 0) <= EPSILON)) .map((row) => ({ id: `${row.id}-canonical-mismatch`, severity: 'critical' as const, profileName: row.profileName || profiles.find((p) => p.id === row.profileId)?.name || 'BOT', symbol: row.symbol, tradeId: row.tradeId, reason: row.state === 'ORPHAN_EXIT' ? 'Lifecycle has EXIT fill without a matching ENTRY chain.' : 'Open lifecycle is missing a valid ENTRY fill.' })) : []; const deduped = new Map(); for (const issue of [...canonicalMismatches, ...runtimeMismatches]) { const key = `${issue.profileName}|${issue.symbol}|${issue.tradeId}|${issue.reason}`; if (!deduped.has(key)) { deduped.set(key, issue); } } return Array.from(deduped.values()); }, [ hasCanonicalLifecycle, canonicalSnapshot, selectedProfileId, filteredBotPositions, entryOrdersLookup, profiles, hasManagedSymbolScope, managedSymbolTokens, EPSILON ]); return (

Positions & Orders

Live positions, orders, and lifecycle status scoped by strategy profile.

{profiles.map(p => ( ))}
{!hasCanonicalLifecycle && (
Canonical lifecycle is unavailable{canonicalLoading ? ' (loading)' : ''}. Truth labels are in fallback mode (DB/runtime-derived). {canonicalError ? ` ${canonicalError}` : ''}
)} {canonicalSnapshot?.diagnostics?.truncated && (
Canonical lifecycle snapshot is truncated ({canonicalSnapshot.diagnostics.orderRows} rows). Narrow profile scope before operational decisions.
)} {/* Stale Orders Warning Banner */} {staleWarningOrders.length > 0 && (
⚠️

{staleWarningOrders.length} Stale Order{staleWarningOrders.length > 1 ? 's' : ''} Detected

Some orders have remained in pending_new for more than 5 minutes without stronger fill or position evidence. The background sync service is re-checking their exchange status.

)} {positionMismatches.length > 0 && (

Lifecycle Mismatch Diagnostics ({positionMismatches.length})

One or more open positions do not have a clean entry-order lineage by profile and trade ID.

{positionMismatches.map((issue) => (
{issue.severity} {issue.profileName} {issue.symbol} {issue.tradeId} {issue.reason}
))}
)}

Open Positions

{allPositions.length > 0 ? ( allPositions.map(pos => { const entryRisk = resolveEntryOrder(pos.tradeId, pos.profileId); const positionTruth = getTruthSourceForPosition(entryRisk, hasCanonicalLifecycle); const hasCurrentPrice = hasFiniteNumber(pos.currentPrice); const hasPnl = hasFiniteNumber(pos.pnl); const hasPnlPercent = hasFiniteNumber(pos.pnlPercent); const currentPrice: number | null = hasCurrentPrice ? Number(pos.currentPrice) : null; const pnl: number | null = hasPnl ? Number(pos.pnl) : null; const pnlPercent: number | null = hasPnlPercent ? Number(pos.pnlPercent) : null; const displayStopLoss = (pos.stopLoss && pos.stopLoss > 0) ? pos.stopLoss : (entryRisk?.stopLoss || 0); const displayTakeProfit = (pos.takeProfit && pos.takeProfit > 0) ? pos.takeProfit : (entryRisk?.takeProfit || 0); const slBreached = currentPrice !== null && displayStopLoss > 0 && ( (pos.side === 'BUY' && currentPrice <= displayStopLoss) || (pos.side === 'SELL' && currentPrice >= displayStopLoss) ); const tpHit = currentPrice !== null && displayTakeProfit > 0 && ( (pos.side === 'BUY' && currentPrice >= displayTakeProfit) || (pos.side === 'SELL' && currentPrice <= displayTakeProfit) ); return ( )}) ) : ( )}
Source Truth Trade Asset Side Size Entry Current SL TP P/L (%) Action
{pos.source === 'BOT' ? (pos.profileName || profiles.find(pr => pr.id === pos.profileId)?.name || 'BOT') : 'MANUAL'} {positionTruth.label} {pos.tradeId ? (
{pos.tradeId.split('-').pop()}
) : ( )}
{pos.symbol} {pos.side} {pos.planMode ? (
{pos.planMode === 'long_term' ? 'Long-term hold' : 'Short-term managed'}
) : null}
{formatDisplayQty(pos.size)} ${pos.entryPrice.toLocaleString()} {currentPrice !== null ? `$${currentPrice.toLocaleString()}` : '-'}
{slBreached && ( SL breached )} {!slBreached && tpHit && ( TP hit )}
{displayStopLoss ? `$${displayStopLoss.toLocaleString()}` : '-'} {displayTakeProfit ? `$${displayTakeProfit.toLocaleString()}` : '-'} {pnl !== null && pnlPercent !== null ? (
= 0 ? 'text-green-400' : 'text-red-400'}`}> {pnl >= 0 ? '+' : ''}{pnlPercent.toFixed(2)}%
${pnl.toFixed(2)}
) : (
-
)}
{pos.source === 'BOT' && pos.profileId && pos.tradeId && onManageHolding && ( )} {pos.source === 'BOT' && ( )}
No active positions for this selection.

Order Activity

{finalOrders.some((o) => o.tradeId) && ( Trade cycle tracing active )}
{finalOrders.length > 0 ? ( finalOrders.map((order) => { const hasTradeId = !!order.tradeId; const entryOrder = hasTradeId ? resolveEntryOrder(order.tradeId, order.profileId) : undefined; const resolvedAction: OrderAction | undefined = order.action || (hasTradeId ? (entryOrder && order.side === entryOrder.side ? 'ENTRY' : 'EXIT') : undefined); const isEntry = resolvedAction === 'ENTRY'; const isExit = resolvedAction === 'EXIT'; const truthSource = getTruthSourceForOrder(order, historyTradeKeySet, hasCanonicalLifecycle); const tradePnl = isExit && entryOrder ? (order.price - entryOrder.price) * Math.min(order.qty || 0, entryOrder.qty || order.qty || 0) * (entryOrder.side === 'BUY' ? 1 : -1) : null; return ( ); }) ) : ( )}
Trade ID Sub-tag Source Truth Time Asset Action Side Size Price Status
Profile + Date filters
) => setOrdersDateFrom(e.target.value)} controlSize="sm" variant="muted" 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-orange-500/40" /> ) => setOrdersDateTo(e.target.value)} controlSize="sm" variant="muted" 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-orange-500/40" />
Sorting is applied by time on this table.
{hasTradeId ? (
{order.tradeId}
) : ( - )}
{order.subTag ? ( {order.subTag} ) : ( - )} {order.profileId ? (profiles.find(pr => pr.id === order.profileId)?.name || 'BOT') : 'MANUAL'} {truthSource.label} {order.timestamp ? new Date(order.timestamp).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'} {order.symbol} {resolvedAction ? ( {isEntry ? 'ENTRY' : 'EXIT'} ) : ( {order.type} )} {order.side} {Number(order.qty || 0).toFixed(4)} ${Number(order.price).toLocaleString()}
{tradePnl !== null && ( = 0 ? 'text-green-400' : 'text-red-400'}`}> {tradePnl >= 0 ? '+' : ''}{tradePnl.toFixed(2)} )} {(() => { const isPendingNew = isPendingLikeStatus(order.status); const isExpired = order.status === 'expired'; const isUnknown = order.status === 'unknown'; const orderAge = order.timestamp ? Date.now() - order.timestamp : 0; const isStale = isPendingNew && orderAge > 5 * 60 * 1000; let badgeClass = 'bg-white/10 text-gray-400'; let tooltip = ''; if (order.status === 'filled') { badgeClass = 'bg-green-500/20 text-green-400 border border-green-500/20'; } else if (isStale) { badgeClass = 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/20'; tooltip = 'Order pending for >5 min - sync in progress'; } else if (isExpired) { badgeClass = 'bg-orange-500/20 text-orange-400 border border-orange-500/20'; tooltip = 'Order not found on exchange - likely never executed'; } else if (isUnknown) { badgeClass = 'bg-gray-500/20 text-gray-400 border border-gray-500/20'; tooltip = 'Order status could not be verified'; } return (
{order.status} {isStale && ( ! )} {(isExpired || isUnknown) && ( i )}
); })()}
No recent orders for this cluster.
Showing {sortedOrdersForActivity.length === 0 ? 0 : ((ordersPage - 1) * ORDER_ACTIVITY_PAGE_SIZE) + 1} -{Math.min(ordersPage * ORDER_ACTIVITY_PAGE_SIZE, sortedOrdersForActivity.length)} of {sortedOrdersForActivity.length}
Page {ordersPage} / {ordersTotalPages}

Lifecycle Trace

ENTRY -> EXIT chain by trade_id
OPEN: {lifecycleSummary.OPEN} PARTIAL_EXIT: {lifecycleSummary.PARTIAL_EXIT} CLOSED: {lifecycleSummary.CLOSED} EXIT_PENDING: {lifecycleSummary.EXIT_PENDING} ORPHAN_EXIT: {lifecycleSummary.ORPHAN_EXIT}

State is computed from filled ENTRY/EXIT quantities and reconciled with `trade_history` for closed-lifecycle confirmation.

{filteredLifecycleTraces.length > 0 ? ( paginatedLifecycleTraces.map((trace) => ( )) ) : ( )}
Trade ID Profile Asset Lifecycle Chain Entry Used ($) Open Qty State
Profile + Date filters Filter in headers
) => setLifecycleDateFrom(e.target.value)} controlSize="sm" variant="muted" 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-cyan-500/40" /> ) => setLifecycleDateTo(e.target.value)} controlSize="sm" variant="muted" 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-cyan-500/40" />
Sorting is applied by latest lifecycle event time.
{trace.tradeId}
{trace.profileName} {trace.symbol}
{trace.orderedEvents.map((event) => { const actionLabel: OrderAction = event.action === 'EXIT' ? 'EXIT' : 'ENTRY'; const actionClass = actionLabel === 'ENTRY' ? 'bg-blue-500/10 text-blue-300 border border-blue-500/20' : 'bg-amber-500/10 text-amber-300 border border-amber-500/20'; return (
{actionLabel} {event.timestamp ? new Date(event.timestamp).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'} {event.side} {Number(event.qty || 0).toFixed(4)} @ ${Number(event.price || 0).toLocaleString()} {event.status}
); })}
${trace.entryUsedUsd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
@ ${trace.entryAvgPrice.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{trace.openQty.toFixed(4)}
{trace.state}
{trace.stateReason}
{trace.entryOrder && } {trace.hasHistoryMatch && } {trace.state === 'PARTIAL_EXIT' && } {trace.hasCancel && } {trace.state === 'CLOSED' && }
No trade lifecycle traces available for this selection.
Showing {sortedLifecycleTraces.length === 0 ? 0 : ((lifecyclePage - 1) * TRACE_PAGE_SIZE) + 1} -{Math.min(lifecyclePage * TRACE_PAGE_SIZE, sortedLifecycleTraces.length)} of {sortedLifecycleTraces.length}
Page {lifecyclePage} / {lifecycleTotalPages}
); };