diff --git a/mcp/client.go b/mcp/client.go index 12973753..eefddbe2 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -32,61 +32,60 @@ type Client struct { func New() *Client { // 默认配置 - var defaultClient = Client{ + return &Client{ Provider: ProviderDeepSeek, BaseURL: "https://api.deepseek.com/v1", Model: "deepseek-chat", Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据 } - return &defaultClient } // SetDeepSeekAPIKey 设置DeepSeek API密钥 -func (cfg *Client) SetDeepSeekAPIKey(apiKey string) { - cfg.Provider = ProviderDeepSeek - cfg.APIKey = apiKey - cfg.BaseURL = "https://api.deepseek.com/v1" - cfg.Model = "deepseek-chat" +func (client *Client) SetDeepSeekAPIKey(apiKey string) { + client.Provider = ProviderDeepSeek + client.APIKey = apiKey + client.BaseURL = "https://api.deepseek.com/v1" + client.Model = "deepseek-chat" } // SetQwenAPIKey 设置阿里云Qwen API密钥 -func (cfg *Client) SetQwenAPIKey(apiKey, secretKey string) { - cfg.Provider = ProviderQwen - cfg.APIKey = apiKey - cfg.SecretKey = secretKey - cfg.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" - cfg.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max +func (client *Client) SetQwenAPIKey(apiKey, secretKey string) { + client.Provider = ProviderQwen + client.APIKey = apiKey + client.SecretKey = secretKey + client.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" + client.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max } // SetCustomAPI 设置自定义OpenAI兼容API -func (cfg *Client) SetCustomAPI(apiURL, apiKey, modelName string) { - cfg.Provider = ProviderCustom - cfg.APIKey = apiKey +func (client *Client) SetCustomAPI(apiURL, apiKey, modelName string) { + client.Provider = ProviderCustom + client.APIKey = apiKey // 检查URL是否以#结尾,如果是则使用完整URL(不添加/chat/completions) if strings.HasSuffix(apiURL, "#") { - cfg.BaseURL = strings.TrimSuffix(apiURL, "#") - cfg.UseFullURL = true + client.BaseURL = strings.TrimSuffix(apiURL, "#") + client.UseFullURL = true } else { - cfg.BaseURL = apiURL - cfg.UseFullURL = false + client.BaseURL = apiURL + client.UseFullURL = false } - cfg.Model = modelName - cfg.Timeout = 120 * time.Second + client.Model = modelName + client.Timeout = 120 * time.Second } // SetClient 设置完整的AI配置(高级用户) -func (cfg *Client) SetClient(Client Client) { +func (client *Client) SetClient(Client Client) { if Client.Timeout == 0 { Client.Timeout = 30 * time.Second } - cfg = &Client + client = &Client } // CallWithMessages 使用 system + user prompt 调用AI API(推荐) -func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) { - if cfg.APIKey == "" { +func (client *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) { + if client.APIKey == "" { return "", fmt.Errorf("AI API密钥未设置,请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()") } @@ -99,7 +98,7 @@ func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, er fmt.Printf("⚠️ AI API调用失败,正在重试 (%d/%d)...\n", attempt, maxRetries) } - result, err := cfg.callOnce(systemPrompt, userPrompt) + result, err := client.callOnce(systemPrompt, userPrompt) if err == nil { if attempt > 1 { fmt.Printf("✓ AI API重试成功\n") @@ -125,7 +124,7 @@ func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, er } // callOnce 单次调用AI API(内部使用) -func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) { +func (client *Client) callOnce(systemPrompt, userPrompt string) (string, error) { // 构建 messages 数组 messages := []map[string]string{} @@ -145,7 +144,7 @@ func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) { // 构建请求体 requestBody := map[string]interface{}{ - "model": cfg.Model, + "model": client.Model, "messages": messages, "temperature": 0.5, // 降低temperature以提高JSON格式稳定性 "max_tokens": 2000, @@ -161,12 +160,12 @@ func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) { // 创建HTTP请求 var url string - if cfg.UseFullURL { + if client.UseFullURL { // 使用完整URL,不添加/chat/completions - url = cfg.BaseURL + url = client.BaseURL } else { // 默认行为:添加/chat/completions - url = fmt.Sprintf("%s/chat/completions", cfg.BaseURL) + url = fmt.Sprintf("%s/chat/completions", client.BaseURL) } req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { @@ -176,20 +175,20 @@ func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) { req.Header.Set("Content-Type", "application/json") // 根据不同的Provider设置认证方式 - switch cfg.Provider { + switch client.Provider { case ProviderDeepSeek: - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.APIKey)) case ProviderQwen: // 阿里云Qwen使用API-Key认证 - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.APIKey)) // 注意:如果使用的不是兼容模式,可能需要不同的认证方式 default: - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.APIKey)) } // 发送请求 - client := &http.Client{Timeout: cfg.Timeout} - resp, err := client.Do(req) + httpClient := &http.Client{Timeout: client.Timeout} + resp, err := httpClient.Do(req) if err != nil { return "", fmt.Errorf("发送请求失败: %w", err) } diff --git a/web/package-lock.json b/web/package-lock.json index c7990faa..32e3c01b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "clsx": "^2.1.1", "date-fns": "^4.1.0", + "lucide-react": "^0.552.0", "react": "^18.3.1", "react-dom": "^18.3.1", "recharts": "^2.15.2", @@ -2156,6 +2157,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.552.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.552.0.tgz", + "integrity": "sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", diff --git a/web/package.json b/web/package.json index a126d761..dfe57495 100644 --- a/web/package.json +++ b/web/package.json @@ -8,22 +8,23 @@ "preview": "vite preview" }, "dependencies": { + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lucide-react": "^0.552.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "zustand": "^5.0.2", - "swr": "^2.2.5", "recharts": "^2.15.2", - "date-fns": "^4.1.0", - "clsx": "^2.1.1" + "swr": "^2.2.5", + "zustand": "^5.0.2" }, "devDependencies": { "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", "@vitejs/plugin-react": "^4.3.4", - "typescript": "^5.8.3", - "vite": "^6.0.7", - "tailwindcss": "^3.4.17", + "autoprefixer": "^10.4.20", "postcss": "^8.4.49", - "autoprefixer": "^10.4.20" + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "vite": "^6.0.7" } } diff --git a/web/public/images/logo.png b/web/public/images/logo.png new file mode 100644 index 00000000..28ec8c71 Binary files /dev/null and b/web/public/images/logo.png differ diff --git a/web/src/components/AILearning.tsx b/web/src/components/AILearning.tsx index 20da61d2..f44baf27 100644 --- a/web/src/components/AILearning.tsx +++ b/web/src/components/AILearning.tsx @@ -2,6 +2,7 @@ import useSWR from 'swr'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; import { api } from '../lib/api'; +import { Brain, BarChart3, TrendingUp, TrendingDown, Sparkles, Coins, Trophy, ScrollText, Lightbulb } from 'lucide-react'; interface TradeOutcome { symbol: string; @@ -72,7 +73,9 @@ export default function AILearning({ traderId }: AILearningProps) { if (!performance) { return (
-
📊 {t('loading', language)}
+
+ {t('loading', language)} +
); } @@ -81,7 +84,7 @@ export default function AILearning({ traderId }: AILearningProps) { return (
- 🧠 +

{t('aiLearning', language)}

@@ -109,12 +112,12 @@ export default function AILearning({ traderId }: AILearningProps) { filter: 'blur(60px)' }} />
-
- 🧠 +

{performance.total_trades}

-
📊 Trades
+
+ Trades +
@@ -199,7 +204,9 @@ export default function AILearning({ traderId }: AILearningProps) {
+{(performance.avg_win || 0).toFixed(2)}
-
📈 USDT Average
+
+ USDT Average +
@@ -220,7 +227,9 @@ export default function AILearning({ traderId }: AILearningProps) {
{(performance.avg_loss || 0).toFixed(2)}
-
📉 USDT Average
+
+ USDT Average +
@@ -239,11 +248,11 @@ export default function AILearning({ traderId }: AILearningProps) { }} />
-
- 🧬 +
夏普比率
@@ -307,11 +316,11 @@ export default function AILearning({ traderId }: AILearningProps) { }} />
-
- 💰 +
@@ -373,7 +382,7 @@ export default function AILearning({ traderId }: AILearningProps) { boxShadow: '0 4px 16px rgba(16, 185, 129, 0.1)' }}>
- 🏆 + {t('bestPerformer', language)}
@@ -395,7 +404,7 @@ export default function AILearning({ traderId }: AILearningProps) { boxShadow: '0 4px 16px rgba(248, 113, 113, 0.1)' }}>
- 📉 + {t('worstPerformer', language)}
@@ -428,7 +437,7 @@ export default function AILearning({ traderId }: AILearningProps) { backdropFilter: 'blur(10px)' }}>

- 📊 {t('symbolPerformance', language)} + {t('symbolPerformance', language)}

@@ -488,7 +497,7 @@ export default function AILearning({ traderId }: AILearningProps) { backdropFilter: 'blur(10px)' }}>
- 📜 +

{t('tradeHistory', language)}

@@ -631,7 +640,9 @@ export default function AILearning({ traderId }: AILearningProps) { }) ) : (

-
📜
+
+ +
{t('noCompletedTrades', language)}
)} @@ -646,11 +657,11 @@ export default function AILearning({ traderId }: AILearningProps) { boxShadow: '0 4px 16px rgba(240, 185, 11, 0.1)' }}>
-
- 💡 +

{t('howAILearns', language)}

diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index d69ce7f3..7af6c623 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -7,6 +7,7 @@ import { t } from '../i18n/translations'; import { getExchangeIcon } from './ExchangeIcons'; import { getModelIcon } from './ModelIcons'; import { TraderConfigModal } from './TraderConfigModal'; +import { Bot, Brain, Landmark, BarChart3, Trash2, Plus, Users, AlertTriangle } from 'lucide-react'; // 获取友好的AI模型名称 function getModelDisplayName(modelId: string): string { @@ -440,11 +441,11 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { {/* Header */}
-
- 🤖 +

@@ -465,28 +466,30 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
- + - +

@@ -517,8 +521,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{/* AI Models */}
-

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

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

{configuredModels.map(model => { @@ -557,7 +562,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { })} {configuredModels.length === 0 && (
-
🧠
+
暂无已配置的AI模型
)} @@ -566,8 +571,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { {/* Exchanges */}
-

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

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

{configuredExchanges.map(exchange => { @@ -598,7 +604,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { })} {configuredExchanges.length === 0 && (
-
🏦
+
暂无已配置的交易所
)} @@ -610,23 +616,24 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {

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

{traders && traders.length > 0 ? (
{traders.map(trader => ( -
-
- 🤖 +
@@ -658,12 +665,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
- + - +
@@ -701,7 +709,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
) : (
-
🤖
+
{t('noTraders', language)}
{t('createFirstTrader', language)}
{(configuredModels.length === 0 || configuredExchanges.length === 0) && ( @@ -946,7 +954,7 @@ function ModelConfigModal({ style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }} title="删除配置" > - 🗑️ + )}
@@ -1138,7 +1146,7 @@ function ExchangeConfigModal({ style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }} title="删除配置" > - 🗑️ + )}
diff --git a/web/src/components/EquityChart.tsx b/web/src/components/EquityChart.tsx index b8b846cf..bfa89617 100644 --- a/web/src/components/EquityChart.tsx +++ b/web/src/components/EquityChart.tsx @@ -13,6 +13,7 @@ import useSWR from 'swr'; import { api } from '../lib/api'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; +import { AlertTriangle, BarChart3, DollarSign, Percent, TrendingUp as ArrowUp, TrendingDown as ArrowDown } from 'lucide-react' interface EquityPoint { timestamp: string; @@ -52,16 +53,26 @@ export function EquityChart({ traderId }: EquityChartProps) { if (error) { return ( -
-
-
⚠️
+
+
+
-
{t('loadingError', language)}
-
{error.message}
+
+ {t('loadingError', language)} +
+
+ {error.message} +
- ); + ) } // 过滤掉无效数据:total_equity为0或小于1的数据点(API失败导致) @@ -69,15 +80,21 @@ export function EquityChart({ traderId }: EquityChartProps) { if (!validHistory || validHistory.length === 0) { return ( -
-

{t('accountEquityCurve', language)}

-
-
📊
-
{t('noHistoricalData', language)}
-
{t('dataWillAppear', language)}
+
+

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

+
+
+ +
+
+ {t('noHistoricalData', language)} +
+
{t('dataWillAppear', language)}
- ); + ) } // 限制显示最近的数据点(性能优化) @@ -161,142 +178,238 @@ export function EquityChart({ traderId }: EquityChartProps) { }; return ( -
+
{/* Header */} -
-
-

{t('accountEquityCurve', language)}

-
- +
+
+

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

+
+ {account?.total_equity.toFixed(2) || '0.00'} - USDT - -
+ USDT + + +
+ - {isProfit ? '▲' : '▼'} {isProfit ? '+' : ''} + {isProfit ? : } + {isProfit ? '+' : ''} {currentValue.raw_pnl_pct}% - - ({isProfit ? '+' : ''}{currentValue.raw_pnl.toFixed(2)} USDT) + + ({isProfit ? '+' : ''} + {currentValue.raw_pnl.toFixed(2)} USDT)
{/* Display Mode Toggle */} -
+
{/* Chart */} -
- - - - - - - - - - - - displayMode === 'dollar' ? `$${value.toFixed(0)}` : `${value}%` - } - /> - } /> - - 50 ? false : { fill: '#F0B90B', r: 3 }} - activeDot={{ r: 6, fill: '#FCD535', stroke: '#F0B90B', strokeWidth: 2 }} - connectNulls={true} - /> - - +
+ + + + + + + + + + + + displayMode === 'dollar' ? `$${value.toFixed(0)}` : `${value}%` + } + /> + } /> + + 50 ? false : { fill: '#F0B90B', r: 3 }} + activeDot={{ + r: 6, + fill: '#FCD535', + stroke: '#F0B90B', + strokeWidth: 2, + }} + connectNulls={true} + /> + +
{/* Footer Stats */} -
-
-
{t('initialBalance', language)}
-
+
+
+
+ {t('initialBalance', language)} +
+
{initialBalance.toFixed(2)} USDT
-
-
{t('currentEquity', language)}
-
+
+
+ {t('currentEquity', language)} +
+
{currentValue.raw_equity.toFixed(2)} USDT
-
-
{t('historicalCycles', language)}
-
{validHistory.length} {t('cycles', language)}
+
+
+ {t('historicalCycles', language)} +
+
+ {validHistory.length} {t('cycles', language)} +
-
-
{t('displayRange', language)}
-
+
+
+ {t('displayRange', language)} +
+
{validHistory.length > MAX_DISPLAY_POINTS ? `${t('recent', language)} ${MAX_DISPLAY_POINTS}` - : t('allData', language) - } + : t('allData', language)}
- ); + ) }