import { useEffect, useState, useRef } from 'react' import { mutate } from 'swr' import { api } from '../lib/api' import { ChartTabs } from '../components/ChartTabs' import { DecisionCard } from '../components/DecisionCard' import { PositionHistory } from '../components/PositionHistory' import { PunkAvatar, getTraderAvatar } from '../components/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/DeepVoidBackground' import { GridRiskPanel } from '../components/strategy/GridRiskPanel' import type { SystemStatus, AccountInfo, Position, DecisionRecord, Statistics, TraderInfo, Exchange, } from '../types' // --- Helper Functions --- // 获取友好的AI模型名称 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 positions?: Position[] decisions?: DecisionRecord[] decisionsLimit: number onDecisionsLimitChange: (limit: number) => void stats?: Statistics lastUpdate: string language: Language exchanges?: Exchange[] } export function TraderDashboardPage({ selectedTrader, status, account, positions, decisions, 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) } // 平仓操作 const handleClosePosition = async (symbol: string, side: string) => { if (!selectedTraderId) return const confirmMsg = language === 'zh' ? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?` : `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?` const confirmed = await confirmToast(confirmMsg, { title: language === 'zh' ? '确认平仓' : 'Confirm Close', okText: language === 'zh' ? '确认' : 'Confirm', cancelText: language === 'zh' ? '取消' : 'Cancel', }) if (!confirmed) return setClosingPosition(symbol) try { await api.closePosition(selectedTraderId, symbol, side) notify.success( language === 'zh' ? '平仓成功' : 'Position closed successfully' ) // 使用 SWR mutate 刷新数据而非重新加载页面 await Promise.all([ mutate(`positions-${selectedTraderId}`), mutate(`account-${selectedTraderId}`), ]) } catch (err: unknown) { const errorMsg = err instanceof Error ? err.message : language === 'zh' ? '平仓失败' : 'Failed to close position' notify.error(errorMsg) } finally { setClosingPosition(null) } } // If API failed with error, show empty state (likely backend not running) if (tradersError) { return (

{language === 'zh' ? '无法连接到服务器' : 'Connection Failed'}

{language === 'zh' ? '请确认后端服务已启动。' : 'Please check if the backend service is running.'}

) } // 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 && (
)} {/* Wallet Address Display for Perp-DEX */} {exchanges && isPerpDex && (
{walletAddress ? ( <> {showWalletAddress ? walletAddress : truncateAddress(walletAddress)} ) : ( {language === 'zh' ? '未配置地址' : 'No address configured'} )}
)}
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 */} {account && (
SYSTEM_STATUS::ONLINE
LAST_UPDATE::{lastUpdate} EQ::{account?.total_equity?.toFixed(2)} PNL::{account?.total_pnl?.toFixed(2)}
)} {/* Account Overview */}
0} icon="💰" /> = 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'}`} unit="USDT" change={account?.total_pnl_pct || 0} positive={(account?.total_pnl ?? 0) >= 0} icon="📈" />
{/* 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)} {language === 'zh' ? '操作' : 'Action'} {language === 'zh' ? '入场价' : 'Entry'} {language === 'zh' ? '标记价' : 'Mark'} {language === 'zh' ? '数量' : 'Qty'} {language === 'zh' ? '价值' : 'Value'} {language === 'zh' ? '杠杆' : 'Lev.'} {language === 'zh' ? '未实现盈亏' : 'uPnL'} {language === 'zh' ? '强平价' : 'Liq.'}
{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 && (
{language === 'zh' ? `显示 ${paginatedPositions.length} / ${totalPositions} 个持仓` : `Showing ${paginatedPositions.length} of ${totalPositions} positions`}
{language === 'zh' ? '每页' : 'Per page'}:
{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 ( ) })}
)}
)}
) : (
📊
{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 */}
{/* Decisions List - Scrollable */}
{decisions && decisions.length > 0 ? ( decisions.map((decision, i) => ( )) ) : (
🧠
{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, }: { title: string value: string unit?: string change?: number positive?: boolean subtitle?: string icon?: string }) { return (
{icon}
{title}
{value}
{unit && {unit}}
{change !== undefined && (
{positive ? '▲' : '▼'} {positive ? '+' : ''}{change.toFixed(2)}%
)} {subtitle && (
{subtitle}
)}
) }