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 && (
)}
@@ -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) {
onTraderSelect?.(trader.trader_id)}
- className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
+ className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1"
style={{ background: 'rgba(99, 102, 241, 0.1)', color: '#6366F1' }}
>
- 📊 查看
+
+ 查看
-
+
handleEditTrader(trader.trader_id)}
disabled={trader.is_running}
@@ -679,20 +687,20 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
handleToggleTrader(trader.trader_id, trader.is_running || false)}
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
- style={trader.is_running
+ style={trader.is_running
? { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
: { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
}
>
{trader.is_running ? t('stop', language) : t('start', language)}
-
+
handleDeleteTrader(trader.trader_id)}
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
>
- 🗑️
+
@@ -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 */}
-
+
setDisplayMode('dollar')}
- className="px-3 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-bold transition-all"
- style={displayMode === 'dollar'
- ? { background: '#F0B90B', color: '#000', boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)' }
- : { background: 'transparent', color: '#848E9C' }
+ className='px-3 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-bold transition-all flex items-center gap-1'
+ style={
+ displayMode === 'dollar'
+ ? {
+ background: '#F0B90B',
+ color: '#000',
+ boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)',
+ }
+ : { background: 'transparent', color: '#848E9C' }
}
>
- 💵 USDT
+ USDT
setDisplayMode('percent')}
- className="px-3 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-bold transition-all"
- style={displayMode === 'percent'
- ? { background: '#F0B90B', color: '#000', boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)' }
- : { background: 'transparent', color: '#848E9C' }
+ className='px-3 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-bold transition-all flex items-center gap-1'
+ style={
+ displayMode === 'percent'
+ ? {
+ background: '#F0B90B',
+ color: '#000',
+ boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)',
+ }
+ : { background: 'transparent', color: '#848E9C' }
}
>
- 📊 %
+
{/* 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)}
- );
+ )
}