diff --git a/web/src/App.tsx b/web/src/App.tsx index 07e582e2..920d1a7b 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -3,7 +3,8 @@ import { motion, AnimatePresence } from 'framer-motion' // Force HMR Update - Reliability Fix v3 (Emergency Recovery) import useSWR, { mutate } from 'swr' import { api } from './lib/api' -import { ChartTabs } from './components/ChartTabs' +import { TraderDashboardPage } from './pages/TraderDashboardPage' + import { AITradersPage } from './components/AITradersPage' import { LoginPage } from './components/LoginPage' import { RegisterPage } from './components/RegisterPage' @@ -22,12 +23,10 @@ import { ConfirmDialogProvider } from './components/ConfirmDialog' import { t, type Language } from './i18n/translations' import { confirmToast, notify } from './lib/notify' import { useSystemConfig } from './hooks/useSystemConfig' -import { DecisionCard } from './components/DecisionCard' -import { PositionHistory } from './components/PositionHistory' -import { PunkAvatar, getTraderAvatar } from './components/PunkAvatar' + import { OFFICIAL_LINKS } from './constants/branding' import { BacktestPage } from './components/BacktestPage' -import { LogOut, Loader2, Eye, EyeOff, Copy, Check } from 'lucide-react' +import { LogOut, Loader2 } from 'lucide-react' import type { SystemStatus, AccountInfo, @@ -50,73 +49,7 @@ type Page = | 'login' | 'register' -// 获取友好的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)}` -} function App() { const { language, setLanguage } = useLanguage() @@ -467,13 +400,7 @@ function App() { /> {/* Main Content with Page Transitions */} -
+
) : ( - 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[] -}) { - const [closingPosition, setClosingPosition] = useState(null) - const [selectedChartSymbol, setSelectedChartSymbol] = useState< - string | undefined - >(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]) - - // 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 ( -
-
- {/* Icon */} -
- - - -
- - {/* Title */} -

- {t('dashboardEmptyTitle', language)} -

- - {/* Description */} -

- {t('dashboardEmptyDescription', language)} -

- - {/* CTA Button */} - -
-
- ) - } - - // If traders is loaded and empty, show empty state - if (traders && traders.length === 0) { - return ( -
-
- {/* Icon */} -
- - - -
- - {/* Title */} -

- {t('dashboardEmptyTitle', language)} -

- - {/* Description */} -

- {t('dashboardEmptyDescription', language)} -

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

- - {selectedTrader.trader_name} -

- -
- {/* Trader Selector */} - {traders && traders.length > 0 && ( -
- - {t('switchTrader', language)}: - - -
- )} - - {/* 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 && ( -
-
- 🔄 Last Update: {lastUpdate} | Total Equity:{' '} - {account?.total_equity?.toFixed(2) || '0.00'} | Available:{' '} - {account?.available_balance?.toFixed(2) || '0.00'} | P&L:{' '} - {account?.total_pnl?.toFixed(2) || '0.00'} ( - {account?.total_pnl_pct?.toFixed(2) || '0.00'}%) -
-
- )} - - {/* Account Overview */} -
- 0} - /> - - = 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'} USDT`} - change={account?.total_pnl_pct || 0} - positive={(account?.total_pnl ?? 0) >= 0} - /> - -
- - {/* 主要内容区:左右分屏 */} -
- {/* 左侧:图表 + 持仓 */} -
- {/* 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()) - // Smooth scroll to chart with ref - 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 - )} - - - - - {pos.entry_price.toFixed(4)} - - {pos.mark_price.toFixed(4)} - - {pos.quantity.toFixed(4)} - - {(pos.quantity * pos.mark_price).toFixed(2)} - - {pos.leverage}x - - = 0 ? '#0ECB81' : '#F6465D', - fontWeight: 'bold', - }} - > - {pos.unrealized_pnl >= 0 ? '+' : ''} - {pos.unrealized_pnl.toFixed(2)} - - - {pos.liquidation_price.toFixed(4)} -
-
- {/* Pagination footer - only show when there are many positions */} - {totalPositions > 10 && ( -
- - {language === 'zh' - ? `显示 ${paginatedPositions.length} / ${totalPositions} 个持仓` - : `Showing ${paginatedPositions.length} of ${totalPositions} positions`} - -
- {/* Page size selector */} -
- - {language === 'zh' ? '每页' : 'Per page'}: - - -
- {/* Page navigation */} - {totalPositionPages > 1 && ( -
- - - - {positionsCurrentPage} / {totalPositionPages} - - - -
- )} -
-
- )} -
- ) : ( -
-
📊
-
- {t('noPositions', language)} -
-
- {t('noActivePositions', language)} -
-
- )} -
-
- {/* 左侧结束 */} - - {/* 右侧:Recent Decisions - 卡片容器 */} -
- {/* 标题 */} -
-
- 🧠 -
-
-

- {t('recentDecisions', language)} -

- {decisions && decisions.length > 0 && ( -
- {t('lastCycles', language, { count: decisions.length })} -
- )} -
- {/* 数量选择器 */} - -
- - {/* 决策列表 - 可滚动 */} -
- {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 - Binance Style Enhanced -function StatCard({ - title, - value, - change, - positive, - subtitle, -}: { - title: string - value: string - change?: number - positive?: boolean - subtitle?: string -}) { - return ( -
-
- {title} -
-
- {value} -
- {change !== undefined && ( -
-
- {positive ? '▲' : '▼'} {positive ? '+' : ''} - {change.toFixed(2)}% -
-
- )} - {subtitle && ( -
- {subtitle} -
- )} -
- ) -} // Wrap App with providers export default function AppWithProviders() { diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index 7ce1f1ed..f30f254c 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -14,6 +14,7 @@ import { useAuth } from '../contexts/AuthContext' import { getExchangeIcon } from './ExchangeIcons' import { getModelIcon } from './ModelIcons' import { TraderConfigModal } from './TraderConfigModal' +import { DeepVoidBackground } from './DeepVoidBackground' import { ExchangeConfigModal } from './traders/ExchangeConfigModal' import { PunkAvatar, getTraderAvatar } from './PunkAvatar' import { @@ -616,12 +617,12 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { allModels?.map((m) => m.id === modelId ? { - ...m, - apiKey, - customApiUrl: customApiUrl || '', - customModelName: customModelName || '', - enabled: true, - } + ...m, + apiKey, + customApiUrl: customApiUrl || '', + customModelName: customModelName || '', + enabled: true, + } : m ) || [] } else { @@ -802,271 +803,324 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { } return ( -
- {/* Header */} -
-
-
- + +
+ {/* Header - Terminal Style */} +
+
+
+
+
+ +
+
+
+

+ {t('aiTraders', language)} + + {traders?.length || 0} ACTIVE_NODES + +

+

+ + SYSTEM_READY +

+
-
-

+ + + + +

-

- {t('manageAITraders', language)} -

+
+
-
- + {/* Configuration Status Grid */} +
+ {/* AI Models Card */} +
+
+ +

+ {t('aiModels', language)} +

+
- - - -
-
- - {/* Configuration Status */} -
- {/* AI Models */} -
-

- - {t('aiModels', language)} -

-
- {configuredModels.map((model) => { - const inUse = isModelInUse(model.id) - const usageInfo = getModelUsageInfo(model.id) - return ( -
handleModelClick(model.id)} - > -
-
- {getModelIcon(model.provider || model.id, { - width: 28, - height: 28, - }) || ( -
- {getShortName(model.name)[0]} +
+ {configuredModels.map((model) => { + const inUse = isModelInUse(model.id) + const usageInfo = getModelUsageInfo(model.id) + return ( +
handleModelClick(model.id)} + > +
+
+
+
+ {getModelIcon(model.provider || model.id, { width: 20, height: 20 }) || ( + {getShortName(model.name)[0]} + )}
+
+ +
+
+ {getShortName(model.name)} +
+
+ {model.customModelName || AI_PROVIDER_CONFIG[model.provider]?.defaultModel || ''} +
+
+
+ +
+ {usageInfo.totalCount > 0 ? ( + 0 + ? 'bg-green-500/10 border-green-500/30 text-green-400' + : 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400' + }`}> + {usageInfo.runningCount}/{usageInfo.totalCount} ACTIVE + + ) : ( + + {language === 'zh' ? '就绪' : 'STANDBY'} + )}
-
-
- {getShortName(model.name)} +
+ ) + })} + + {configuredModels.length === 0 && ( +
+ +
{t('noModelsConfigured', language)}
+
+ )} +
+
+ + {/* Exchanges Card */} +
+
+ +

+ {t('exchanges', language)} +

+
+ +
+ {configuredExchanges.map((exchange) => { + const inUse = isExchangeInUse(exchange.id) + const usageInfo = getExchangeUsageInfo(exchange.id) + return ( +
handleExchangeClick(exchange.id)} + > +
+
+
+
+ {getExchangeIcon(exchange.exchange_type || exchange.id, { width: 20, height: 20 })} +
-
- {model.customModelName || AI_PROVIDER_CONFIG[model.provider]?.defaultModel || ''} -
-
- {usageInfo.totalCount > 0 ? ( - 0 - ? { background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' } - : { background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' } - } - > - {usageInfo.runningCount > 0 - ? `${usageInfo.runningCount}/${usageInfo.totalCount} ${language === 'zh' ? '运行中' : 'Running'}` - : `${usageInfo.totalCount} ${language === 'zh' ? '个交易员' : usageInfo.totalCount === 1 ? 'Trader' : 'Traders'}` - } + +
+
+ {exchange.exchange_type?.toUpperCase() || getShortName(exchange.name)} + + {exchange.account_name || 'DEFAULT'} - ) : ( - - {model.enabled - ? (language === 'zh' ? '空闲' : 'Idle') - : (language === 'zh' ? '已配置' : 'Configured')} - - )} +
+
+ {exchange.type?.toUpperCase() || 'CEX'} +
+ +
+ {/* Wallet Address Display Logic */} + {(() => { + const walletAddr = exchange.hyperliquidWalletAddr || exchange.asterUser || exchange.lighterWalletAddr + if (exchange.type !== 'dex' || !walletAddr) return null + const isVisible = visibleExchangeAddresses.has(exchange.id) + const isCopied = copiedId === `exchange-${exchange.id}` + + return ( +
e.stopPropagation()}> + + {isVisible ? walletAddr : truncateAddress(walletAddr)} + + + +
+ ) + })()} + + {usageInfo.totalCount > 0 ? ( + 0 + ? 'bg-green-500/10 border-green-500/30 text-green-400' + : 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400' + }`}> + {usageInfo.runningCount}/{usageInfo.totalCount} ACTIVE + + ) : ( + + {language === 'zh' ? '就绪' : 'STANDBY'} + + )} +
-
+ ) + })} + {configuredExchanges.length === 0 && ( +
+ +
{t('noExchangesConfigured', language)}
- ) - })} - {configuredModels.length === 0 && ( -
- -
- {t('noModelsConfigured', language)} -
-
- )} + )} +
- {/* Exchanges */} -
-

- - {t('exchanges', language)} -

-
- {configuredExchanges.map((exchange) => { - const inUse = isExchangeInUse(exchange.id) - const usageInfo = getExchangeUsageInfo(exchange.id) - return ( + {/* Traders List */} +
+
+

+ + {t('currentTraders', language)} +

+
+ + {isTradersLoading ? ( + /* Loading Skeleton */ +
+ {[1, 2, 3].map((i) => (
handleExchangeClick(exchange.id)} > - {/* Left: Icon + Name + Type */} -
-
- {getExchangeIcon(exchange.exchange_type || exchange.id, { width: 28, height: 28 })} +
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) : traders && traders.length > 0 ? ( +
+ {traders.map((trader) => ( +
+
+
+ +
- {exchange.exchange_type?.toUpperCase() || getShortName(exchange.name)} - - - {exchange.account_name || 'Default'} - + {trader.trader_name}
-
- {exchange.type?.toUpperCase() || 'CEX'} - - {usageInfo.totalCount > 0 ? ( - 0 - ? { background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' } - : { background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' } - } - > - {usageInfo.runningCount > 0 - ? `${usageInfo.runningCount}/${usageInfo.totalCount} ${language === 'zh' ? '运行中' : 'Running'}` - : `${usageInfo.totalCount} ${language === 'zh' ? '个交易员' : usageInfo.totalCount === 1 ? 'Trader' : 'Traders'}` - } - - ) : ( - - {exchange.enabled - ? (language === 'zh' ? '空闲' : 'Idle') - : (language === 'zh' ? '已配置' : 'Configured')} - - )} +
+ {getModelDisplayName( + trader.ai_model.split('_').pop() || trader.ai_model + )}{' '} + Model • {getExchangeDisplayName(trader.exchange_id, allExchanges)}
- {/* Right: Wallet Address + Status Dot */} -
- {/* Wallet address for DEX exchanges */} - {(() => { - const walletAddr = exchange.hyperliquidWalletAddr || exchange.asterUser || exchange.lighterWalletAddr - if (exchange.type !== 'dex' || !walletAddr) return null - const isVisible = visibleExchangeAddresses.has(exchange.id) - const isCopied = copiedId === `exchange-${exchange.id}` +
+ {/* Wallet Address for Perp-DEX - placed before status for alignment */} + {(() => { + const exchange = allExchanges.find(e => e.id === trader.exchange_id) + const walletAddr = getWalletAddress(exchange) + const isPerpDex = isPerpDexExchange(exchange?.exchange_type) + if (!isPerpDex || !walletAddr) return null + + const isVisible = visibleTraderAddresses.has(trader.trader_id) + const isCopied = copiedId === trader.trader_id return (
e.stopPropagation()} > {isVisible ? walletAddr : truncateAddress(walletAddr)} @@ -1084,7 +1137,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { type="button" onClick={(e) => { e.stopPropagation() - toggleExchangeAddressVisibility(exchange.id) + toggleTraderAddressVisibility(trader.trader_id) }} className="p-0.5 rounded hover:bg-gray-700 transition-colors" title={isVisible ? (language === 'zh' ? '隐藏' : 'Hide') : (language === 'zh' ? '显示' : 'Show')} @@ -1099,7 +1152,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { type="button" onClick={(e) => { e.stopPropagation() - handleCopyAddress(`exchange-${exchange.id}`, walletAddr) + handleCopyAddress(trader.trader_id, walletAddr) }} className="p-0.5 rounded hover:bg-gray-700 transition-colors" title={language === 'zh' ? '复制' : 'Copy'} @@ -1113,381 +1166,221 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
) })()} -
-
-
- ) - })} - {configuredExchanges.length === 0 && ( -
- -
- {t('noExchangesConfigured', language)} -
-
- )} -
-
-
- - {/* Traders List */} -
-
-

- - {t('currentTraders', language)} -

-
- - {isTradersLoading ? ( - /* Loading Skeleton */ -
- {[1, 2, 3].map((i) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))} -
- ) : traders && traders.length > 0 ? ( -
- {traders.map((trader) => ( -
-
-
- - -
-
-
- {trader.trader_name} -
-
- {getModelDisplayName( - trader.ai_model.split('_').pop() || trader.ai_model - )}{' '} - Model • {getExchangeDisplayName(trader.exchange_id, allExchanges)} -
-
-
- -
- {/* Wallet Address for Perp-DEX - placed before status for alignment */} - {(() => { - const exchange = allExchanges.find(e => e.id === trader.exchange_id) - const walletAddr = getWalletAddress(exchange) - const isPerpDex = isPerpDexExchange(exchange?.exchange_type) - if (!isPerpDex || !walletAddr) return null - - const isVisible = visibleTraderAddresses.has(trader.trader_id) - const isCopied = copiedId === trader.trader_id - - return ( -
- - {isVisible ? walletAddr : truncateAddress(walletAddr)} - - - -
- ) - })()} - {/* Status */} -
- {/*
+ {/* Status */} +
+ {/*
{t('status', language)}
*/} -
- {trader.is_running - ? t('running', language) - : t('stopped', language)} -
-
- - {/* Actions: 禁止换行,超出横向滚动 */} -
- + > + {trader.is_running + ? t('running', language) + : t('stopped', language)} +
+
- + {/* Actions: 禁止换行,超出横向滚动 */} +
+ - + + + } + > + {trader.is_running + ? t('stop', language) + : t('start', language)} + - + } + title={trader.show_in_competition !== false ? '在竞技场显示' : '在竞技场隐藏'} + > + {trader.show_in_competition !== false ? ( + + ) : ( + + )} + - + +
-
- ))} -
- ) : ( -
- -
- {t('noTraders', language)} + ))}
-
- {t('createFirstTrader', language)} -
- {(configuredModels.length === 0 || - configuredExchanges.length === 0) && ( -
- {configuredModels.length === 0 && - configuredExchanges.length === 0 - ? t('configureModelsAndExchangesFirst', language) - : configuredModels.length === 0 - ? t('configureModelsFirst', language) - : t('configureExchangesFirst', language)} + ) : ( +
+ +
+ {t('noTraders', language)}
- )} -
+
+ {t('createFirstTrader', language)} +
+ {(configuredModels.length === 0 || + configuredExchanges.length === 0) && ( +
+ {configuredModels.length === 0 && + configuredExchanges.length === 0 + ? t('configureModelsAndExchangesFirst', language) + : configuredModels.length === 0 + ? t('configureModelsFirst', language) + : t('configureExchangesFirst', language)} +
+ )} +
+ )} +
+ + {/* Create Trader Modal */} + {showCreateModal && ( + setShowCreateModal(false)} + /> + )} + + {/* Edit Trader Modal */} + {showEditModal && editingTrader && ( + { + setShowEditModal(false) + setEditingTrader(null) + }} + /> + )} + + {/* Model Configuration Modal */} + {showModelModal && ( + { + setShowModelModal(false) + setEditingModel(null) + }} + language={language} + /> + )} + + {/* Exchange Configuration Modal */} + {showExchangeModal && ( + { + setShowExchangeModal(false) + setEditingExchange(null) + }} + language={language} + /> )}
- - {/* Create Trader Modal */} - {showCreateModal && ( - setShowCreateModal(false)} - /> - )} - - {/* Edit Trader Modal */} - {showEditModal && editingTrader && ( - { - setShowEditModal(false) - setEditingTrader(null) - }} - /> - )} - - {/* Model Configuration Modal */} - {showModelModal && ( - { - setShowModelModal(false) - setEditingModel(null) - }} - language={language} - /> - )} - - {/* Exchange Configuration Modal */} - {showExchangeModal && ( - { - setShowExchangeModal(false) - setEditingExchange(null) - }} - language={language} - /> - )} -
+ ) } @@ -1624,19 +1517,19 @@ function ModelConfigModal({ width: 32, height: 32, }) || ( -
- {selectedModel.name[0]} -
- )} +
+ {selectedModel.name[0]} +
+ )}
diff --git a/web/src/components/BacktestPage.tsx b/web/src/components/BacktestPage.tsx index 041ae200..ea6ac5a0 100644 --- a/web/src/components/BacktestPage.tsx +++ b/web/src/components/BacktestPage.tsx @@ -28,6 +28,7 @@ import { ArrowDownRight, CandlestickChart as CandlestickIcon, } from 'lucide-react' +import { DeepVoidBackground } from './DeepVoidBackground' import { ResponsiveContainer, AreaChart, @@ -785,7 +786,7 @@ export function BacktestPage() { // Data fetching const { data: runsResp, mutate: refreshRuns } = useSWR(['backtest-runs'], () => api.getBacktestRuns({ limit: 100, offset: 0 }) - , { refreshInterval: 5000 }) + , { refreshInterval: 5000 }) const runs = runsResp?.items ?? [] const { data: aiModels } = useSWR('ai-models', api.getModelConfigs, { refreshInterval: 30000 }) @@ -1068,200 +1069,317 @@ export function BacktestPage() { // Render return ( -
- {/* Toast */} - - {toast && ( - +
+ {/* Toast */} + + {toast && ( + + {toast.text} + + )} + + + {/* Header */} +
+
+

+ + {tr('title')} +

+

+ {tr('subtitle')} +

+
+
- -
-
- {/* Left Panel - Config / History */} -
- {/* Wizard */} -
-
- {[1, 2, 3].map((step) => ( -
- - {step < 3 && ( -
step ? '#F0B90B' : '#2B3139' }} - /> - )} -
- ))} - - {wizardStep === 1 - ? language === 'zh' - ? '选择模型' - : 'Select Model' - : wizardStep === 2 +
+ {/* Left Panel - Config / History */} +
+ {/* Wizard */} +
+
+ {[1, 2, 3].map((step) => ( +
+ + {step < 3 && ( +
step ? '#F0B90B' : '#2B3139' }} + /> + )} +
+ ))} + + {wizardStep === 1 ? language === 'zh' - ? '配置参数' - : 'Configure' - : language === 'zh' - ? '确认启动' - : 'Confirm'} - -
+ ? '选择模型' + : 'Select Model' + : wizardStep === 2 + ? language === 'zh' + ? '配置参数' + : 'Configure' + : language === 'zh' + ? '确认启动' + : 'Confirm'} + +
-
- - {/* Step 1: Model & Symbols */} - {wizardStep === 1 && ( - -
- - - {selectedModel && ( -
- - {selectedModel.enabled ? tr('form.enabled') : tr('form.disabled')} - -
- )} -
- - {/* Strategy Selection (Optional) */} -
- - - {formState.strategyId && coinSourceDescription && ( -
-
- - {language === 'zh' ? '币种来源:' : 'Coin Source:'} - - - {coinSourceDescription.type} - {coinSourceDescription.limit && ` (${coinSourceDescription.limit})`} - {coinSourceDescription.desc && ` - ${coinSourceDescription.desc}`} + + + {/* Step 1: Model & Symbols */} + {wizardStep === 1 && ( + +
+ + + {selectedModel && ( +
+ + {selectedModel.enabled ? tr('form.enabled') : tr('form.disabled')}
- {strategyHasDynamicCoins && ( -
- {language === 'zh' - ? '⚡ 清空下方币种输入框即可使用策略的动态币种' - : '⚡ Clear the symbols field below to use strategy\'s dynamic coins'} + )} +
+ + {/* Strategy Selection (Optional) */} +
+ + + {formState.strategyId && coinSourceDescription && ( +
+
+ + {language === 'zh' ? '币种来源:' : 'Coin Source:'} + + + {coinSourceDescription.type} + {coinSourceDescription.limit && ` (${coinSourceDescription.limit})`} + {coinSourceDescription.desc && ` - ${coinSourceDescription.desc}`} +
+ {strategyHasDynamicCoins && ( +
+ {language === 'zh' + ? '⚡ 清空下方币种输入框即可使用策略的动态币种' + : '⚡ Clear the symbols field below to use strategy\'s dynamic coins'} +
+ )} +
+ )} +
+ +
+ + {!strategyHasDynamicCoins && ( +
+ {POPULAR_SYMBOLS.map((sym) => { + const isSelected = formState.symbols.includes(sym) + return ( + + ) + })} +
+ )} +
+