import { useEffect, useState, useRef } from 'react' import { mutate } from 'swr' import { api } from '../lib/api' import { ChartTabs } from '../components/charts/ChartTabs' import { DecisionCard } from '../components/trader/DecisionCard' import { PositionHistory } from '../components/trader/PositionHistory' import { PunkAvatar, getTraderAvatar } from '../components/common/PunkAvatar' import { confirmToast, notify } from '../lib/notify' import { formatPrice, formatQuantity } from '../utils/format' import { t, type Language } from '../i18n/translations' import { LogOut, Loader2, Eye, EyeOff, Copy, Check } from 'lucide-react' import { DeepVoidBackground } from '../components/common/DeepVoidBackground' import { NofxSelect } from '../components/ui/select' import { GridRiskPanel } from '../components/strategy/GridRiskPanel' import type { SystemStatus, AccountInfo, Position, DecisionRecord, Statistics, TraderInfo, Exchange, } from '../types' // --- Helper Functions --- // Get friendly AI model display name function getModelDisplayName(modelId: string): string { switch (modelId.toLowerCase()) { case 'deepseek': return 'DeepSeek' case 'qwen': return 'Qwen' case 'claude': return 'Claude' default: return modelId.toUpperCase() } } // Helper function to get exchange display name from exchange ID (UUID) function getExchangeDisplayNameFromList( exchangeId: string | undefined, exchanges: Exchange[] | undefined ): string { if (!exchangeId) return 'Unknown' const exchange = exchanges?.find((e) => e.id === exchangeId) if (!exchange) return exchangeId.substring(0, 8).toUpperCase() + '...' const typeName = exchange.exchange_type?.toUpperCase() || exchange.name return exchange.account_name ? `${typeName} - ${exchange.account_name}` : typeName } // Helper function to get exchange type from exchange ID (UUID) - for kline charts function getExchangeTypeFromList( exchangeId: string | undefined, exchanges: Exchange[] | undefined ): string { if (!exchangeId) return 'binance' const exchange = exchanges?.find((e) => e.id === exchangeId) if (!exchange) return 'binance' // Default to binance for charts return exchange.exchange_type?.toLowerCase() || 'binance' } // Helper function to check if exchange is a perp-dex type (wallet-based) function isPerpDexExchange(exchangeType: string | undefined): boolean { if (!exchangeType) return false const perpDexTypes = ['hyperliquid', 'lighter', 'aster'] return perpDexTypes.includes(exchangeType.toLowerCase()) } // Helper function to get wallet address for perp-dex exchanges function getWalletAddress(exchange: Exchange | undefined): string | undefined { if (!exchange) return undefined const type = exchange.exchange_type?.toLowerCase() switch (type) { case 'hyperliquid': return exchange.hyperliquidWalletAddr case 'lighter': return exchange.lighterWalletAddr case 'aster': return exchange.asterSigner default: return undefined } } // Helper function to truncate wallet address for display function truncateAddress(address: string, startLen = 6, endLen = 4): string { if (address.length <= startLen + endLen + 3) return address return `${address.slice(0, startLen)}...${address.slice(-endLen)}` } // --- Components --- interface TraderDashboardPageProps { selectedTrader?: TraderInfo traders?: TraderInfo[] tradersError?: Error selectedTraderId?: string onTraderSelect: (traderId: string) => void onNavigateToTraders: () => void status?: SystemStatus account?: AccountInfo accountFailed?: boolean positions?: Position[] positionsFailed?: boolean decisions?: DecisionRecord[] decisionsFailed?: boolean decisionsLimit: number onDecisionsLimitChange: (limit: number) => void stats?: Statistics lastUpdate: string language: Language exchanges?: Exchange[] } export function TraderDashboardPage({ selectedTrader, status, account, accountFailed, positions, positionsFailed, decisions, decisionsFailed, decisionsLimit, onDecisionsLimitChange, lastUpdate, language, traders, tradersError, selectedTraderId, onTraderSelect, onNavigateToTraders, exchanges, }: TraderDashboardPageProps) { const [closingPosition, setClosingPosition] = useState(null) const [selectedChartSymbol, setSelectedChartSymbol] = useState(undefined) const [chartUpdateKey, setChartUpdateKey] = useState(0) const chartSectionRef = useRef(null) const [showWalletAddress, setShowWalletAddress] = useState(false) const [copiedAddress, setCopiedAddress] = useState(false) // Current positions pagination const [positionsPageSize, setPositionsPageSize] = useState(20) const [positionsCurrentPage, setPositionsCurrentPage] = useState(1) // Calculate paginated positions const totalPositions = positions?.length || 0 const totalPositionPages = Math.ceil(totalPositions / positionsPageSize) const paginatedPositions = positions?.slice( (positionsCurrentPage - 1) * positionsPageSize, positionsCurrentPage * positionsPageSize ) || [] // Reset page when positions change useEffect(() => { setPositionsCurrentPage(1) }, [selectedTraderId, positionsPageSize]) // Auto-set chart symbol for grid trading useEffect(() => { if (status?.strategy_type === 'grid_trading' && status?.grid_symbol) { setSelectedChartSymbol(status.grid_symbol) } }, [status?.strategy_type, status?.grid_symbol]) // Get current exchange info for perp-dex wallet display const currentExchange = exchanges?.find( (e) => e.id === selectedTrader?.exchange_id ) const walletAddress = getWalletAddress(currentExchange) const isPerpDex = isPerpDexExchange(currentExchange?.exchange_type) // Copy wallet address to clipboard const handleCopyAddress = async () => { if (!walletAddress) return try { await navigator.clipboard.writeText(walletAddress) setCopiedAddress(true) setTimeout(() => setCopiedAddress(false), 2000) } catch (err) { console.error('Failed to copy address:', err) } } // Handle symbol click from Decision Card const handleSymbolClick = (symbol: string) => { // Set the selected symbol setSelectedChartSymbol(symbol) // Scroll to chart section setTimeout(() => { chartSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, 100) } // Close position handler const handleClosePosition = async (symbol: string, side: string) => { if (!selectedTraderId) return const sideLabel = side === 'LONG' ? 'LONG' : 'SHORT' const confirmMsg = t('traderDashboard.confirmClosePosition', language, { symbol, side: sideLabel }) const confirmed = await confirmToast(confirmMsg, { title: t('traderDashboard.confirmClose', language), okText: t('traderDashboard.confirm', language), cancelText: t('traderDashboard.cancel', language), }) if (!confirmed) return setClosingPosition(symbol) try { await api.closePosition(selectedTraderId, symbol, side) notify.success(t('traderDashboard.positionClosed', language)) // Use SWR mutate to refresh data instead of reloading page await Promise.all([ mutate(`positions-${selectedTraderId}`), mutate(`account-${selectedTraderId}`), ]) } catch (err: unknown) { const errorMsg = err instanceof Error ? err.message : t('traderDashboard.closeFailed', language) notify.error(errorMsg) } finally { setClosingPosition(null) } } // If API failed with error, show empty state (likely backend not running) if (tradersError) { return (

{t('traderDashboard.connectionFailed', language)}

{t('traderDashboard.connectionFailedDesc', language)}

) } // If traders is loaded and empty, show empty state if (traders && traders.length === 0) { return (

{t('dashboardEmptyTitle', language)}

{t('dashboardEmptyDescription', language)}

) } // If traders is still loading or selectedTrader is not ready, show skeleton if (!selectedTrader) { return (
{[1, 2, 3, 4].map((i) => (
))}
) } return (
{/* Trader Header */}

{selectedTrader.trader_name}
ID: {selectedTrader.trader_id.slice(0, 8)}...

{/* Trader Selector */} {traders && traders.length > 0 && (
onTraderSelect(val)} options={traders.map(t => ({ value: t.trader_id, label: t.trader_name }))} className="bg-transparent text-sm font-medium cursor-pointer transition-colors text-nofx-text-main px-2 py-1" />
)} {/* Wallet Address Display for Perp-DEX */} {exchanges && isPerpDex && (
{walletAddress ? ( <> {showWalletAddress ? walletAddress : truncateAddress(walletAddress)} ) : ( {t('traderDashboard.noAddressConfigured', language)} )}
)}
AI Model: {getModelDisplayName( selectedTrader.ai_model.split('_').pop() || selectedTrader.ai_model )} Exchange: {getExchangeDisplayNameFromList( selectedTrader.exchange_id, exchanges )} Strategy: {selectedTrader.strategy_name || 'No Strategy'} {status && (
Cycles: {status.call_count} Runtime: {status.runtime_minutes} min
)}
{/* Debug Info */}
SYSTEM_STATUS::ONLINE {account ? (
LAST_UPDATE::{lastUpdate} EQ::{account.total_equity?.toFixed(2)} PNL::{account.total_pnl?.toFixed(2)}
) : accountFailed ? ( {t('traderDashboard.accountFetchFailed', language)} ) : (
)}
{/* Account Overview */}
0} icon="๐Ÿ’ฐ" loading={!account && !accountFailed} /> = 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) ?? '--'}`} unit="USDT" change={account ? (account.total_pnl_pct || 0) : undefined} positive={(account?.total_pnl ?? 0) >= 0} icon="๐Ÿ“ˆ" loading={!account && !accountFailed} />
{/* Grid Risk Panel - Only show for grid trading strategy */} {status?.strategy_type === 'grid_trading' && selectedTraderId && (
)} {/* Main Content Area */}
{/* Left Column: Charts + Positions */}
{/* Chart Tabs (Equity / K-line) */}
{/* Current Positions */}

โ—ˆ {t('currentPositions', language)}

{positions && positions.length > 0 && (
{positions.length} {t('active', language)}
)}
{positions && positions.length > 0 ? (
{paginatedPositions.map((pos, i) => ( { setSelectedChartSymbol(pos.symbol) setChartUpdateKey(Date.now()) if (chartSectionRef.current) { chartSectionRef.current.scrollIntoView({ behavior: 'smooth', block: 'start', }) } }} > ))}
{t('symbol', language)} {t('side', language)} {t('traderDashboard.action', language)} {t('traderDashboard.entry', language)} {t('traderDashboard.mark', language)} {t('traderDashboard.qty', language)} {t('traderDashboard.value', language)} {t('traderDashboard.lev', language)} {t('traderDashboard.uPnL', language)} {t('traderDashboard.liq', language)}
{pos.symbol} {t(pos.side === 'long' ? 'long' : 'short', language)} {formatPrice(pos.entry_price)} {formatPrice(pos.mark_price)} {formatQuantity(pos.quantity)} {(pos.quantity * pos.mark_price).toFixed(2)} {pos.leverage}x = 0 ? 'text-nofx-green shadow-nofx-green' : 'text-nofx-red shadow-nofx-red'}`} style={{ textShadow: pos.unrealized_pnl >= 0 ? '0 0 10px rgba(14,203,129,0.3)' : '0 0 10px rgba(246,70,93,0.3)' }} > {pos.unrealized_pnl >= 0 ? '+' : ''} {pos.unrealized_pnl.toFixed(2)} {formatPrice(pos.liquidation_price)}
{/* Pagination footer */} {totalPositions > 10 && (
{t('traderDashboard.showingPositions', language, { shown: paginatedPositions.length, total: totalPositions })}
{t('traderDashboard.perPage', language)}: setPositionsPageSize(Number(val))} options={[{ value: 20, label: '20' }, { value: 50, label: '50' }, { value: 100, label: '100' }]} className="bg-black/40 border border-white/10 rounded px-2 py-1 text-xs text-nofx-text-main transition-colors" />
{totalPositionPages > 1 && (
{['ยซ', 'โ€น', `${positionsCurrentPage} / ${totalPositionPages}`, 'โ€บ', 'ยป'].map((label, idx) => { const isText = idx === 2; const isFirst = idx === 0; const isPrev = idx === 1; const isNext = idx === 3; const isLast = idx === 4; if (isText) return {label}; let onClick = () => { }; let disabled = false; if (isFirst) { onClick = () => setPositionsCurrentPage(1); disabled = positionsCurrentPage === 1; } if (isPrev) { onClick = () => setPositionsCurrentPage(p => Math.max(1, p - 1)); disabled = positionsCurrentPage === 1; } if (isNext) { onClick = () => setPositionsCurrentPage(p => Math.min(totalPositionPages, p + 1)); disabled = positionsCurrentPage === totalPositionPages; } if (isLast) { onClick = () => setPositionsCurrentPage(totalPositionPages); disabled = positionsCurrentPage === totalPositionPages; } return ( ) })}
)}
)}
) : positionsFailed ? (
โš ๏ธ
{t('traderDashboard.positionsFetchFailed', language)}
) : (
๐Ÿ“Š
{t('noPositions', language)}
{t('noActivePositions', language)}
)}
{/* Right Column: Recent Decisions */}
{/* Header */}
๐Ÿง 

{t('recentDecisions', language)}

{decisions && decisions.length > 0 && (
{t('lastCycles', language, { count: decisions.length })}
)}
{/* Limit Selector */} onDecisionsLimitChange(Number(val))} options={[{ value: 5, label: '5' }, { value: 10, label: '10' }, { value: 20, label: '20' }, { value: 50, label: '50' }, { value: 100, label: '100' }]} className="px-3 py-1.5 rounded-lg text-sm font-medium cursor-pointer transition-all bg-black/40 text-nofx-text-main border border-white/10 hover:border-nofx-accent" />
{/* Decisions List - Scrollable */}
{decisions && decisions.length > 0 ? ( decisions.map((decision, i) => ( )) ) : decisionsFailed ? (
โš ๏ธ
{t('traderDashboard.decisionsFetchFailed', language)}
) : (
๐Ÿง 
{t('noDecisionsYet', language)}
{t('aiDecisionsWillAppear', language)}
)}
{/* Position History Section */} {selectedTraderId && (

๐Ÿ“œ {t('positionHistory.title', language)}

)}
) } // Stat Card Component - Deep Void Style function StatCard({ title, value, unit, change, positive, subtitle, icon, loading, }: { title: string value: string unit?: string change?: number positive?: boolean subtitle?: string icon?: string loading?: boolean }) { return (
{icon}
{title}
{loading ? (
) : ( <>
{value}
{unit && {unit}}
{change !== undefined && (
{positive ? 'โ–ฒ' : 'โ–ผ'} {positive ? '+' : ''}{change.toFixed(2)}%
)} {subtitle && (
{subtitle}
)} )}
) }