diff --git a/web/src/components/AILearning.tsx b/web/src/components/AILearning.tsx index a10f8f1417..9049bb40ff 100644 --- a/web/src/components/AILearning.tsx +++ b/web/src/components/AILearning.tsx @@ -75,11 +75,8 @@ export default function AILearning({ traderId }: AILearningProps) { if (error) { return ( -
-
+
+
{stripLeadingIcons(t('loadingError', language))}
@@ -88,11 +85,8 @@ export default function AILearning({ traderId }: AILearningProps) { if (!performance) { return ( -
-
+
+
{t('loading', language)}
@@ -101,17 +95,16 @@ export default function AILearning({ traderId }: AILearningProps) { if (!performance || performance.total_trades === 0) { return ( -
+
- -

+ +

{t('aiLearning', language)}

-
{t('noCompleteData', language)}
+
+ {t('noCompleteData', language)} +
) } @@ -124,44 +117,17 @@ export default function AILearning({ traderId }: AILearningProps) { return (
{/* 标题区 - 优化设计 */} -
-
+
+
-
- +
+
-

+

{t('aiLearning', language)}

-

+

{t('tradesAnalyzed', language, { count: performance.total_trades, })} @@ -173,40 +139,16 @@ export default function AILearning({ traderId }: AILearningProps) { {/* 核心指标卡片 - 4列网格 */}

{/* 总交易数 */} -
-
+
+
-
+
{t('totalTrades', language)}
-
+
{performance.total_trades}
-
+
Trades
@@ -214,43 +156,39 @@ export default function AILearning({ traderId }: AILearningProps) { {/* 胜率 */}
= 50 - ? 'linear-gradient(135deg, rgba(16, 185, 129, 0.2) 0%, rgba(30, 35, 41, 0.8) 100%)' - : 'linear-gradient(135deg, rgba(248, 113, 113, 0.2) 0%, rgba(30, 35, 41, 0.8) 100%)', - border: `1px solid ${(performance.win_rate || 0) >= 50 ? 'rgba(16, 185, 129, 0.4)' : 'rgba(248, 113, 113, 0.4)'}`, - boxShadow: `0 4px 16px ${(performance.win_rate || 0) >= 50 ? 'rgba(16, 185, 129, 0.2)' : 'rgba(248, 113, 113, 0.2)'}`, - }} + className={`rounded-2xl p-5 relative overflow-hidden group hover:scale-105 transition-transform ${ + (performance.win_rate || 0) >= 50 + ? 'bg-gradient-to-br from-emerald-500/20 to-darkmoon-surface/80 border-emerald-500/40 shadow-[0_4px_16px_rgba(16,185,129,0.2)]' + : 'bg-gradient-to-br from-red-500/20 to-darkmoon-surface/80 border-red-500/40 shadow-[0_4px_16px_rgba(248,113,113,0.2)]' + } border`} >
= 50 ? '#10B981' : '#F87171'} 0%, transparent 70%)`, - filter: 'blur(20px)', - }} + className={`absolute top-0 right-0 w-24 h-24 rounded-full opacity-20 blur-[20px] ${ + (performance.win_rate || 0) >= 50 + ? 'bg-[radial-gradient(circle,_#10B981_0%,_transparent_70%)]' + : 'bg-[radial-gradient(circle,_#F87171_0%,_transparent_70%)]' + }`} />
= 50 ? '#6EE7B7' : '#FCA5A5', - }} + className={`text-xs font-semibold mb-3 uppercase tracking-wider ${ + (performance.win_rate || 0) >= 50 + ? 'text-emerald-300' + : 'text-red-300' + }`} > {t('winRate', language)}
= 50 ? '#10B981' : '#F87171', - }} + className={`text-4xl font-bold mono mb-1 ${ + (performance.win_rate || 0) >= 50 + ? 'text-emerald-500' + : 'text-red-500' + }`} > {(performance.win_rate || 0).toFixed(1)}%
-
+
{performance.winning_trades || 0}W /{' '} {performance.losing_trades || 0}L
@@ -258,80 +196,32 @@ export default function AILearning({ traderId }: AILearningProps) {
{/* 平均盈利 */} -
-
+
+
-
+
{t('avgWin', language)}
-
+
+{(performance.avg_win || 0).toFixed(2)}
-
+
USDT Average
{/* 平均亏损 */} -
-
+
+
-
+
{t('avgLoss', language)}
-
+
{(performance.avg_loss || 0).toFixed(2)}
-
+
USDT Average
@@ -341,39 +231,18 @@ export default function AILearning({ traderId }: AILearningProps) { {/* 关键指标:夏普比率 & 盈亏比 - 2列网格 */}
{/* 夏普比率 */} -
-
+
+
-
- +
+
-
+
夏普比率
-
+
风险调整后收益 · AI自我进化指标
@@ -381,18 +250,15 @@ export default function AILearning({ traderId }: AILearningProps) {
= 2 - ? '#10B981' - : (performance.sharpe_ratio || 0) >= 1 - ? '#22D3EE' - : (performance.sharpe_ratio || 0) >= 0 - ? '#F0B90B' - : '#F87171', - textShadow: '0 4px 12px rgba(0, 0, 0, 0.3)', - }} + className={`text-6xl font-bold mono drop-shadow-[0_4px_12px_rgba(0,0,0,0.3)] ${ + (performance.sharpe_ratio || 0) >= 2 + ? 'text-emerald-500' + : (performance.sharpe_ratio || 0) >= 1 + ? 'text-cyan-400' + : (performance.sharpe_ratio || 0) >= 0 + ? 'text-darkmoon-gold' + : 'text-red-400' + }`} > {performance.sharpe_ratio ? performance.sharpe_ratio.toFixed(2) @@ -402,25 +268,15 @@ export default function AILearning({ traderId }: AILearningProps) { {performance.sharpe_ratio !== undefined && (
= 2 - ? '#10B981' - : (performance.sharpe_ratio || 0) >= 1 - ? '#22D3EE' - : (performance.sharpe_ratio || 0) >= 0 - ? '#F0B90B' - : '#F87171', - background: - (performance.sharpe_ratio || 0) >= 2 - ? 'rgba(16, 185, 129, 0.2)' - : (performance.sharpe_ratio || 0) >= 1 - ? 'rgba(34, 211, 238, 0.2)' - : (performance.sharpe_ratio || 0) >= 0 - ? 'rgba(240, 185, 11, 0.2)' - : 'rgba(248, 113, 113, 0.2)', - }} + className={`text-sm font-bold px-3 py-1 rounded-lg ${ + (performance.sharpe_ratio || 0) >= 2 + ? 'text-emerald-500 bg-emerald-500/20' + : (performance.sharpe_ratio || 0) >= 1 + ? 'text-cyan-400 bg-cyan-400/20' + : (performance.sharpe_ratio || 0) >= 0 + ? 'text-darkmoon-gold bg-darkmoon-gold/20' + : 'text-red-400 bg-red-400/20' + }`} > {performance.sharpe_ratio >= 2 ? '🟢 卓越表现' @@ -435,17 +291,8 @@ export default function AILearning({ traderId }: AILearningProps) {
{performance.sharpe_ratio !== undefined && ( -
-
+
+
{performance.sharpe_ratio >= 2 && '✨ AI策略非常有效!风险调整后收益优异,可适度扩大仓位但保持纪律。'} {performance.sharpe_ratio >= 1 && @@ -463,39 +310,18 @@ export default function AILearning({ traderId }: AILearningProps) {
{/* 盈亏比 */} -
-
+
+
-
- +
+
-
+
{t('profitFactor', language)}
-
+
{t('avgWinDivLoss', language)}
@@ -503,18 +329,15 @@ export default function AILearning({ traderId }: AILearningProps) {
= 2.0 - ? '#10B981' - : (performance.profit_factor || 0) >= 1.5 - ? '#F0B90B' - : (performance.profit_factor || 0) >= 1.0 - ? '#FB923C' - : '#F87171', - textShadow: '0 4px 12px rgba(0, 0, 0, 0.3)', - }} + className={`text-6xl font-bold mono drop-shadow-[0_4px_12px_rgba(0,0,0,0.3)] ${ + (performance.profit_factor || 0) >= 2.0 + ? 'text-emerald-500' + : (performance.profit_factor || 0) >= 1.5 + ? 'text-darkmoon-gold' + : (performance.profit_factor || 0) >= 1.0 + ? 'text-orange-400' + : 'text-red-400' + }`} > {(performance.profit_factor || 0) > 0 ? (performance.profit_factor || 0).toFixed(2) @@ -523,21 +346,13 @@ export default function AILearning({ traderId }: AILearningProps) {
= 2.0 - ? '#10B981' - : (performance.profit_factor || 0) >= 1.5 - ? '#F0B90B' - : '#94A3B8', - background: - (performance.profit_factor || 0) >= 2.0 - ? 'rgba(16, 185, 129, 0.2)' - : (performance.profit_factor || 0) >= 1.5 - ? 'rgba(240, 185, 11, 0.2)' - : 'rgba(148, 163, 184, 0.2)', - }} + className={`text-sm font-bold px-3 py-1 rounded-lg ${ + (performance.profit_factor || 0) >= 2.0 + ? 'text-emerald-500 bg-emerald-500/20' + : (performance.profit_factor || 0) >= 1.5 + ? 'text-darkmoon-gold bg-darkmoon-gold/20' + : 'text-slate-400 bg-slate-400/20' + }`} > {(performance.profit_factor || 0) >= 2.0 && t('excellent', language)} @@ -554,17 +369,8 @@ export default function AILearning({ traderId }: AILearningProps) {
-
-
+
+
{(performance.profit_factor || 0) >= 2.0 && '🔥 盈利能力出色!每亏1元能赚' + (performance.profit_factor || 0).toFixed(1) + @@ -588,35 +394,18 @@ export default function AILearning({ traderId }: AILearningProps) { {(performance.best_symbol || performance.worst_symbol) && (
{performance.best_symbol && ( -
+
- - + + {t('bestPerformer', language)}
-
+
{performance.best_symbol}
{symbolStats[performance.best_symbol] && ( -
+
{symbolStats[performance.best_symbol].total_pn_l > 0 ? '+' : ''} @@ -628,38 +417,18 @@ export default function AILearning({ traderId }: AILearningProps) { )} {performance.worst_symbol && ( -
+
- - + + {t('worstPerformer', language)}
-
+
{performance.worst_symbol}
{symbolStats[performance.worst_symbol] && ( -
+
{symbolStats[performance.worst_symbol].total_pn_l > 0 ? '+' : ''} @@ -677,70 +446,35 @@ export default function AILearning({ traderId }: AILearningProps) { {/* 左侧:币种表现统计表格 */} {symbolStatsList.length > 0 && (
-
-

+
+

{' '} {stripLeadingIcons(t('symbolPerformance', language))}

- - + - - - - @@ -749,53 +483,43 @@ export default function AILearning({ traderId }: AILearningProps) { {symbolStatsList.map((stat, idx) => ( 0 - ? '1px solid rgba(99, 102, 241, 0.1)' - : 'none', - }} + className={`transition-colors hover:bg-white/5 ${ + idx > 0 ? 'border-t border-indigo-500/10' : '' + }`} > -
+
Symbol + Trades + Win Rate + Total P&L (USDT) + Avg P&L (USDT)
- + {stat.symbol} + {stat.total_trades} = 50 ? '#10B981' : '#F87171', - }} + className={`px-4 py-3 text-right mono text-sm font-semibold ${ + (stat.win_rate || 0) >= 50 + ? 'text-emerald-500' + : 'text-red-400' + }`} > {(stat.win_rate || 0).toFixed(1)}% 0 ? '#10B981' : '#F87171', - }} + className={`px-4 py-3 text-right mono text-sm font-bold ${ + (stat.total_pn_l || 0) > 0 + ? 'text-emerald-500' + : 'text-red-400' + }`} > {(stat.total_pn_l || 0) > 0 ? '+' : ''} {(stat.total_pn_l || 0).toFixed(2)} 0 ? '#10B981' : '#F87171', - }} + className={`px-4 py-3 text-right mono text-sm ${ + (stat.avg_pn_l || 0) > 0 + ? 'text-emerald-500' + : 'text-red-400' + }`} > {(stat.avg_pn_l || 0) > 0 ? '+' : ''} {(stat.avg_pn_l || 0).toFixed(2)} @@ -810,28 +534,17 @@ export default function AILearning({ traderId }: AILearningProps) { {/* 右侧:历史成交记录 */}
-
+
- +
-

+

{t('tradeHistory', language)}

-

+

{performance?.recent_trades && performance.recent_trades.length > 0 ? t('completedTrades', language, { @@ -844,7 +557,7 @@ export default function AILearning({ traderId }: AILearningProps) {

{performance?.recent_trades && @@ -857,61 +570,38 @@ export default function AILearning({ traderId }: AILearningProps) { return (
- + {trade.symbol} {trade.side.toUpperCase()} {isRecent && ( - + {t('latest', language)} )}
{isProfitable ? '+' : ''} {trade.pn_l_pct.toFixed(2)}% @@ -920,24 +610,18 @@ export default function AILearning({ traderId }: AILearningProps) {
-
+
{t('entry', language)}
-
+
{trade.open_price.toFixed(4)}
-
+
{t('exit', language)}
-
+
{trade.close_price.toFixed(4)}
@@ -946,40 +630,36 @@ export default function AILearning({ traderId }: AILearningProps) { {/* Position Details */}
-
Quantity
-
+
+ Quantity +
+
{trade.quantity ? trade.quantity.toFixed(4) : '-'}
-
Leverage
-
+
+ Leverage +
+
{trade.leverage ? `${trade.leverage}x` : '-'}
-
Position Value
-
+
+ Position Value +
+
{trade.position_value ? `$${trade.position_value.toFixed(2)}` : '-'}
-
Margin Used
-
+
+ Margin Used +
+
{trade.margin_used ? `$${trade.margin_used.toFixed(2)}` : '-'} @@ -988,20 +668,22 @@ export default function AILearning({ traderId }: AILearningProps) {
- P&L + + P&L + {isProfitable ? '+' : ''} {trade.pn_l.toFixed(2)} USDT @@ -1009,31 +691,16 @@ export default function AILearning({ traderId }: AILearningProps) {
-
+
⏱️ {formatDuration(trade.duration)} {trade.was_stop_loss && ( - + {t('stopLoss', language)} )}
-
+
{new Date(trade.close_time).toLocaleString('en-US', { month: 'short', day: '2-digit', @@ -1048,12 +715,9 @@ export default function AILearning({ traderId }: AILearningProps) { ) : (
- +
-
+
{t('noCompletedTrades', language)}
@@ -1063,54 +727,37 @@ export default function AILearning({ traderId }: AILearningProps) {
{/* AI学习说明 - 现代化设计 */} -
+
-
- +
+
-

+

{stripLeadingIcons(t('howAILearns', language))}

- - + + {t('aiLearningPoint1', language)}
- - + + {t('aiLearningPoint2', language)}
- - + + {t('aiLearningPoint3', language)}
- - + + {t('aiLearningPoint4', language)}
diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx deleted file mode 100644 index 3f05c5e18c..0000000000 --- a/web/src/components/AITradersPage.tsx +++ /dev/null @@ -1,2652 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import useSWR from 'swr' -import { api } from '../lib/api' -import type { - TraderInfo, - CreateTraderRequest, - AIModel, - Exchange, -} from '../types' -import { useLanguage } from '../contexts/LanguageContext' -import { t, type Language } from '../i18n/translations' -import { useAuth } from '../contexts/AuthContext' -import { getExchangeIcon } from './ExchangeIcons' -import { getModelIcon } from './ModelIcons' -import { TraderConfigModal } from './TraderConfigModal' -import { - TwoStageKeyModal, - type TwoStageKeyModalResult, -} from './TwoStageKeyModal' -import { - WebCryptoEnvironmentCheck, - type WebCryptoCheckStatus, -} from './WebCryptoEnvironmentCheck' -import { - Bot, - Brain, - Landmark, - BarChart3, - Trash2, - Plus, - Users, - AlertTriangle, - BookOpen, - HelpCircle, - Radio, - Pencil, -} from 'lucide-react' -import { confirmToast } from '../lib/notify' -import { toast } from 'sonner' - -// 获取友好的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() - } -} - -// 提取下划线后面的名称部分 -function getShortName(fullName: string): string { - const parts = fullName.split('_') - return parts.length > 1 ? parts[parts.length - 1] : fullName -} - -interface AITradersPageProps { - onTraderSelect?: (traderId: string) => void -} - -export function AITradersPage({ onTraderSelect }: AITradersPageProps) { - const { language } = useLanguage() - const { user, token } = useAuth() - const navigate = useNavigate() - const [showCreateModal, setShowCreateModal] = useState(false) - const [showEditModal, setShowEditModal] = useState(false) - const [showModelModal, setShowModelModal] = useState(false) - const [showExchangeModal, setShowExchangeModal] = useState(false) - const [showSignalSourceModal, setShowSignalSourceModal] = useState(false) - const [editingModel, setEditingModel] = useState(null) - const [editingExchange, setEditingExchange] = useState(null) - const [editingTrader, setEditingTrader] = useState(null) - const [allModels, setAllModels] = useState([]) - const [allExchanges, setAllExchanges] = useState([]) - const [supportedModels, setSupportedModels] = useState([]) - const [supportedExchanges, setSupportedExchanges] = useState([]) - const [userSignalSource, setUserSignalSource] = useState<{ - coinPoolUrl: string - oiTopUrl: string - }>({ - coinPoolUrl: '', - oiTopUrl: '', - }) - - const { data: traders, mutate: mutateTraders } = useSWR( - user && token ? 'traders' : null, - api.getTraders, - { refreshInterval: 5000 } - ) - - // 加载AI模型和交易所配置 - useEffect(() => { - const loadConfigs = async () => { - if (!user || !token) { - // 未登录时只加载公开的支持模型和交易所 - try { - const [supportedModels, supportedExchanges] = await Promise.all([ - api.getSupportedModels(), - api.getSupportedExchanges(), - ]) - setSupportedModels(supportedModels) - setSupportedExchanges(supportedExchanges) - } catch (err) { - console.error('Failed to load supported configs:', err) - } - return - } - - try { - const [ - modelConfigs, - exchangeConfigs, - supportedModels, - supportedExchanges, - ] = await Promise.all([ - api.getModelConfigs(), - api.getExchangeConfigs(), - api.getSupportedModels(), - api.getSupportedExchanges(), - ]) - setAllModels(modelConfigs) - setAllExchanges(exchangeConfigs) - setSupportedModels(supportedModels) - setSupportedExchanges(supportedExchanges) - - // 加载用户信号源配置 - try { - const signalSource = await api.getUserSignalSource() - setUserSignalSource({ - coinPoolUrl: signalSource.coin_pool_url || '', - oiTopUrl: signalSource.oi_top_url || '', - }) - } catch (error) { - console.log('📡 用户信号源配置暂未设置') - } - } catch (error) { - console.error('Failed to load configs:', error) - } - } - loadConfigs() - }, [user, token]) - - // 只显示已配置的模型和交易所 - // 注意:后端返回的数据不包含敏感信息(apiKey等),所以通过其他字段判断是否已配置 - const configuredModels = - allModels?.filter((m) => { - // 如果模型已启用,说明已配置 - // 或者有自定义API URL,也说明已配置 - return m.enabled || (m.customApiUrl && m.customApiUrl.trim() !== '') - }) || [] - const configuredExchanges = - allExchanges?.filter((e) => { - // Aster 交易所检查特殊字段 - if (e.id === 'aster') { - return e.asterUser && e.asterUser.trim() !== '' - } - // Hyperliquid 需要检查钱包地址(后端会返回这个字段) - if (e.id === 'hyperliquid') { - return e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== '' - } - // 其他交易所:如果已启用,说明已配置(后端返回的已配置交易所会有 enabled: true) - return e.enabled - }) || [] - - // 只在创建交易员时使用已启用且配置完整的 - // 注意:后端返回的数据不包含敏感信息,所以只检查 enabled 状态和必要的非敏感字段 - const enabledModels = allModels?.filter((m) => m.enabled) || [] - const enabledExchanges = - allExchanges?.filter((e) => { - if (!e.enabled) return false - - // Aster 交易所需要特殊字段(后端会返回这些非敏感字段) - if (e.id === 'aster') { - return ( - e.asterUser && - e.asterUser.trim() !== '' && - e.asterSigner && - e.asterSigner.trim() !== '' - ) - } - - // Hyperliquid 需要钱包地址(后端会返回这个字段) - if (e.id === 'hyperliquid') { - return e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== '' - } - - // 其他交易所:如果已启用,说明已配置完整(后端只返回已配置的交易所) - return true - }) || [] - - // 检查模型是否正在被运行中的交易员使用(用于UI禁用) - const isModelInUse = (modelId: string) => { - return traders?.some((t) => t.ai_model === modelId && t.is_running) - } - - // 检查交易所是否正在被运行中的交易员使用(用于UI禁用) - const isExchangeInUse = (exchangeId: string) => { - return traders?.some((t) => t.exchange_id === exchangeId && t.is_running) - } - - // 检查模型是否被任何交易员使用(包括停止状态的) - const isModelUsedByAnyTrader = (modelId: string) => { - return traders?.some((t) => t.ai_model === modelId) || false - } - - // 检查交易所是否被任何交易员使用(包括停止状态的) - const isExchangeUsedByAnyTrader = (exchangeId: string) => { - return traders?.some((t) => t.exchange_id === exchangeId) || false - } - - // 获取使用特定模型的交易员列表 - const getTradersUsingModel = (modelId: string) => { - return traders?.filter((t) => t.ai_model === modelId) || [] - } - - // 获取使用特定交易所的交易员列表 - const getTradersUsingExchange = (exchangeId: string) => { - return traders?.filter((t) => t.exchange_id === exchangeId) || [] - } - - const handleCreateTrader = async (data: CreateTraderRequest) => { - try { - const model = allModels?.find((m) => m.id === data.ai_model_id) - const exchange = allExchanges?.find((e) => e.id === data.exchange_id) - - if (!model?.enabled) { - toast.error(t('modelNotConfigured', language)) - return - } - - if (!exchange?.enabled) { - toast.error(t('exchangeNotConfigured', language)) - return - } - - await toast.promise(api.createTrader(data), { - loading: '正在创建…', - success: '创建成功', - error: '创建失败', - }) - setShowCreateModal(false) - // Immediately refresh traders list for better UX - await mutateTraders() - } catch (error) { - console.error('Failed to create trader:', error) - toast.error(t('createTraderFailed', language)) - } - } - - const handleEditTrader = async (traderId: string) => { - try { - const traderConfig = await api.getTraderConfig(traderId) - setEditingTrader(traderConfig) - setShowEditModal(true) - } catch (error) { - console.error('Failed to fetch trader config:', error) - toast.error(t('getTraderConfigFailed', language)) - } - } - - const handleSaveEditTrader = async (data: CreateTraderRequest) => { - if (!editingTrader) return - - try { - const model = enabledModels?.find((m) => m.id === data.ai_model_id) - const exchange = enabledExchanges?.find((e) => e.id === data.exchange_id) - - if (!model) { - toast.error(t('modelConfigNotExist', language)) - return - } - - if (!exchange) { - toast.error(t('exchangeConfigNotExist', language)) - return - } - - const request = { - name: data.name, - ai_model_id: data.ai_model_id, - exchange_id: data.exchange_id, - initial_balance: data.initial_balance, - scan_interval_minutes: data.scan_interval_minutes, - btc_eth_leverage: data.btc_eth_leverage, - altcoin_leverage: data.altcoin_leverage, - trading_symbols: data.trading_symbols, - custom_prompt: data.custom_prompt, - override_base_prompt: data.override_base_prompt, - system_prompt_template: data.system_prompt_template, - is_cross_margin: data.is_cross_margin, - use_coin_pool: data.use_coin_pool, - use_oi_top: data.use_oi_top, - } - - await toast.promise(api.updateTrader(editingTrader.trader_id, request), { - loading: '正在保存…', - success: '保存成功', - error: '保存失败', - }) - setShowEditModal(false) - setEditingTrader(null) - // Immediately refresh traders list for better UX - await mutateTraders() - } catch (error) { - console.error('Failed to update trader:', error) - toast.error(t('updateTraderFailed', language)) - } - } - - const handleDeleteTrader = async (traderId: string) => { - { - const ok = await confirmToast(t('confirmDeleteTrader', language)) - if (!ok) return - } - - try { - await toast.promise(api.deleteTrader(traderId), { - loading: '正在删除…', - success: '删除成功', - error: '删除失败', - }) - - // Immediately refresh traders list for better UX - await mutateTraders() - } catch (error) { - console.error('Failed to delete trader:', error) - toast.error(t('deleteTraderFailed', language)) - } - } - - const handleToggleTrader = async (traderId: string, running: boolean) => { - try { - if (running) { - await toast.promise(api.stopTrader(traderId), { - loading: '正在停止…', - success: '已停止', - error: '停止失败', - }) - } else { - await toast.promise(api.startTrader(traderId), { - loading: '正在启动…', - success: '已启动', - error: '启动失败', - }) - } - - // Immediately refresh traders list to update running status - await mutateTraders() - } catch (error) { - console.error('Failed to toggle trader:', error) - toast.error(t('operationFailed', language)) - } - } - - const handleModelClick = (modelId: string) => { - if (!isModelInUse(modelId)) { - setEditingModel(modelId) - setShowModelModal(true) - } - } - - const handleExchangeClick = (exchangeId: string) => { - if (!isExchangeInUse(exchangeId)) { - setEditingExchange(exchangeId) - setShowExchangeModal(true) - } - } - - // 通用删除配置处理函数 - const handleDeleteConfig = async (config: { - id: string - type: 'model' | 'exchange' - checkInUse: (id: string) => boolean - getUsingTraders: (id: string) => any[] - cannotDeleteKey: string - confirmDeleteKey: string - allItems: T[] | undefined - clearFields: (item: T) => T - buildRequest: (items: T[]) => any - updateApi: (request: any) => Promise - refreshApi: () => Promise - setItems: (items: T[]) => void - closeModal: () => void - errorKey: string - }) => { - // 检查是否有交易员正在使用 - if (config.checkInUse(config.id)) { - const usingTraders = config.getUsingTraders(config.id) - const traderNames = usingTraders.map((t) => t.trader_name).join(', ') - toast.error( - `${t(config.cannotDeleteKey, language)} · ${t('tradersUsing', language)}: ${traderNames} · ${t('pleaseDeleteTradersFirst', language)}` - ) - return - } - - { - const ok = await confirmToast(t(config.confirmDeleteKey, language)) - if (!ok) return - } - - try { - const updatedItems = - config.allItems?.map((item) => - item.id === config.id ? config.clearFields(item) : item - ) || [] - - const request = config.buildRequest(updatedItems) - await toast.promise(config.updateApi(request), { - loading: '正在更新配置…', - success: '配置已更新', - error: '更新配置失败', - }) - - // 重新获取用户配置以确保数据同步 - const refreshedItems = await config.refreshApi() - config.setItems(refreshedItems) - - config.closeModal() - } catch (error) { - console.error(`Failed to delete ${config.type} config:`, error) - toast.error(t(config.errorKey, language)) - } - } - - const handleDeleteModelConfig = async (modelId: string) => { - await handleDeleteConfig({ - id: modelId, - type: 'model', - checkInUse: isModelUsedByAnyTrader, - getUsingTraders: getTradersUsingModel, - cannotDeleteKey: 'cannotDeleteModelInUse', - confirmDeleteKey: 'confirmDeleteModel', - allItems: allModels, - clearFields: (m) => ({ - ...m, - apiKey: '', - customApiUrl: '', - customModelName: '', - enabled: false, - }), - buildRequest: (models) => ({ - models: Object.fromEntries( - models.map((model) => [ - model.provider, - { - enabled: model.enabled, - api_key: model.apiKey || '', - custom_api_url: model.customApiUrl || '', - custom_model_name: model.customModelName || '', - }, - ]) - ), - }), - updateApi: api.updateModelConfigs, - refreshApi: api.getModelConfigs, - setItems: (items) => { - // 使用函数式更新确保状态正确更新 - setAllModels([...items]) - }, - closeModal: () => { - setShowModelModal(false) - setEditingModel(null) - }, - errorKey: 'deleteConfigFailed', - }) - } - - const handleSaveModelConfig = async ( - modelId: string, - apiKey: string, - customApiUrl?: string, - customModelName?: string - ) => { - try { - // 创建或更新用户的模型配置 - const existingModel = allModels?.find((m) => m.id === modelId) - let updatedModels - - // 找到要配置的模型(优先从已配置列表,其次从支持列表) - const modelToUpdate = - existingModel || supportedModels?.find((m) => m.id === modelId) - if (!modelToUpdate) { - toast.error(t('modelNotExist', language)) - return - } - - if (existingModel) { - // 更新现有配置 - updatedModels = - allModels?.map((m) => - m.id === modelId - ? { - ...m, - apiKey, - customApiUrl: customApiUrl || '', - customModelName: customModelName || '', - enabled: true, - } - : m - ) || [] - } else { - // 添加新配置 - const newModel = { - ...modelToUpdate, - apiKey, - customApiUrl: customApiUrl || '', - customModelName: customModelName || '', - enabled: true, - } - updatedModels = [...(allModels || []), newModel] - } - - const request = { - models: Object.fromEntries( - updatedModels.map((model) => [ - model.provider, // 使用 provider 而不是 id - { - enabled: model.enabled, - api_key: model.apiKey || '', - custom_api_url: model.customApiUrl || '', - custom_model_name: model.customModelName || '', - }, - ]) - ), - } - - await toast.promise(api.updateModelConfigs(request), { - loading: '正在更新模型配置…', - success: '模型配置已更新', - error: '更新模型配置失败', - }) - - // 重新获取用户配置以确保数据同步 - const refreshedModels = await api.getModelConfigs() - setAllModels(refreshedModels) - - setShowModelModal(false) - setEditingModel(null) - } catch (error) { - console.error('Failed to save model config:', error) - toast.error(t('saveConfigFailed', language)) - } - } - - const handleDeleteExchangeConfig = async (exchangeId: string) => { - await handleDeleteConfig({ - id: exchangeId, - type: 'exchange', - checkInUse: isExchangeUsedByAnyTrader, - getUsingTraders: getTradersUsingExchange, - cannotDeleteKey: 'cannotDeleteExchangeInUse', - confirmDeleteKey: 'confirmDeleteExchange', - allItems: allExchanges, - clearFields: (e) => ({ - ...e, - apiKey: '', - secretKey: '', - hyperliquidWalletAddr: '', - asterUser: '', - asterSigner: '', - asterPrivateKey: '', - enabled: false, - }), - buildRequest: (exchanges) => ({ - exchanges: Object.fromEntries( - exchanges.map((exchange) => [ - exchange.id, - { - enabled: exchange.enabled, - api_key: exchange.apiKey || '', - secret_key: exchange.secretKey || '', - testnet: exchange.testnet || false, - hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '', - aster_user: exchange.asterUser || '', - aster_signer: exchange.asterSigner || '', - aster_private_key: exchange.asterPrivateKey || '', - }, - ]) - ), - }), - updateApi: api.updateExchangeConfigsEncrypted, - refreshApi: api.getExchangeConfigs, - setItems: (items) => { - // 使用函数式更新确保状态正确更新 - setAllExchanges([...items]) - }, - closeModal: () => { - setShowExchangeModal(false) - setEditingExchange(null) - }, - errorKey: 'deleteExchangeConfigFailed', - }) - } - - const handleSaveExchangeConfig = async ( - exchangeId: string, - apiKey: string, - secretKey?: string, - testnet?: boolean, - hyperliquidWalletAddr?: string, - asterUser?: string, - asterSigner?: string, - asterPrivateKey?: string - ) => { - try { - // 找到要配置的交易所(从supportedExchanges中) - const exchangeToUpdate = supportedExchanges?.find( - (e) => e.id === exchangeId - ) - if (!exchangeToUpdate) { - toast.error(t('exchangeNotExist', language)) - return - } - - // 创建或更新用户的交易所配置 - const existingExchange = allExchanges?.find((e) => e.id === exchangeId) - let updatedExchanges - - if (existingExchange) { - // 更新现有配置 - updatedExchanges = - allExchanges?.map((e) => - e.id === exchangeId - ? { - ...e, - apiKey, - secretKey, - testnet, - hyperliquidWalletAddr, - asterUser, - asterSigner, - asterPrivateKey, - enabled: true, - } - : e - ) || [] - } else { - // 添加新配置 - const newExchange = { - ...exchangeToUpdate, - apiKey, - secretKey, - testnet, - hyperliquidWalletAddr, - asterUser, - asterSigner, - asterPrivateKey, - enabled: true, - } - updatedExchanges = [...(allExchanges || []), newExchange] - } - - const request = { - exchanges: Object.fromEntries( - updatedExchanges.map((exchange) => [ - exchange.id, - { - enabled: exchange.enabled, - api_key: exchange.apiKey || '', - secret_key: exchange.secretKey || '', - testnet: exchange.testnet || false, - hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '', - aster_user: exchange.asterUser || '', - aster_signer: exchange.asterSigner || '', - aster_private_key: exchange.asterPrivateKey || '', - }, - ]) - ), - } - - await toast.promise(api.updateExchangeConfigsEncrypted(request), { - loading: '正在更新交易所配置…', - success: '交易所配置已更新', - error: '更新交易所配置失败', - }) - - // 重新获取用户配置以确保数据同步 - const refreshedExchanges = await api.getExchangeConfigs() - setAllExchanges(refreshedExchanges) - - setShowExchangeModal(false) - setEditingExchange(null) - } catch (error) { - console.error('Failed to save exchange config:', error) - toast.error(t('saveConfigFailed', language)) - } - } - - const handleAddModel = () => { - setEditingModel(null) - setShowModelModal(true) - } - - const handleAddExchange = () => { - setEditingExchange(null) - setShowExchangeModal(true) - } - - const handleSaveSignalSource = async ( - coinPoolUrl: string, - oiTopUrl: string - ) => { - try { - await toast.promise(api.saveUserSignalSource(coinPoolUrl, oiTopUrl), { - loading: '正在保存…', - success: '保存成功', - error: '保存失败', - }) - setUserSignalSource({ coinPoolUrl, oiTopUrl }) - setShowSignalSourceModal(false) - } catch (error) { - console.error('Failed to save signal source:', error) - toast.error(t('saveSignalSourceFailed', language)) - } - } - - return ( -
- {/* Header */} -
-
-
- -
-
-

- {t('aiTraders', language)} - - {traders?.length || 0} {t('active', language)} - -

-

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

-
-
- -
- - - - - - - -
-
- - {/* 信号源配置警告 */} - {traders && - traders.some((t) => t.use_coin_pool || t.use_oi_top) && - !userSignalSource.coinPoolUrl && - !userSignalSource.oiTopUrl && ( -
- -
-
- ⚠️ {t('signalSourceNotConfigured', language)} -
-
-

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

-

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

-
    -
  • 点击"{t('signalSource', language)}"按钮配置API地址
  • -
  • 或在交易员配置中禁用"使用币种池"和"使用OI Top"
  • -
  • 或在交易员配置中设置自定义币种列表
  • -
-
- -
-
- )} - - {/* Configuration Status */} -
- {/* AI Models */} -
-

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

-
- {configuredModels.map((model) => { - const inUse = isModelInUse(model.id) - return ( -
handleModelClick(model.id)} - > -
-
- {getModelIcon(model.provider || model.id, { - width: 28, - height: 28, - }) || ( -
- {getShortName(model.name)[0]} -
- )} -
-
-
- {getShortName(model.name)} -
-
- {inUse - ? t('inUse', language) - : model.enabled - ? t('enabled', language) - : t('configured', language)} -
-
-
-
-
- ) - })} - {configuredModels.length === 0 && ( -
- -
- {t('noModelsConfigured', language)} -
-
- )} -
-
- - {/* Exchanges */} -
-

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

-
- {configuredExchanges.map((exchange) => { - const inUse = isExchangeInUse(exchange.id) - return ( -
handleExchangeClick(exchange.id)} - > -
-
- {getExchangeIcon(exchange.id, { width: 28, height: 28 })} -
-
-
- {getShortName(exchange.name)} -
-
- {exchange.type.toUpperCase()} •{' '} - {inUse - ? t('inUse', language) - : exchange.enabled - ? t('enabled', language) - : t('configured', language)} -
-
-
-
-
- ) - })} - {configuredExchanges.length === 0 && ( -
- -
- {t('noExchangesConfigured', language)} -
-
- )} -
-
-
- - {/* Traders List */} -
-
-

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

-
- - {traders && traders.length > 0 ? ( -
- {traders.map((trader) => ( -
-
-
- -
-
-
- {trader.trader_name} -
-
- {getModelDisplayName( - trader.ai_model.split('_').pop() || trader.ai_model - )}{' '} - Model • {trader.exchange_id?.toUpperCase()} -
-
-
- -
- {/* Status */} -
- {/*
- {t('status', language)} -
*/} -
- {trader.is_running - ? t('running', language) - : t('stopped', language)} -
-
- - {/* Actions: 禁止换行,超出横向滚动 */} -
- - - - - - - -
-
-
- ))} -
- ) : ( -
- -
- {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} - /> - )} - - {/* Signal Source Configuration Modal */} - {showSignalSourceModal && ( - setShowSignalSourceModal(false)} - language={language} - /> - )} -
- ) -} - -// Tooltip Helper Component -function Tooltip({ - content, - children, -}: { - content: string - children: React.ReactNode -}) { - const [show, setShow] = useState(false) - - return ( -
-
setShow(true)} - onMouseLeave={() => setShow(false)} - onClick={() => setShow(!show)} - > - {children} -
- {show && ( -
- {content} -
-
- )} -
- ) -} - -// Signal Source Configuration Modal Component -function SignalSourceModal({ - coinPoolUrl, - oiTopUrl, - onSave, - onClose, - language, -}: { - coinPoolUrl: string - oiTopUrl: string - onSave: (coinPoolUrl: string, oiTopUrl: string) => void - onClose: () => void - language: Language -}) { - const [coinPool, setCoinPool] = useState(coinPoolUrl || '') - const [oiTop, setOiTop] = useState(oiTopUrl || '') - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - onSave(coinPool.trim(), oiTop.trim()) - } - - return ( -
-
-

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

- -
-
-
- - setCoinPool(e.target.value)} - placeholder="https://api.example.com/coinpool" - className="w-full px-3 py-2 rounded" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - /> -
- {t('coinPoolDescription', language)} -
-
- -
- - setOiTop(e.target.value)} - placeholder="https://api.example.com/oitop" - className="w-full px-3 py-2 rounded" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - /> -
- {t('oiTopDescription', language)} -
-
- -
-
- ℹ️ {t('information', language)} -
-
-
{t('signalSourceInfo1', language)}
-
{t('signalSourceInfo2', language)}
-
{t('signalSourceInfo3', language)}
-
-
-
- -
- - -
-
-
-
- ) -} - -// Model Configuration Modal Component -function ModelConfigModal({ - allModels, - configuredModels, - editingModelId, - onSave, - onDelete, - onClose, - language, -}: { - allModels: AIModel[] - configuredModels: AIModel[] - editingModelId: string | null - onSave: ( - modelId: string, - apiKey: string, - baseUrl?: string, - modelName?: string - ) => void - onDelete: (modelId: string) => void - onClose: () => void - language: Language -}) { - const [selectedModelId, setSelectedModelId] = useState(editingModelId || '') - const [apiKey, setApiKey] = useState('') - const [baseUrl, setBaseUrl] = useState('') - const [modelName, setModelName] = useState('') - - // 获取当前编辑的模型信息 - 编辑时从已配置的模型中查找,新建时从所有支持的模型中查找 - const selectedModel = editingModelId - ? configuredModels?.find((m) => m.id === selectedModelId) - : allModels?.find((m) => m.id === selectedModelId) - - // 如果是编辑现有模型,初始化API Key、Base URL和Model Name - useEffect(() => { - if (editingModelId && selectedModel) { - setApiKey(selectedModel.apiKey || '') - setBaseUrl(selectedModel.customApiUrl || '') - setModelName(selectedModel.customModelName || '') - } - }, [editingModelId, selectedModel]) - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - if (!selectedModelId || !apiKey.trim()) return - - onSave( - selectedModelId, - apiKey.trim(), - baseUrl.trim() || undefined, - modelName.trim() || undefined - ) - } - - // 可选择的模型列表(所有支持的模型) - const availableModels = allModels || [] - - return ( -
-
-
-

- {editingModelId - ? t('editAIModel', language) - : t('addAIModel', language)} -

- {editingModelId && ( - - )} -
- -
-
- {!editingModelId && ( -
- - -
- )} - - {selectedModel && ( -
-
-
- {getModelIcon(selectedModel.provider || selectedModel.id, { - width: 32, - height: 32, - }) || ( -
- {selectedModel.name[0]} -
- )} -
-
-
- {getShortName(selectedModel.name)} -
-
- {selectedModel.provider} • {selectedModel.id} -
-
-
-
- )} - - {selectedModel && ( - <> -
- - setApiKey(e.target.value)} - placeholder={t('enterAPIKey', language)} - className="w-full px-3 py-2 rounded" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - required - /> -
- -
- - setBaseUrl(e.target.value)} - placeholder={t('customBaseURLPlaceholder', language)} - className="w-full px-3 py-2 rounded" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - /> -
- {t('leaveBlankForDefault', language)} -
-
- -
- - setModelName(e.target.value)} - placeholder="例如: deepseek-chat, qwen3-max, gpt-5" - className="w-full px-3 py-2 rounded" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - /> -
- 留空使用默认模型名称 -
-
- -
-
- ℹ️ {t('information', language)} -
-
-
{t('modelConfigInfo1', language)}
-
{t('modelConfigInfo2', language)}
-
{t('modelConfigInfo3', language)}
-
-
- - )} -
- -
- - -
-
-
-
- ) -} - -// Exchange Configuration Modal Component -function ExchangeConfigModal({ - allExchanges, - editingExchangeId, - onSave, - onDelete, - onClose, - language, -}: { - allExchanges: Exchange[] - editingExchangeId: string | null - onSave: ( - exchangeId: string, - apiKey: string, - secretKey?: string, - testnet?: boolean, - hyperliquidWalletAddr?: string, - asterUser?: string, - asterSigner?: string, - asterPrivateKey?: string - ) => Promise - onDelete: (exchangeId: string) => void - onClose: () => void - language: Language -}) { - const [selectedExchangeId, setSelectedExchangeId] = useState( - editingExchangeId || '' - ) - const [apiKey, setApiKey] = useState('') - const [secretKey, setSecretKey] = useState('') - const [passphrase, setPassphrase] = useState('') - const [testnet, setTestnet] = useState(false) - const [showGuide, setShowGuide] = useState(false) - const [serverIP, setServerIP] = useState<{ - public_ip: string - message: string - } | null>(null) - const [loadingIP, setLoadingIP] = useState(false) - const [copiedIP, setCopiedIP] = useState(false) - const [webCryptoStatus, setWebCryptoStatus] = - useState('idle') - - // 币安配置指南展开状态 - const [showBinanceGuide, setShowBinanceGuide] = useState(false) - - // Aster 特定字段 - const [asterUser, setAsterUser] = useState('') - const [asterSigner, setAsterSigner] = useState('') - const [asterPrivateKey, setAsterPrivateKey] = useState('') - - // Hyperliquid 特定字段 - const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('') - - // 安全输入状态 - const [secureInputTarget, setSecureInputTarget] = useState< - null | 'hyperliquid' | 'aster' - >(null) - - // 获取当前编辑的交易所信息 - const selectedExchange = allExchanges?.find( - (e) => e.id === selectedExchangeId - ) - - // 如果是编辑现有交易所,初始化表单数据 - useEffect(() => { - if (editingExchangeId && selectedExchange) { - setApiKey(selectedExchange.apiKey || '') - setSecretKey(selectedExchange.secretKey || '') - setPassphrase('') // Don't load existing passphrase for security - setTestnet(selectedExchange.testnet || false) - - // Aster 字段 - setAsterUser(selectedExchange.asterUser || '') - setAsterSigner(selectedExchange.asterSigner || '') - setAsterPrivateKey('') // Don't load existing private key for security - - // Hyperliquid 字段 - setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '') - } - }, [editingExchangeId, selectedExchange]) - - // 加载服务器IP(当选择binance时) - useEffect(() => { - if (selectedExchangeId === 'binance' && !serverIP) { - setLoadingIP(true) - api - .getServerIP() - .then((data) => { - setServerIP(data) - }) - .catch((err) => { - console.error('Failed to load server IP:', err) - }) - .finally(() => { - setLoadingIP(false) - }) - } - }, [selectedExchangeId]) - - const handleCopyIP = async (ip: string) => { - try { - // 优先使用现代 Clipboard API - if (navigator.clipboard && navigator.clipboard.writeText) { - await navigator.clipboard.writeText(ip) - setCopiedIP(true) - setTimeout(() => setCopiedIP(false), 2000) - toast.success(t('ipCopied', language)) - } else { - // 降级方案: 使用传统的 execCommand 方法 - const textArea = document.createElement('textarea') - textArea.value = ip - textArea.style.position = 'fixed' - textArea.style.left = '-999999px' - textArea.style.top = '-999999px' - document.body.appendChild(textArea) - textArea.focus() - textArea.select() - - try { - const successful = document.execCommand('copy') - if (successful) { - setCopiedIP(true) - setTimeout(() => setCopiedIP(false), 2000) - toast.success(t('ipCopied', language)) - } else { - throw new Error('复制命令执行失败') - } - } finally { - document.body.removeChild(textArea) - } - } - } catch (err) { - console.error('复制失败:', err) - // 显示错误提示 - toast.error( - t('copyIPFailed', language) || `复制失败: ${ip}\n请手动复制此IP地址` - ) - } - } - - // 安全输入处理函数 - const secureInputContextLabel = - secureInputTarget === 'aster' - ? t('asterExchangeName', language) - : secureInputTarget === 'hyperliquid' - ? t('hyperliquidExchangeName', language) - : undefined - - const handleSecureInputCancel = () => { - setSecureInputTarget(null) - } - - const handleSecureInputComplete = ({ - value, - obfuscationLog, - }: TwoStageKeyModalResult) => { - const trimmed = value.trim() - if (secureInputTarget === 'hyperliquid') { - setApiKey(trimmed) - } - if (secureInputTarget === 'aster') { - setAsterPrivateKey(trimmed) - } - console.log('Secure input obfuscation log:', obfuscationLog) - setSecureInputTarget(null) - } - - // 掩盖敏感数据显示 - const maskSecret = (secret: string) => { - if (!secret || secret.length === 0) return '' - if (secret.length <= 8) return '*'.repeat(secret.length) - return ( - secret.slice(0, 4) + - '*'.repeat(Math.max(secret.length - 8, 4)) + - secret.slice(-4) - ) - } - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!selectedExchangeId) return - - // 根据交易所类型验证不同字段 - if (selectedExchange?.id === 'binance') { - if (!apiKey.trim() || !secretKey.trim()) return - await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet) - } else if (selectedExchange?.id === 'hyperliquid') { - if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址 - await onSave( - selectedExchangeId, - apiKey.trim(), - '', - testnet, - hyperliquidWalletAddr.trim() - ) - } else if (selectedExchange?.id === 'aster') { - if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim()) - return - await onSave( - selectedExchangeId, - '', - '', - testnet, - undefined, - asterUser.trim(), - asterSigner.trim(), - asterPrivateKey.trim() - ) - } else if (selectedExchange?.id === 'okx') { - if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return - await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet) - } else { - // 默认情况(其他CEX交易所) - if (!apiKey.trim() || !secretKey.trim()) return - await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet) - } - } - - // 可选择的交易所列表(所有支持的交易所) - const availableExchanges = allExchanges || [] - - return ( -
-
-
-

- {editingExchangeId - ? t('editExchange', language) - : t('addExchange', language)} -

-
- {selectedExchange?.id === 'binance' && ( - - )} - {editingExchangeId && ( - - )} -
-
- -
-
- {!editingExchangeId && ( -
-
-
- {t('environmentSteps.checkTitle', language)} -
- -
-
-
- {t('environmentSteps.selectTitle', language)} -
- -
-
- )} - - {selectedExchange && ( -
-
-
- {getExchangeIcon(selectedExchange.id, { - width: 32, - height: 32, - })} -
-
-
- {getShortName(selectedExchange.name)} -
-
- {selectedExchange.type.toUpperCase()} •{' '} - {selectedExchange.id} -
-
-
-
- )} - - {selectedExchange && ( - <> - {/* Binance 和其他 CEX 交易所的字段 */} - {(selectedExchange.id === 'binance' || - selectedExchange.type === 'cex') && - selectedExchange.id !== 'hyperliquid' && - selectedExchange.id !== 'aster' && ( - <> - {/* 币安用户配置提示 (D1 方案) */} - {selectedExchange.id === 'binance' && ( -
setShowBinanceGuide(!showBinanceGuide)} - > -
-
- ℹ️ - - 币安用户必读: - 使用「现货与合约交易」API,不要用「统一账户 - API」 - -
- - {showBinanceGuide ? '▲' : '▼'} - -
- - {/* 展开的详细说明 */} - {showBinanceGuide && ( -
e.stopPropagation()} - > -

- 原因:统一账户 API - 权限结构不同,会导致订单提交失败 -

- -

- 正确配置步骤: -

-
    -
  1. - 登录币安 → 个人中心 →{' '} - API 管理 -
  2. -
  3. - 创建 API → 选择「 - 系统生成的 API 密钥」 -
  4. -
  5. - 勾选「现货与合约交易」( - - 不选统一账户 - - ) -
  6. -
  7. - IP 限制选「无限制 - 」或添加服务器 IP -
  8. -
- -

- 💡 多资产模式用户注意: - 如果您开启了多资产模式,将强制使用全仓模式。建议关闭多资产模式以支持逐仓交易。 -

- - - 📖 查看币安官方教程 ↗ - -
- )} -
- )} - -
- - setApiKey(e.target.value)} - placeholder={t('enterAPIKey', language)} - className="w-full px-3 py-2 rounded" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - required - /> -
- -
- - setSecretKey(e.target.value)} - placeholder={t('enterSecretKey', language)} - className="w-full px-3 py-2 rounded" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - required - /> -
- - {selectedExchange.id === 'okx' && ( -
- - setPassphrase(e.target.value)} - placeholder={t('enterPassphrase', language)} - className="w-full px-3 py-2 rounded" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - required - /> -
- )} - - {/* Binance 白名单IP提示 */} - {selectedExchange.id === 'binance' && ( -
-
- {t('whitelistIP', language)} -
-
- {t('whitelistIPDesc', language)} -
- - {loadingIP ? ( -
- {t('loadingServerIP', language)} -
- ) : serverIP && serverIP.public_ip ? ( -
- - {serverIP.public_ip} - - -
- ) : null} -
- )} - - )} - - {/* Aster 交易所的字段 */} - {selectedExchange.id === 'aster' && ( - <> -
- - setAsterUser(e.target.value)} - placeholder={t('enterUser', language)} - className="w-full px-3 py-2 rounded" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - required - /> -
- -
- - setAsterSigner(e.target.value)} - placeholder={t('enterSigner', language)} - className="w-full px-3 py-2 rounded" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - required - /> -
- -
- - setAsterPrivateKey(e.target.value)} - placeholder={t('enterPrivateKey', language)} - className="w-full px-3 py-2 rounded" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - required - /> -
- - )} - - {/* Hyperliquid 交易所的字段 */} - {selectedExchange.id === 'hyperliquid' && ( - <> - {/* 安全提示 banner */} -
-
- - 🔐 - -
-
- {t('hyperliquidAgentWalletTitle', language)} -
-
- {t('hyperliquidAgentWalletDesc', language)} -
-
-
-
- - {/* Agent Private Key 字段 */} -
- -
-
- - - {apiKey && ( - - )} -
- {apiKey && ( -
- {t('secureInputHint', language)} -
- )} -
-
- {t('hyperliquidAgentPrivateKeyDesc', language)} -
-
- - {/* Main Wallet Address 字段 */} -
- - - setHyperliquidWalletAddr(e.target.value) - } - placeholder={t( - 'enterHyperliquidMainWalletAddress', - language - )} - className="w-full px-3 py-2 rounded" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - required - /> -
- {t('hyperliquidMainWalletAddressDesc', language)} -
-
- - )} - - )} -
- -
- - -
-
-
- - {/* Binance Setup Guide Modal */} - {showGuide && ( -
setShowGuide(false)} - > -
e.stopPropagation()} - > -
-

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

- -
-
- {t('binanceSetupGuide', -
-
-
- )} - - {/* Two Stage Key Modal */} - -
- ) -} diff --git a/web/src/components/ComparisonChart.tsx b/web/src/components/ComparisonChart.tsx index 554c9b5076..593eb079b7 100644 --- a/web/src/components/ComparisonChart.tsx +++ b/web/src/components/ComparisonChart.tsx @@ -141,7 +141,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { if (isLoading) { return ( -
+
Loading comparison data...
@@ -150,7 +150,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { if (combinedData.length === 0) { return ( -
+
{t('noHistoricalData', language)} @@ -197,11 +197,8 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { if (active && payload && payload.length) { const data = payload[0].payload return ( -
-
+
+
{data.time} - #{data.index}
{traders.map((trader) => { @@ -218,15 +215,13 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { {trader.trader_name}
= 0 ? '#0ECB81' : '#F6465D' }} + className={`text-sm mono font-bold ${ + pnlPct >= 0 ? 'text-emerald-500' : 'text-red-500' + }`} > {pnlPct >= 0 ? '+' : ''} {pnlPct.toFixed(2)}% - + ({equity?.toFixed(2)} USDT)
@@ -253,27 +248,9 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { return (
-
+
{/* NOFX Watermark */} -
+
NOFX
@@ -305,13 +282,13 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { ))} - + `${value.toFixed(1)}%`} width={60} @@ -336,7 +313,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { strokeWidth={1.5} label={{ value: 'Break Even', - fill: '#848E9C', + fill: '#94A3B8', fontSize: 11, position: 'right', }} @@ -391,75 +368,40 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{/* Stats */} -
-
-
+
+
+
{t('comparisonMode', language)}
-
+
PnL %
-
-
+
+
{t('dataPoints', language)}
-
+
{t('count', language, { count: combinedData.length })}
-
-
+
+
{t('currentGap', language)}
1 ? '#F0B90B' : '#EAECEF' }} + className={`text-sm md:text-base font-bold mono ${ + currentGap > 1 ? 'text-darkmoon-gold' : 'text-darkmoon-text-primary' + }`} > {currentGap.toFixed(2)}%
-
-
+
+
{t('displayRange', language)}
-
+
{combinedData.length > MAX_DISPLAY_POINTS ? `${t('recent', language)} ${MAX_DISPLAY_POINTS}` : t('allData', language)} diff --git a/web/src/components/CompetitionPage.tsx b/web/src/components/CompetitionPage.tsx index e74d119b9e..169bed93e8 100644 --- a/web/src/components/CompetitionPage.tsx +++ b/web/src/components/CompetitionPage.tsx @@ -44,20 +44,20 @@ export function CompetitionPage() { if (!competition) { return (
-
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
@@ -71,35 +71,17 @@ export function CompetitionPage() { {/* Competition Header - 精简版 */}
-
- +
+
-

+

{t('aiCompetition', language)} - + 0 {t('traders', language)}

-

+

{t('liveBattle', language)}

@@ -107,15 +89,12 @@ export function CompetitionPage() {
{/* Empty State */} -
- -

+
+ +

{t('noTraders', language)}

-

+

{t('createFirstTrader', language)}

@@ -136,54 +115,32 @@ export function CompetitionPage() { {/* Competition Header - 精简版 */}
-
- +
+
-

+

{t('aiCompetition', language)} - + {competition.count} {t('traders', language)}

-

+

{t('liveBattle', language)}

-
+
{t('leader', language)}
-
+
{leader?.trader_name}
= 0 ? '#0ECB81' : '#F6465D', - }} + className={`text-sm font-semibold ${ + (leader?.total_pnl ?? 0) >= 0 ? 'text-emerald-500' : 'text-red-500' + }`} > {(leader?.total_pnl ?? 0) >= 0 ? '+' : ''} {leader?.total_pnl_pct?.toFixed(2) || '0.00'}% @@ -194,18 +151,12 @@ export function CompetitionPage() { {/* Left/Right Split: Performance Chart + Leaderboard */}
{/* Left: Performance Comparison Chart */} -
+
-

+

{t('performanceComparison', language)}

-
+
{t('realTimePnL', language)}
@@ -213,25 +164,12 @@ export function CompetitionPage() {
{/* Right: Leaderboard */} -
+
-

+

{t('leaderboard', language)}

-
+
{t('live', language)}
@@ -247,16 +185,11 @@ export function CompetitionPage() {
handleTraderClick(trader.trader_id)} - className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px] cursor-pointer hover:shadow-lg" - style={{ - background: isLeader - ? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, #0B0E11 100%)' - : '#0B0E11', - border: `1px solid ${isLeader ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`, - boxShadow: isLeader - ? '0 3px 15px rgba(240, 185, 11, 0.12), 0 0 0 1px rgba(240, 185, 11, 0.15)' - : '0 1px 4px rgba(0, 0, 0, 0.3)', - }} + className={`rounded p-3 transition-all duration-300 hover:translate-y-[-1px] cursor-pointer hover:shadow-lg ${ + isLeader + ? 'bg-gradient-to-br from-darkmoon-gold/10 to-darkmoon-surface border border-darkmoon-gold/40 shadow-[0_3px_15px_rgba(240,185,11,0.12),0_0_0_1px_rgba(240,185,11,0.15)]' + : 'bg-darkmoon-surface border border-darkmoon-border shadow-sm' + }`} >
{/* Rank & Name */} @@ -275,10 +208,7 @@ export function CompetitionPage() { />
-
+
{trader.trader_name}
{/* Total Equity */}
-
+
{t('equity', language)}
-
+
{trader.total_equity?.toFixed(2) || '0.00'}
{/* P&L */}
-
+
{t('pnl', language)}
= 0 - ? '#0ECB81' - : '#F6465D', - }} + className={`text-base md:text-lg font-bold mono ${ + (trader.total_pnl ?? 0) >= 0 + ? 'text-emerald-500' + : 'text-red-500' + }`} > {(trader.total_pnl ?? 0) >= 0 ? '+' : ''} {trader.total_pnl_pct?.toFixed(2) || '0.00'}%
-
+
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''} {trader.total_pnl?.toFixed(2) || '0.00'}
@@ -334,16 +256,13 @@ export function CompetitionPage() { {/* Positions */}
-
+
{t('pos', language)}
-
+
{trader.position_count}
-
+
{trader.margin_used_pct.toFixed(1)}%
@@ -351,18 +270,11 @@ export function CompetitionPage() { {/* Status */}
{trader.is_running ? '●' : '○'}
@@ -378,14 +290,8 @@ export function CompetitionPage() { {/* Head-to-Head Stats */} {competition.traders.length === 2 && ( -
-

+
+

{t('headToHead', language)}

@@ -407,21 +313,11 @@ export function CompetitionPage() { return (
= 0 ? '#0ECB81' : '#F6465D', - }} + className={`text-lg md:text-2xl font-bold mono mb-1 ${ + (trader.total_pnl ?? 0) >= 0 + ? 'text-emerald-500' + : 'text-red-500' + }`} > {trader.total_pnl_pct != null && !isNaN(trader.total_pnl_pct) @@ -445,28 +341,19 @@ export function CompetitionPage() { : '—'}
{hasValidData && isWinning && gap > 0 && ( -
+
{t('leadingBy', language, { gap: gap.toFixed(2) })}
)} {hasValidData && !isWinning && gap < 0 && ( -
+
{t('behindBy', language, { gap: Math.abs(gap).toFixed(2), })}
)} {!hasValidData && ( -
+
)} diff --git a/web/src/components/CryptoFeatureCard.tsx b/web/src/components/CryptoFeatureCard.tsx index d206d8aee7..1b2e3dc4c6 100644 --- a/web/src/components/CryptoFeatureCard.tsx +++ b/web/src/components/CryptoFeatureCard.tsx @@ -33,7 +33,7 @@ export const CryptoFeatureCard = React.forwardRef< className={cn( 'relative h-full overflow-hidden border-2 transition-all duration-300 rounded-xl', 'bg-gradient-to-br from-[#000000] to-[#0A0A0A]', - 'border-[#1A1A1A] hover:border-[#F0B90B]/50', + 'border-[#1A1A1A] hover:border-darkmoon-gold/50', isHovered && 'shadow-[0_0_20px_rgba(240,185,11,0.2)]', className )} @@ -46,29 +46,20 @@ export const CryptoFeatureCard = React.forwardRef< }} transition={{ duration: 0.3 }} > -
+
{/* Background pattern */}
{/* Icon container */} -
{icon}
+
{icon}
{/* Title */}

{title}

{/* Description */}

{description}

@@ -109,18 +98,15 @@ export const CryptoFeatureCard = React.forwardRef< >
{feature} diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index 02b7320f7a..22257c4fb6 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -10,20 +10,20 @@ export function Header({ simple = false }: HeaderProps) { const { language, setLanguage } = useLanguage() return ( -
+
{/* Left - Logo and Title */}
-
- NoFx Logo +
+ DarkMoon Logo
-

- {t('appTitle', language)} +

+ DarkMoon

{!simple && ( -

+

{t('subtitle', language)}

)} @@ -31,29 +31,24 @@ export function Header({ simple = false }: HeaderProps) {
{/* Right - Language Toggle (always show) */} -
+
diff --git a/web/src/components/HeaderBar.tsx b/web/src/components/HeaderBar.tsx index 7d3bd1c6e0..6f599c8d18 100644 --- a/web/src/components/HeaderBar.tsx +++ b/web/src/components/HeaderBar.tsx @@ -60,279 +60,89 @@ export default function HeaderBar({ } }, []) + const NavButton = ({ page, label, onClick }: { page: string, label: string, onClick: () => void }) => ( + + ) + + const NavLink = ({ page, label, href }: { page: string, label: string, href: string }) => ( + + {currentPage === page && ( + + )} + {label} + + ) + return ( -