import { useState, useEffect, useMemo } from 'react'; import type { BotState } from '../hooks/useWebSocket'; import { useAuth } from '../components/AuthContext'; import { aggregateCanonicalLifecycleTrades } from '../lib/orderLifecycleLedger'; import { useCanonicalLifecycle } from '../hooks/useCanonicalLifecycle'; import { fetchTradeProfiles } from '../lib/profileApi'; import { Button } from '../components/ui/Primitives'; interface OverviewTabProps { botState: BotState; previewAsCustomer?: boolean; connected?: boolean; } interface ActiveProfileCapital { id: string; name: string; allocated: number; cooldownMinutes: number; } interface TradeAggregate { tradeCount: number; wins: number; winRate: number; netPnl: number; lastClosedTradeAt: number; } interface ProfileSignalAggregate { totalSignals: number; activeSignals: number; blockedSignals: number; skippedSignals: number; executedSignals: number; } type WinRateWindow = '24h' | '7d' | '30d' | 'all'; const WIN_RATE_WINDOW_OPTIONS: Array<{ key: WinRateWindow; label: string; ms?: number }> = [ { key: '24h', label: '24H', ms: 24 * 60 * 60 * 1000 }, { key: '7d', label: '7D', ms: 7 * 24 * 60 * 60 * 1000 }, { key: '30d', label: '30D', ms: 30 * 24 * 60 * 60 * 1000 }, { key: 'all', label: 'All' } ]; const REFRESH_INTERVAL_MS = 30_000; const overviewWarningBorder = '1px solid color-mix(in oklab, var(--bl-warning) 28%, transparent)'; const overviewWarningSurface = 'var(--bl-warning-muted)'; const overviewWarningText = 'var(--bl-warning)'; const overviewDangerBorder = '1px solid color-mix(in oklab, var(--bl-danger) 28%, transparent)'; const overviewDangerSurface = 'var(--bl-danger-muted)'; const overviewDangerText = 'var(--bl-danger)'; const overviewMutedText = 'var(--bl-text-faint)'; const overviewQuietText = 'var(--bl-text-quiet)'; const overviewInfoText = 'var(--bl-info-strong)'; const overviewAttentionText = 'var(--bl-warning)'; const overviewSuccessText = 'var(--bl-success)'; const overviewNeutralText = 'var(--foreground)'; const overviewSoftBorder = '1px solid var(--bl-border-subtle)'; const overviewActiveBorder = '1px solid color-mix(in oklab, var(--bl-success) 45%, transparent)'; const overviewActiveSurface = 'color-mix(in oklab, var(--bl-success) 8%, transparent)'; const overviewMutedSurface = 'color-mix(in oklab, var(--foreground) 3%, transparent)'; const overviewSectionBorder = '1px solid color-mix(in oklab, var(--bl-border) 70%, transparent)'; const overviewCardGradient = 'linear-gradient(145deg, color-mix(in oklab, var(--bl-surface-overlay) 92%, var(--card-elevated)), color-mix(in oklab, var(--bl-surface-overlay) 84%, var(--background)))'; const overviewCardBorder = '1px solid color-mix(in oklab, var(--bl-border) 55%, transparent)'; const overviewDivider = '1px solid color-mix(in oklab, var(--bl-border) 45%, transparent)'; const overviewBotMuted = 'color-mix(in oklab, var(--muted-foreground) 62%, var(--background))'; const overviewBotQuiet = 'color-mix(in oklab, var(--muted-foreground) 52%, var(--background))'; const overviewTagBorder = '1px solid color-mix(in oklab, var(--bl-border) 82%, transparent)'; const formatUptime = (ms: number): string => { if (ms < 60000) return `${Math.floor(ms / 1000)}s`; if (ms < 3600000) return `${Math.floor(ms / 60000)}m`; const hours = Math.floor(ms / 3600000); const mins = Math.floor((ms % 3600000) / 60000); return `${hours}h ${mins}m`; }; const formatDurationCompact = (ms: number): string => { if (!Number.isFinite(ms) || ms <= 0) return '0m'; const totalMinutes = Math.max(0, Math.floor(ms / 60000)); const days = Math.floor(totalMinutes / 1440); const hours = Math.floor((totalMinutes % 1440) / 60); const minutes = totalMinutes % 60; if (days > 0) return `${days}d ${hours}h`; if (hours > 0) return `${hours}h ${minutes}m`; return `${minutes}m`; }; const formatUsd = (value: number): string => `$${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; 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 dedupeLivePositions = (positions: BotState['positions'] = []): BotState['positions'] => { const mergedByKey = new Map(); const score = (position: BotState['positions'][0]): number => { const tradeScore = position.tradeId ? 4 : 0; const profileScore = position.profileId ? 3 : 0; const userScore = position.profileName ? 1 : 0; const notional = Math.abs(Number(position.entryPrice || 0) * Number(position.size || 0)); return tradeScore + profileScore + userScore + Math.min(notional, 100_000); }; for (const position of positions || []) { const tradeId = String(position.tradeId || '').trim(); const key = tradeId ? `trade:${tradeId}` : `fallback:${position.profileId || 'global'}|${position.symbol}|${position.side}`; const existing = mergedByKey.get(key); if (!existing) { mergedByKey.set(key, position); continue; } const preferred = score(position) >= score(existing) ? position : existing; const fallback = preferred === position ? existing : position; mergedByKey.set(key, { ...fallback, ...preferred, profileId: preferred.profileId || fallback.profileId, profileName: preferred.profileName || fallback.profileName, tradeId: preferred.tradeId || fallback.tradeId }); } return Array.from(mergedByKey.values()); }; export const OverviewTab = ({ botState, previewAsCustomer = false, connected = true }: OverviewTabProps) => { const { user, profile } = useAuth(); const { snapshot: canonicalSnapshot, loading: canonicalLoading, error: canonicalError } = useCanonicalLifecycle(); const isAdminView = profile?.role === 'admin' && !previewAsCustomer; const symbols = Object.keys(botState.symbols || {}); const [fallbackCapital, setFallbackCapital] = useState(botState.settings.totalCapital); const [profileCount, setProfileCount] = useState(0); const [activeProfiles, setActiveProfiles] = useState([]); const [winRateWindow, setWinRateWindow] = useState('7d'); const canonicalLifecycleReady = Boolean(canonicalSnapshot && !canonicalSnapshot.diagnostics?.truncated); useEffect(() => { if (!user) return; let cancelled = false; const loadStats = async () => { if (cancelled) return; try { const profilesData = await fetchTradeProfiles({ scope: profile?.role === 'admin' ? 'all' : 'user' }); const activeProfileRows: ActiveProfileCapital[] = ((profilesData as any[]) || []) .filter((p: any) => Boolean(p.is_active)) .map((p: any) => { const rawCooldown = Number(p?.strategy_config?.execution?.cooldownMinutes); return { id: String(p.id), name: String(p.name || p.id), allocated: Number(p.allocated_capital || 0), cooldownMinutes: Number.isFinite(rawCooldown) && rawCooldown >= 0 ? rawCooldown : 30 }; }); const activeCapital = activeProfileRows.reduce((sum, p) => sum + p.allocated, 0); if (cancelled) return; setFallbackCapital(activeCapital || botState.settings.totalCapital); setProfileCount(activeProfileRows.length); setActiveProfiles(activeProfileRows); } catch (err) { console.error('[OverviewTab] Unexpected load error:', err); } }; loadStats(); const refreshTimer = window.setInterval(loadStats, REFRESH_INTERVAL_MS); return () => { cancelled = true; window.clearInterval(refreshTimer); }; }, [user, profile?.role, botState.settings.totalCapital]); const lifecycleOpenByProfile = useMemo(() => { if (canonicalLifecycleReady && canonicalSnapshot) { const usedByProfile = new Map(); const unrealizedByProfile = new Map(); const openTradesByProfile = new Map(); for (const [profileId, aggregate] of Object.entries(canonicalSnapshot.aggregates.byProfile || {})) { const openNotional = Number(aggregate.openNotional || 0); const unrealizedPnl = Number(aggregate.unrealizedPnl || 0); const openTrades = Number(aggregate.openTrades || 0); if (openNotional > 0) usedByProfile.set(profileId, openNotional); if (Math.abs(unrealizedPnl) > 0) unrealizedByProfile.set(profileId, unrealizedPnl); if (openTrades > 0) openTradesByProfile.set(profileId, openTrades); } return { hasRows: true, usedByProfile, unrealizedByProfile, openTradesByProfile }; } return { hasRows: false, usedByProfile: new Map(), unrealizedByProfile: new Map(), openTradesByProfile: new Map() }; }, [canonicalLifecycleReady, canonicalSnapshot]); const liveOpenByProfile = useMemo(() => { const usedByProfile = new Map(); const unrealizedByProfile = new Map(); const openTradesByProfile = new Map(); const deduped = dedupeLivePositions(botState.positions || []); for (const position of deduped) { const profileId = String(position.profileId || '').trim(); if (!profileId) continue; const notional = Math.max(0, Math.abs(Number(position.entryPrice || 0) * Number(position.size || 0))); const unrealized = Number(position.unrealizedPnl || 0); usedByProfile.set(profileId, (usedByProfile.get(profileId) || 0) + notional); unrealizedByProfile.set(profileId, (unrealizedByProfile.get(profileId) || 0) + unrealized); openTradesByProfile.set(profileId, (openTradesByProfile.get(profileId) || 0) + 1); } return { hasRows: deduped.length > 0, usedByProfile, unrealizedByProfile, openTradesByProfile }; }, [botState.positions]); const effectiveOpenByProfile = useMemo(() => { const usedByProfile = new Map(lifecycleOpenByProfile.usedByProfile); const unrealizedByProfile = new Map(lifecycleOpenByProfile.unrealizedByProfile); const openTradesByProfile = new Map(lifecycleOpenByProfile.openTradesByProfile); const suppressedLifecycleNotionalByProfile = new Map(); // If lifecycle still claims open notional while live inventory is flat, prefer live 0 usage // to avoid stale capital-lock visuals in Overview while reconciliation catches up. for (const [profileId, canonicalUsedRaw] of usedByProfile.entries()) { const canonicalUsed = Math.max(0, Number(canonicalUsedRaw || 0)); if (!(canonicalUsed > 1e-8)) continue; const canonicalOpenCount = Number(openTradesByProfile.get(profileId) || 0); const liveUsed = Math.max(0, Number(liveOpenByProfile.usedByProfile.get(profileId) || 0)); const liveOpenCount = Number(liveOpenByProfile.openTradesByProfile.get(profileId) || 0); if (canonicalOpenCount <= 0) continue; if (liveOpenCount <= 0 && !(liveUsed > 1e-8)) { suppressedLifecycleNotionalByProfile.set(profileId, canonicalUsed); usedByProfile.set(profileId, 0); openTradesByProfile.set(profileId, 0); unrealizedByProfile.delete(profileId); } } for (const [profileId, liveUsed] of liveOpenByProfile.usedByProfile.entries()) { const canonicalUsed = Number(usedByProfile.get(profileId) || 0); if (!(canonicalUsed > 0)) { usedByProfile.set(profileId, liveUsed); } } for (const [profileId, liveUnrealized] of liveOpenByProfile.unrealizedByProfile.entries()) { const canonicalUnrealized = Number(unrealizedByProfile.get(profileId) || 0); if (Math.abs(canonicalUnrealized) <= 1e-8) { unrealizedByProfile.set(profileId, liveUnrealized); } } for (const [profileId, liveOpenCount] of liveOpenByProfile.openTradesByProfile.entries()) { const canonicalOpenCount = Number(openTradesByProfile.get(profileId) || 0); if (liveOpenCount > canonicalOpenCount) { openTradesByProfile.set(profileId, liveOpenCount); } } return { hasRows: lifecycleOpenByProfile.hasRows || liveOpenByProfile.hasRows, usedByProfile, unrealizedByProfile, openTradesByProfile, suppressedLifecycleNotionalByProfile }; }, [lifecycleOpenByProfile, liveOpenByProfile]); const openPositionsByProfile = useMemo(() => { return effectiveOpenByProfile.openTradesByProfile; }, [effectiveOpenByProfile]); const profileSignalsByProfile = useMemo(() => { const byProfile = new Map(); for (const symbol of Object.keys(botState.symbols || {})) { const symbolState = botState.symbols[symbol]; const entries = Object.entries(symbolState?.profileSignals || {}); for (const [profileId, signalState] of entries) { const current = byProfile.get(profileId) || { totalSignals: 0, activeSignals: 0, blockedSignals: 0, skippedSignals: 0, executedSignals: 0 }; current.totalSignals += 1; const signal = String((signalState as any)?.signal || '').toUpperCase(); const passed = Boolean((signalState as any)?.passed); const directional = signal === 'BUY' || signal === 'SELL'; const executionStatus = String((signalState as any)?.execution?.status || '').toUpperCase(); if (passed && directional) { if (executionStatus === 'BLOCKED') { current.blockedSignals += 1; } else if (executionStatus === 'SKIPPED') { current.skippedSignals += 1; } else { current.activeSignals += 1; if (executionStatus === 'EXECUTED') { current.executedSignals += 1; } } } byProfile.set(profileId, current); } } return byProfile; }, [botState.symbols]); const canonicalLifecycleTrades = useMemo(() => { if (canonicalLifecycleReady && canonicalSnapshot) { 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 []; }, [canonicalLifecycleReady, canonicalSnapshot]); const hasCanonicalLifecyclePnl = canonicalLifecycleReady; const canonicalAggregate = useMemo( () => aggregateCanonicalLifecycleTrades(canonicalLifecycleTrades), [canonicalLifecycleTrades] ); const winRateWindowConfig = useMemo( () => WIN_RATE_WINDOW_OPTIONS.find((option) => option.key === winRateWindow) || WIN_RATE_WINDOW_OPTIONS[1], [winRateWindow] ); const canonicalWindowTrades = useMemo(() => { if (!winRateWindowConfig.ms) return canonicalLifecycleTrades; const cutoff = Date.now() - winRateWindowConfig.ms; return canonicalLifecycleTrades.filter((trade) => Number(trade.closedAtMs || 0) >= cutoff); }, [canonicalLifecycleTrades, winRateWindowConfig.ms]); const canonicalWindowAggregate = useMemo( () => aggregateCanonicalLifecycleTrades(canonicalWindowTrades), [canonicalWindowTrades] ); const displayRealizedPnl = hasCanonicalLifecyclePnl ? canonicalAggregate.totalPnl : 0; const displayWindowAggregate = hasCanonicalLifecyclePnl ? canonicalWindowAggregate : { totalPnl: 0, tradeCount: 0, wins: 0, winRate: 0, byProfile: {} as Record }; const pnlWindow = useMemo(() => { if (hasCanonicalLifecyclePnl) { const timestamps = canonicalLifecycleTrades .map((trade) => Number(trade.closedAtMs || 0)) .filter((ts) => Number.isFinite(ts) && ts > 0); if (timestamps.length === 0) { return { durationLabel: '-', fromTs: 0, toTs: 0 }; } const fromTs = Math.min(...timestamps); const toTs = Math.max(...timestamps); return { durationLabel: formatDurationCompact(Math.max(0, toTs - fromTs)), fromTs, toTs }; } return { durationLabel: '-', fromTs: 0, toTs: 0 }; }, [canonicalLifecycleTrades, hasCanonicalLifecyclePnl]); const profileTradeStats = useMemo(() => { const aggregates: Record = {}; const source = hasCanonicalLifecyclePnl ? canonicalAggregate.byProfile : {}; for (const [profileId, agg] of Object.entries(source)) { aggregates[profileId] = { tradeCount: agg.tradeCount, wins: agg.wins, winRate: agg.winRate, netPnl: agg.realizedPnl, lastClosedTradeAt: agg.lastClosedTradeAt }; } return aggregates; }, [canonicalAggregate.byProfile, hasCanonicalLifecyclePnl]); const profileWindowStats = useMemo(() => { const aggregates: Record = {}; for (const [profileId, agg] of Object.entries(displayWindowAggregate.byProfile)) { aggregates[profileId] = { tradeCount: agg.tradeCount, winRate: agg.winRate }; } return aggregates; }, [displayWindowAggregate.byProfile]); const unrealizedPnl = useMemo(() => { if (!effectiveOpenByProfile.hasRows) return 0; return Array.from(effectiveOpenByProfile.unrealizedByProfile.values()).reduce((sum, value) => sum + value, 0); }, [effectiveOpenByProfile]); const netPnl = displayRealizedPnl + unrealizedPnl; const profileCapitalRows = useMemo(() => { const usedByProfile = effectiveOpenByProfile.usedByProfile; const suppressedLifecycleNotionalByProfile = effectiveOpenByProfile.suppressedLifecycleNotionalByProfile; const now = Date.now(); return activeProfiles.map((profileRow) => { const allocated = Math.max(0, Number(profileRow.allocated || 0)); const rawUsed = Math.max(0, Number(usedByProfile.get(profileRow.id) || 0)); const suppressedLifecycleNotional = Math.max(0, Number(suppressedLifecycleNotionalByProfile?.get(profileRow.id) || 0)); const used = Math.min(rawUsed, allocated); const overAllocatedAmount = Math.max(0, rawUsed - allocated); const remaining = Math.max(0, allocated - used); const utilizationPct = allocated > 0 ? (used / allocated) * 100 : 0; const tradeStats = profileTradeStats[profileRow.id] || { tradeCount: 0, wins: 0, winRate: 0, netPnl: 0, lastClosedTradeAt: 0 }; const windowStats = profileWindowStats[profileRow.id] || { tradeCount: 0, winRate: 0 }; const openCount = openPositionsByProfile.get(profileRow.id) || 0; const signalStats = profileSignalsByProfile.get(profileRow.id) || { totalSignals: 0, activeSignals: 0, blockedSignals: 0, skippedSignals: 0, executedSignals: 0 }; const cooldownMs = Math.max(0, Number(profileRow.cooldownMinutes || 0)) * 60_000; let cooldownRemainingMs = 0; if (openCount === 0 && cooldownMs > 0 && tradeStats.lastClosedTradeAt > 0) { const elapsed = now - tradeStats.lastClosedTradeAt; if (elapsed < cooldownMs) { cooldownRemainingMs = cooldownMs - elapsed; } } let runtimeState = 'Monitoring (no signal)'; let runtimeDetail = 'Rules are running; waiting for entry setup.'; let runtimeTone: 'running' | 'cooldown' | 'armed' | 'blocked' | 'idle' = 'idle'; if (suppressedLifecycleNotional > 1e-8) { runtimeState = 'Lifecycle sync pending'; runtimeDetail = `Canonical lifecycle still reports ${formatUsd(suppressedLifecycleNotional)} open notional, but live inventory is flat. Utilization is temporarily based on live state.`; runtimeTone = 'blocked'; } else if (overAllocatedAmount > 1e-8) { runtimeState = 'Capital sync warning'; runtimeDetail = `Raw open-position notional is ${formatUsd(overAllocatedAmount)} above allocated capital. Showing capped utilization for clarity.`; runtimeTone = 'armed'; } else if (openCount > 0) { runtimeState = `In Position (${openCount} open)`; runtimeDetail = 'Capital is deployed in active open positions.'; runtimeTone = 'running'; } else if (cooldownRemainingMs > 0) { runtimeState = `Cooldown (wake up in ${Math.ceil(cooldownRemainingMs / 60000)}m)`; runtimeDetail = 'Recent close detected; profile is waiting for cooldown expiry.'; runtimeTone = 'cooldown'; } else if (signalStats.blockedSignals > 0 && signalStats.activeSignals === 0) { runtimeState = 'Signal blocked by guard'; runtimeDetail = 'Directional setup exists, but execution guards blocked entry (pause/capital/risk/cooldown).'; runtimeTone = 'blocked'; } else if (signalStats.executedSignals > 0 && signalStats.activeSignals > 0) { runtimeState = 'Entry submitted, awaiting fill'; runtimeDetail = 'Signal executed recently and waiting for broker fill confirmation.'; runtimeTone = 'armed'; } else if (signalStats.activeSignals > 0) { if (profileRow.allocated <= 0) { runtimeState = 'Signal active, no allocation'; runtimeDetail = 'Allocated capital is 0. Increase allocation to place entries.'; } else if (remaining <= 1e-8) { runtimeState = 'Signal active, capital full'; runtimeDetail = 'No free capital remains in this profile right now.'; } else { runtimeState = 'Signal active, waiting entry'; runtimeDetail = 'No filled entry yet. Utilization will increase once an entry fills.'; } runtimeTone = 'armed'; } return { ...profileRow, allocated, rawUsed, used, overAllocatedAmount, remaining, utilizationPct, winRate: windowStats.winRate, tradeCount: windowStats.tradeCount, netPnl: tradeStats.netPnl, runtimeState, runtimeDetail, runtimeTone }; }); }, [ activeProfiles, effectiveOpenByProfile, profileSignalsByProfile, profileTradeStats, profileWindowStats, openPositionsByProfile ]); const allocatedCapital = useMemo(() => { if (profileCapitalRows.length > 0) { return profileCapitalRows.reduce((sum, row) => sum + row.allocated, 0); } return Number(fallbackCapital || 0); }, [profileCapitalRows, fallbackCapital]); const capitalUsed = useMemo(() => { if (profileCapitalRows.length > 0) { return profileCapitalRows.reduce((sum, row) => sum + row.used, 0); } return 0; }, [profileCapitalRows]); const rawCapitalUsed = useMemo(() => { if (profileCapitalRows.length > 0) { return profileCapitalRows.reduce((sum, row) => sum + row.rawUsed, 0); } return 0; }, [profileCapitalRows]); const overAllocatedCapital = Math.max(0, rawCapitalUsed - capitalUsed); const remainingCapital = Math.max(0, allocatedCapital - capitalUsed); const canonicalUnavailable = !canonicalLifecycleReady || !!canonicalError; return (

Market Readiness

Global view of market conditions and bot status.

{canonicalUnavailable && (
Canonical lifecycle is unavailable{canonicalLoading ? ' (loading)' : ''}. Showing fallback values from DB/runtime sources. {canonicalError ? ` ${canonicalError}` : ''}
)} {canonicalSnapshot?.diagnostics?.truncated && (
Canonical lifecycle snapshot is truncated ({canonicalSnapshot.diagnostics.orderRows} rows). Narrow scope before using this for operational decisions.
)}
Win Rate Window: {WIN_RATE_WINDOW_OPTIONS.map((option) => { const active = option.key === winRateWindow; return ( ); })}
System: {(() => { const mode = botState?.health?.tradingControl?.mode; const wsConnected = connected; if (!wsConnected) return DISCONNECTED; if (mode === 'PAUSED') return PAUSED; if (mode === 'RUNNING' || !mode) return RUNNING; return {mode}; })()}
Mode: {botState.settings.executionMode}
Allocated: {formatUsd(allocatedCapital)} {profileCount > 0 && ({profileCount} profiles)}
Capital Used: {formatUsd(capitalUsed)} {overAllocatedCapital > 1e-8 && ( capped (+{formatUsd(overAllocatedCapital)} raw) )}
Remaining: 1e-8 ? 'status-offline' : 'status-online'}`}> {formatUsd(remainingCapital)}
Uptime: {formatUptime(botState.uptime)}
Realized P&L (90d): = 0 ? 'status-online' : 'status-offline'}`}> {formatUsd(displayRealizedPnl)}
Net P&L (90d): = 0 ? 'status-online' : 'status-offline'}`}> {formatUsd(netPnl)}
Win Rate ({winRateWindowConfig.label}): = 50 ? overviewSuccessText : overviewWarningText }}> {displayWindowAggregate.winRate.toFixed(1)}% {displayWindowAggregate.tradeCount} trades
P&L Duration: 0 ? `${new Date(pnlWindow.fromTs).toLocaleString()} -> ${new Date(pnlWindow.toTs).toLocaleString()}` : 'No realized trades yet'} > {pnlWindow.durationLabel} {pnlWindow.fromTs > 0 ? `From ${new Date(pnlWindow.fromTs).toLocaleDateString()}` : 'Awaiting trade history'}
{/* --- NEW: Alpaca Account Health --- */}
Broker Balance (Alpaca): {botState.accountSnapshot ? formatUsd(botState.accountSnapshot.buying_power) : 'Waiting for snapshot...'} {botState.accountSnapshot && ( Cash: {formatUsd(botState.accountSnapshot.cash)} ({botState.accountSnapshot.currency}) )}
{botState.accountSnapshot && (
Last Sync: {new Date(botState.accountSnapshot.timestamp).toLocaleTimeString()}
)} {/* Parity & Self-Healing Heartbeat */}
Parity Heartbeat: {botState.health?.reconciliationParityMismatchTrades || 0} Mismatches {Number(botState.health?.reconciliationParityAutoClosedTrades || 0) > 0 && ( ({botState.health?.reconciliationParityAutoClosedTrades} Self-Healed) )} {Number(botState.health?.reconciliationParityQuarantinedTrades || 0) > 0 && ( ({botState.health?.reconciliationParityQuarantinedTrades} Quarantined) )}
{/* Recent Failures Summary */} {(botState.orderFailures || []).length > 0 && (
Recent Rejections: {(botState.orderFailures || []).length} Latest: {(botState.orderFailures || [])[0].symbol} ({(botState.orderFailures || [])[0].reason?.substring(0, 20)}...)
)}
{profileCapitalRows.length > 0 ? ( profileCapitalRows.map((row) => { const overAllocated = row.overAllocatedAmount > 1e-8; const stateColor = row.runtimeTone === 'running' ? overviewSuccessText : row.runtimeTone === 'cooldown' ? overviewWarningText : row.runtimeTone === 'armed' ? overviewInfoText : row.runtimeTone === 'blocked' ? overviewDangerText : overviewQuietText; return ( ); }) ) : ( )}
Capital Scope Allocated Used Remaining Utilization Win Rate ({winRateWindowConfig.label}) Realized P&L State
{row.name} {formatUsd(row.allocated)} {formatUsd(row.used)} {formatUsd(row.remaining)} {overAllocated && (
raw +{formatUsd(row.overAllocatedAmount)}
)}
{row.utilizationPct.toFixed(1)}% = 50 ? overviewSuccessText : overviewWarningText }}> {row.winRate.toFixed(1)}% ({row.tradeCount}) = 0 ? overviewSuccessText : overviewDangerText }}> {formatUsd(row.netPnl)}
{row.runtimeState}
{row.runtimeDetail}
Account (Fallback) {formatUsd(allocatedCapital)} {formatUsd(capitalUsed)} 1e-8 ? overviewDangerText : overviewSuccessText }}> {formatUsd(remainingCapital)} {overAllocatedCapital > 1e-8 && (
raw +{formatUsd(overAllocatedCapital)}
)}
{allocatedCapital > 0 ? ((capitalUsed / allocatedCapital) * 100).toFixed(1) : '0.0'}% - = 0 ? overviewSuccessText : overviewDangerText }}>{formatUsd(displayRealizedPnl)} No signal
{/* NEW: Recent Order Rejections List */} {botState.orderFailures && botState.orderFailures.length > 0 && (

Recent Order Rejections

{botState.orderFailures.slice(0, 10).map((fail, idx) => ( ))}
Time Symbol Side Qty Reason Sub-tag Profile
{new Date(fail.timestamp).toLocaleString()} {fail.symbol} {fail.side} {fail.qty} {fail.reason} {compactTag(fail.subTag)} {fail.profileId || 'Unknown'}
)} {/* Symbol Cards — role-aware rendering */} {isAdminView ? ( /* ── ADMIN: Full technical view ─────────────────── */
{symbols.map(symbol => { const data = botState.symbols[symbol]; const profileSignals = Object.values(data.profileSignals || {}); const bias4h = data.indicators.ema50_4h && data.indicators.ema200_4h ? (data.indicators.ema50_4h > data.indicators.ema200_4h ? 'BULLISH' : 'BEARISH') : 'NEUTRAL'; const momentum1h = data.indicators.rsi_1h ? (data.indicators.rsi_1h > 50 ? 'BULLISH' : 'BEARISH') : 'NEUTRAL'; const normalizedSignal = String(data.signal || 'NONE').toUpperCase(); const hasDirectionalSignal = normalizedSignal === 'BUY' || normalizedSignal === 'SELL'; const readinessLabel = normalizedSignal === 'MIXED' ? 'PROFILE SIGNALS MIXED' : hasDirectionalSignal ? 'SIGNAL ACTIVE' : 'NO SIGNAL'; const readinessClass = hasDirectionalSignal ? 'yes' : 'maybe'; return (

{symbol}

{data.signal}
{profileSignals.length > 0 && (
{profileSignals.map((ps, idx) => ( {(ps.profileName || `P${idx + 1}`)}: {ps.signal} ))}
)}
4H Trend Bias{bias4h}
1H Momentum (RSI){momentum1h} {data.indicators.rsi_1h ? `(${data.indicators.rsi_1h.toFixed(1)})` : ''}
Session{data.session}
Volatility{data.volatility}
{data.indicators.ema50_4h &&
EMA50 (4H){data.indicators.ema50_4h.toFixed(2)}
} {data.indicators.ema200_4h &&
EMA200 (4H){data.indicators.ema200_4h.toFixed(2)}
}
Can trade? {readinessLabel}
); })}
) : ( /* ── CONSUMER: Plain-English friendly cards ────── */
{symbols.map(symbol => { const data = botState.symbols[symbol]; const normalizedSignal = String(data.signal || 'NONE').toUpperCase(); // ── Friendly translations ────────────────────────── const bias4h = data.indicators.ema50_4h && data.indicators.ema200_4h ? (data.indicators.ema50_4h > data.indicators.ema200_4h ? 'BULLISH' : 'BEARISH') : 'NEUTRAL'; const directionLabel = bias4h === 'BULLISH' ? 'Uptrend' : bias4h === 'BEARISH' ? 'Downtrend' : 'Sideways'; const directionColor = bias4h === 'BULLISH' ? overviewSuccessText : bias4h === 'BEARISH' ? overviewDangerText : overviewAttentionText; const rsi = data.indicators.rsi_1h || 50; const momentumLabel = rsi > 60 ? 'Building strongly' : rsi > 50 ? 'Gaining' : rsi > 40 ? 'Neutral' : 'Fading'; const momentumColor = rsi > 55 ? overviewSuccessText : rsi > 45 ? overviewAttentionText : overviewDangerText; const sessionIsOff = data.session === 'OFF' || !data.session; const sessionLabel = sessionIsOff ? 'Outside window' : 'Active now'; const sessionColor = sessionIsOff ? overviewBotMuted : overviewSuccessText; const volLabel = data.volatility === 'HIGH' ? 'Very Active' : data.volatility === 'MEDIUM' ? 'Moderate' : data.volatility === 'LOW' ? 'Calm' : 'Normal'; const volColor = data.volatility === 'HIGH' ? overviewAttentionText : data.volatility === 'MEDIUM' ? overviewInfoText : overviewQuietText; const botStatusEmoji = normalizedSignal === 'BUY' ? '📈' : normalizedSignal === 'SELL' ? '📉' : normalizedSignal === 'MIXED' ? '⚡' : '👀'; const botStatusLabel = normalizedSignal === 'BUY' ? 'Entry opportunity detected' : normalizedSignal === 'SELL' ? 'Exit / short signal detected' : normalizedSignal === 'MIXED' ? 'Conditions mixed — holding off' : 'Watching markets for the right setup'; const botStatusColor = normalizedSignal === 'BUY' ? overviewSuccessText : normalizedSignal === 'SELL' ? overviewDangerText : normalizedSignal === 'MIXED' ? overviewAttentionText : overviewBotMuted; const change24h = data.change24h || 0; return (
{/* Top accent line based on signal */}
{/* Header */}
{symbol.split('/')[0]} /{symbol.split('/')[1] || 'USDT'}
= 0 ? overviewSuccessText : overviewDangerText, marginTop: '2px', fontFamily: 'monospace' }}> {change24h >= 0 ? '+' : ''}{change24h.toFixed(2)}% today
{botStatusEmoji}
{/* Friendly metrics */}
{[ { label: 'Market Direction', value: directionLabel, color: directionColor }, { label: 'Short-term Momentum', value: momentumLabel, color: momentumColor }, { label: 'Trading Window', value: sessionLabel, color: sessionColor }, { label: 'Market Activity', value: volLabel, color: volColor }, ].map(({ label, value, color }) => (
{label}
{value}
))}
{/* Bot Status Footer */}
Bot Status
{botStatusLabel}
); })}
)}
); };