mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 15:57:01 +08:00
手动合并冲突,保留TraderConfigModal功能并添加lucide-react图标支持
- 解决AITradersPage.tsx合并冲突,保留TraderConfigModal导入 - 添加lucide-react图标库支持 - 保留信号源配置的OI TOP URL功能 - 使用我们版本解决其他文件冲突,保持UI简洁 - 确保编译成功和依赖正确安装 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -32,61 +32,60 @@ type Client struct {
|
|||||||
|
|
||||||
func New() *Client {
|
func New() *Client {
|
||||||
// 默认配置
|
// 默认配置
|
||||||
var defaultClient = Client{
|
return &Client{
|
||||||
Provider: ProviderDeepSeek,
|
Provider: ProviderDeepSeek,
|
||||||
BaseURL: "https://api.deepseek.com/v1",
|
BaseURL: "https://api.deepseek.com/v1",
|
||||||
Model: "deepseek-chat",
|
Model: "deepseek-chat",
|
||||||
Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据
|
Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据
|
||||||
}
|
}
|
||||||
return &defaultClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDeepSeekAPIKey 设置DeepSeek API密钥
|
// SetDeepSeekAPIKey 设置DeepSeek API密钥
|
||||||
func (cfg *Client) SetDeepSeekAPIKey(apiKey string) {
|
func (client *Client) SetDeepSeekAPIKey(apiKey string) {
|
||||||
cfg.Provider = ProviderDeepSeek
|
client.Provider = ProviderDeepSeek
|
||||||
cfg.APIKey = apiKey
|
client.APIKey = apiKey
|
||||||
cfg.BaseURL = "https://api.deepseek.com/v1"
|
client.BaseURL = "https://api.deepseek.com/v1"
|
||||||
cfg.Model = "deepseek-chat"
|
client.Model = "deepseek-chat"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetQwenAPIKey 设置阿里云Qwen API密钥
|
// SetQwenAPIKey 设置阿里云Qwen API密钥
|
||||||
func (cfg *Client) SetQwenAPIKey(apiKey, secretKey string) {
|
func (client *Client) SetQwenAPIKey(apiKey, secretKey string) {
|
||||||
cfg.Provider = ProviderQwen
|
client.Provider = ProviderQwen
|
||||||
cfg.APIKey = apiKey
|
client.APIKey = apiKey
|
||||||
cfg.SecretKey = secretKey
|
client.SecretKey = secretKey
|
||||||
cfg.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
client.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||||
cfg.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max
|
client.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCustomAPI 设置自定义OpenAI兼容API
|
// SetCustomAPI 设置自定义OpenAI兼容API
|
||||||
func (cfg *Client) SetCustomAPI(apiURL, apiKey, modelName string) {
|
func (client *Client) SetCustomAPI(apiURL, apiKey, modelName string) {
|
||||||
cfg.Provider = ProviderCustom
|
client.Provider = ProviderCustom
|
||||||
cfg.APIKey = apiKey
|
client.APIKey = apiKey
|
||||||
|
|
||||||
// 检查URL是否以#结尾,如果是则使用完整URL(不添加/chat/completions)
|
// 检查URL是否以#结尾,如果是则使用完整URL(不添加/chat/completions)
|
||||||
if strings.HasSuffix(apiURL, "#") {
|
if strings.HasSuffix(apiURL, "#") {
|
||||||
cfg.BaseURL = strings.TrimSuffix(apiURL, "#")
|
client.BaseURL = strings.TrimSuffix(apiURL, "#")
|
||||||
cfg.UseFullURL = true
|
client.UseFullURL = true
|
||||||
} else {
|
} else {
|
||||||
cfg.BaseURL = apiURL
|
client.BaseURL = apiURL
|
||||||
cfg.UseFullURL = false
|
client.UseFullURL = false
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Model = modelName
|
client.Model = modelName
|
||||||
cfg.Timeout = 120 * time.Second
|
client.Timeout = 120 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetClient 设置完整的AI配置(高级用户)
|
// SetClient 设置完整的AI配置(高级用户)
|
||||||
func (cfg *Client) SetClient(Client Client) {
|
func (client *Client) SetClient(Client Client) {
|
||||||
if Client.Timeout == 0 {
|
if Client.Timeout == 0 {
|
||||||
Client.Timeout = 30 * time.Second
|
Client.Timeout = 30 * time.Second
|
||||||
}
|
}
|
||||||
cfg = &Client
|
client = &Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallWithMessages 使用 system + user prompt 调用AI API(推荐)
|
// CallWithMessages 使用 system + user prompt 调用AI API(推荐)
|
||||||
func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) {
|
func (client *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) {
|
||||||
if cfg.APIKey == "" {
|
if client.APIKey == "" {
|
||||||
return "", fmt.Errorf("AI API密钥未设置,请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()")
|
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)
|
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 err == nil {
|
||||||
if attempt > 1 {
|
if attempt > 1 {
|
||||||
fmt.Printf("✓ AI API重试成功\n")
|
fmt.Printf("✓ AI API重试成功\n")
|
||||||
@@ -125,7 +124,7 @@ func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// callOnce 单次调用AI API(内部使用)
|
// callOnce 单次调用AI API(内部使用)
|
||||||
func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) {
|
func (client *Client) callOnce(systemPrompt, userPrompt string) (string, error) {
|
||||||
// 构建 messages 数组
|
// 构建 messages 数组
|
||||||
messages := []map[string]string{}
|
messages := []map[string]string{}
|
||||||
|
|
||||||
@@ -145,7 +144,7 @@ func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) {
|
|||||||
|
|
||||||
// 构建请求体
|
// 构建请求体
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"model": cfg.Model,
|
"model": client.Model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"temperature": 0.5, // 降低temperature以提高JSON格式稳定性
|
"temperature": 0.5, // 降低temperature以提高JSON格式稳定性
|
||||||
"max_tokens": 2000,
|
"max_tokens": 2000,
|
||||||
@@ -161,12 +160,12 @@ func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) {
|
|||||||
|
|
||||||
// 创建HTTP请求
|
// 创建HTTP请求
|
||||||
var url string
|
var url string
|
||||||
if cfg.UseFullURL {
|
if client.UseFullURL {
|
||||||
// 使用完整URL,不添加/chat/completions
|
// 使用完整URL,不添加/chat/completions
|
||||||
url = cfg.BaseURL
|
url = client.BaseURL
|
||||||
} else {
|
} else {
|
||||||
// 默认行为:添加/chat/completions
|
// 默认行为:添加/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))
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -176,20 +175,20 @@ func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) {
|
|||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
// 根据不同的Provider设置认证方式
|
// 根据不同的Provider设置认证方式
|
||||||
switch cfg.Provider {
|
switch client.Provider {
|
||||||
case ProviderDeepSeek:
|
case ProviderDeepSeek:
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey))
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.APIKey))
|
||||||
case ProviderQwen:
|
case ProviderQwen:
|
||||||
// 阿里云Qwen使用API-Key认证
|
// 阿里云Qwen使用API-Key认证
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey))
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.APIKey))
|
||||||
// 注意:如果使用的不是兼容模式,可能需要不同的认证方式
|
// 注意:如果使用的不是兼容模式,可能需要不同的认证方式
|
||||||
default:
|
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}
|
httpClient := &http.Client{Timeout: client.Timeout}
|
||||||
resp, err := client.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("发送请求失败: %w", err)
|
return "", fmt.Errorf("发送请求失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
10
web/package-lock.json
generated
10
web/package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"lucide-react": "^0.552.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
@@ -2156,6 +2157,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"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": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
|
|||||||
@@ -8,22 +8,23 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"lucide-react": "^0.552.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"zustand": "^5.0.2",
|
|
||||||
"swr": "^2.2.5",
|
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
"date-fns": "^4.1.0",
|
"swr": "^2.2.5",
|
||||||
"clsx": "^2.1.1"
|
"zustand": "^5.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.3.17",
|
"@types/react": "^18.3.17",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^18.3.5",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"typescript": "^5.8.3",
|
"autoprefixer": "^10.4.20",
|
||||||
"vite": "^6.0.7",
|
|
||||||
"tailwindcss": "^3.4.17",
|
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"autoprefixer": "^10.4.20"
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vite": "^6.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
web/public/images/logo.png
Normal file
BIN
web/public/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -2,6 +2,7 @@ import useSWR from 'swr';
|
|||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
import { t } from '../i18n/translations';
|
import { t } from '../i18n/translations';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
import { Brain, BarChart3, TrendingUp, TrendingDown, Sparkles, Coins, Trophy, ScrollText, Lightbulb } from 'lucide-react';
|
||||||
|
|
||||||
interface TradeOutcome {
|
interface TradeOutcome {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
@@ -72,7 +73,9 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
if (!performance) {
|
if (!performance) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
<div className="rounded p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||||
<div style={{ color: '#848E9C' }}>📊 {t('loading', language)}</div>
|
<div className="flex items-center gap-2" style={{ color: '#848E9C' }}>
|
||||||
|
<BarChart3 className="w-4 h-4" /> {t('loading', language)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -81,7 +84,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="rounded p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
<div className="rounded p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span className="text-xl">🧠</span>
|
<Brain className="w-5 h-5" style={{ color: '#8B5CF6' }} />
|
||||||
<h2 className="text-lg font-bold" style={{ color: '#EAECEF' }}>{t('aiLearning', language)}</h2>
|
<h2 className="text-lg font-bold" style={{ color: '#EAECEF' }}>{t('aiLearning', language)}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: '#848E9C' }}>
|
<div style={{ color: '#848E9C' }}>
|
||||||
@@ -109,12 +112,12 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
filter: 'blur(60px)'
|
filter: 'blur(60px)'
|
||||||
}} />
|
}} />
|
||||||
<div className="relative flex items-center gap-4">
|
<div className="relative flex items-center gap-4">
|
||||||
<div className="w-16 h-16 rounded-2xl flex items-center justify-center text-3xl" style={{
|
<div className="w-16 h-16 rounded-2xl flex items-center justify-center" style={{
|
||||||
background: 'linear-gradient(135deg, #8B5CF6 0%, #6366F1 100%)',
|
background: 'linear-gradient(135deg, #8B5CF6 0%, #6366F1 100%)',
|
||||||
boxShadow: '0 8px 24px rgba(139, 92, 246, 0.5)',
|
boxShadow: '0 8px 24px rgba(139, 92, 246, 0.5)',
|
||||||
border: '2px solid rgba(255, 255, 255, 0.1)'
|
border: '2px solid rgba(255, 255, 255, 0.1)'
|
||||||
}}>
|
}}>
|
||||||
🧠
|
<Brain className="w-8 h-8" style={{ color: '#FFF' }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl font-bold mb-1" style={{
|
<h2 className="text-3xl font-bold mb-1" style={{
|
||||||
@@ -149,7 +152,9 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
<div className="text-4xl font-bold mono mb-1" style={{ color: '#E0E7FF' }}>
|
<div className="text-4xl font-bold mono mb-1" style={{ color: '#E0E7FF' }}>
|
||||||
{performance.total_trades}
|
{performance.total_trades}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs" style={{ color: '#6366F1' }}>📊 Trades</div>
|
<div className="text-xs flex items-center gap-1" style={{ color: '#6366F1' }}>
|
||||||
|
<BarChart3 className="w-3 h-3" /> Trades
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -199,7 +204,9 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
<div className="text-4xl font-bold mono mb-1" style={{ color: '#10B981' }}>
|
<div className="text-4xl font-bold mono mb-1" style={{ color: '#10B981' }}>
|
||||||
+{(performance.avg_win || 0).toFixed(2)}
|
+{(performance.avg_win || 0).toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs" style={{ color: '#6EE7B7' }}>📈 USDT Average</div>
|
<div className="text-xs flex items-center gap-1" style={{ color: '#6EE7B7' }}>
|
||||||
|
<TrendingUp className="w-3 h-3" /> USDT Average
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -220,7 +227,9 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
<div className="text-4xl font-bold mono mb-1" style={{ color: '#F87171' }}>
|
<div className="text-4xl font-bold mono mb-1" style={{ color: '#F87171' }}>
|
||||||
{(performance.avg_loss || 0).toFixed(2)}
|
{(performance.avg_loss || 0).toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs" style={{ color: '#FCA5A5' }}>📉 USDT Average</div>
|
<div className="text-xs flex items-center gap-1" style={{ color: '#FCA5A5' }}>
|
||||||
|
<TrendingDown className="w-3 h-3" /> USDT Average
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,11 +248,11 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
}} />
|
}} />
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl" style={{
|
<div className="w-12 h-12 rounded-xl flex items-center justify-center" style={{
|
||||||
background: 'rgba(139, 92, 246, 0.3)',
|
background: 'rgba(139, 92, 246, 0.3)',
|
||||||
border: '1px solid rgba(139, 92, 246, 0.5)'
|
border: '1px solid rgba(139, 92, 246, 0.5)'
|
||||||
}}>
|
}}>
|
||||||
🧬
|
<Sparkles className="w-6 h-6" style={{ color: '#A78BFA' }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-lg font-bold" style={{ color: '#C4B5FD' }}>夏普比率</div>
|
<div className="text-lg font-bold" style={{ color: '#C4B5FD' }}>夏普比率</div>
|
||||||
@@ -307,11 +316,11 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
}} />
|
}} />
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl" style={{
|
<div className="w-12 h-12 rounded-xl flex items-center justify-center" style={{
|
||||||
background: 'rgba(240, 185, 11, 0.3)',
|
background: 'rgba(240, 185, 11, 0.3)',
|
||||||
border: '1px solid rgba(240, 185, 11, 0.5)'
|
border: '1px solid rgba(240, 185, 11, 0.5)'
|
||||||
}}>
|
}}>
|
||||||
💰
|
<Coins className="w-6 h-6" style={{ color: '#FCD34D' }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-lg font-bold" style={{ color: '#FCD34D' }}>
|
<div className="text-lg font-bold" style={{ color: '#FCD34D' }}>
|
||||||
@@ -373,7 +382,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
boxShadow: '0 4px 16px rgba(16, 185, 129, 0.1)'
|
boxShadow: '0 4px 16px rgba(16, 185, 129, 0.1)'
|
||||||
}}>
|
}}>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<span className="text-2xl">🏆</span>
|
<Trophy className="w-6 h-6" style={{ color: '#10B981' }} />
|
||||||
<span className="text-sm font-semibold" style={{ color: '#6EE7B7' }}>{t('bestPerformer', language)}</span>
|
<span className="text-sm font-semibold" style={{ color: '#6EE7B7' }}>{t('bestPerformer', language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-3xl font-bold mono mb-1" style={{ color: '#10B981' }}>
|
<div className="text-3xl font-bold mono mb-1" style={{ color: '#10B981' }}>
|
||||||
@@ -395,7 +404,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
boxShadow: '0 4px 16px rgba(248, 113, 113, 0.1)'
|
boxShadow: '0 4px 16px rgba(248, 113, 113, 0.1)'
|
||||||
}}>
|
}}>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<span className="text-2xl">📉</span>
|
<TrendingDown className="w-6 h-6" style={{ color: '#F87171' }} />
|
||||||
<span className="text-sm font-semibold" style={{ color: '#FCA5A5' }}>{t('worstPerformer', language)}</span>
|
<span className="text-sm font-semibold" style={{ color: '#FCA5A5' }}>{t('worstPerformer', language)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-3xl font-bold mono mb-1" style={{ color: '#F87171' }}>
|
<div className="text-3xl font-bold mono mb-1" style={{ color: '#F87171' }}>
|
||||||
@@ -428,7 +437,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
backdropFilter: 'blur(10px)'
|
backdropFilter: 'blur(10px)'
|
||||||
}}>
|
}}>
|
||||||
<h3 className="font-bold flex items-center gap-2 text-lg" style={{ color: '#E0E7FF' }}>
|
<h3 className="font-bold flex items-center gap-2 text-lg" style={{ color: '#E0E7FF' }}>
|
||||||
📊 {t('symbolPerformance', language)}
|
<BarChart3 className="w-5 h-5" /> {t('symbolPerformance', language)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 280px)' }}>
|
<div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 280px)' }}>
|
||||||
@@ -488,7 +497,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
backdropFilter: 'blur(10px)'
|
backdropFilter: 'blur(10px)'
|
||||||
}}>
|
}}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-2xl">📜</span>
|
<ScrollText className="w-6 h-6" style={{ color: '#FCD34D' }} />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold text-lg" style={{ color: '#FCD34D' }}>{t('tradeHistory', language)}</h3>
|
<h3 className="font-bold text-lg" style={{ color: '#FCD34D' }}>{t('tradeHistory', language)}</h3>
|
||||||
<p className="text-xs" style={{ color: '#94A3B8' }}>
|
<p className="text-xs" style={{ color: '#94A3B8' }}>
|
||||||
@@ -631,7 +640,9 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className="p-6 text-center">
|
<div className="p-6 text-center">
|
||||||
<div className="text-4xl mb-2 opacity-50">📜</div>
|
<div className="mb-2 flex justify-center opacity-50">
|
||||||
|
<ScrollText className="w-10 h-10" style={{ color: '#94A3B8' }} />
|
||||||
|
</div>
|
||||||
<div style={{ color: '#94A3B8' }}>{t('noCompletedTrades', language)}</div>
|
<div style={{ color: '#94A3B8' }}>{t('noCompletedTrades', language)}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -646,11 +657,11 @@ export default function AILearning({ traderId }: AILearningProps) {
|
|||||||
boxShadow: '0 4px 16px rgba(240, 185, 11, 0.1)'
|
boxShadow: '0 4px 16px rgba(240, 185, 11, 0.1)'
|
||||||
}}>
|
}}>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className="w-10 h-10 rounded-lg flex items-center justify-center text-xl flex-shrink-0" style={{
|
<div className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0" style={{
|
||||||
background: 'rgba(240, 185, 11, 0.2)',
|
background: 'rgba(240, 185, 11, 0.2)',
|
||||||
border: '1px solid rgba(240, 185, 11, 0.3)'
|
border: '1px solid rgba(240, 185, 11, 0.3)'
|
||||||
}}>
|
}}>
|
||||||
💡
|
<Lightbulb className="w-5 h-5" style={{ color: '#FCD34D' }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold mb-3 text-base" style={{ color: '#FCD34D' }}>{t('howAILearns', language)}</h3>
|
<h3 className="font-bold mb-3 text-base" style={{ color: '#FCD34D' }}>{t('howAILearns', language)}</h3>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { t } from '../i18n/translations';
|
|||||||
import { getExchangeIcon } from './ExchangeIcons';
|
import { getExchangeIcon } from './ExchangeIcons';
|
||||||
import { getModelIcon } from './ModelIcons';
|
import { getModelIcon } from './ModelIcons';
|
||||||
import { TraderConfigModal } from './TraderConfigModal';
|
import { TraderConfigModal } from './TraderConfigModal';
|
||||||
|
import { Bot, Brain, Landmark, BarChart3, Trash2, Plus, Users, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
// 获取友好的AI模型名称
|
// 获取友好的AI模型名称
|
||||||
function getModelDisplayName(modelId: string): string {
|
function getModelDisplayName(modelId: string): string {
|
||||||
@@ -440,11 +441,11 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl" style={{
|
<div className="w-12 h-12 rounded-xl flex items-center justify-center" style={{
|
||||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||||
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)'
|
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)'
|
||||||
}}>
|
}}>
|
||||||
🤖
|
<Bot className="w-6 h-6" style={{ color: '#000' }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||||
@@ -465,28 +466,30 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={handleAddModel}
|
onClick={handleAddModel}
|
||||||
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 flex items-center gap-2"
|
||||||
style={{
|
style={{
|
||||||
background: '#2B3139',
|
background: '#2B3139',
|
||||||
color: '#EAECEF',
|
color: '#EAECEF',
|
||||||
border: '1px solid #474D57'
|
border: '1px solid #474D57'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
➕ {t('aiModels', language)}
|
<Plus className="w-4 h-4" />
|
||||||
|
{t('aiModels', language)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleAddExchange}
|
onClick={handleAddExchange}
|
||||||
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 flex items-center gap-2"
|
||||||
style={{
|
style={{
|
||||||
background: '#2B3139',
|
background: '#2B3139',
|
||||||
color: '#EAECEF',
|
color: '#EAECEF',
|
||||||
border: '1px solid #474D57'
|
border: '1px solid #474D57'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
➕ {t('exchanges', language)}
|
<Plus className="w-4 h-4" />
|
||||||
|
{t('exchanges', language)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowSignalSourceModal(true)}
|
onClick={() => setShowSignalSourceModal(true)}
|
||||||
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||||
@@ -502,13 +505,14 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateModal(true)}
|
onClick={() => setShowCreateModal(true)}
|
||||||
disabled={configuredModels.length === 0 || configuredExchanges.length === 0}
|
disabled={configuredModels.length === 0 || configuredExchanges.length === 0}
|
||||||
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||||
style={{
|
style={{
|
||||||
background: (configuredModels.length > 0 && configuredExchanges.length > 0) ? '#F0B90B' : '#2B3139',
|
background: (configuredModels.length > 0 && configuredExchanges.length > 0) ? '#F0B90B' : '#2B3139',
|
||||||
color: (configuredModels.length > 0 && configuredExchanges.length > 0) ? '#000' : '#848E9C'
|
color: (configuredModels.length > 0 && configuredExchanges.length > 0) ? '#000' : '#848E9C'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
➕ {t('createTrader', language)}
|
<Plus className="w-4 h-4" />
|
||||||
|
{t('createTrader', language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -517,8 +521,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{/* AI Models */}
|
{/* AI Models */}
|
||||||
<div className="binance-card p-4">
|
<div className="binance-card p-4">
|
||||||
<h3 className="text-lg font-semibold mb-3" style={{ color: '#EAECEF' }}>
|
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||||
🧠 {t('aiModels', language)}
|
<Brain className="w-5 h-5" style={{ color: '#60a5fa' }} />
|
||||||
|
{t('aiModels', language)}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{configuredModels.map(model => {
|
{configuredModels.map(model => {
|
||||||
@@ -557,7 +562,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
})}
|
})}
|
||||||
{configuredModels.length === 0 && (
|
{configuredModels.length === 0 && (
|
||||||
<div className="text-center py-8" style={{ color: '#848E9C' }}>
|
<div className="text-center py-8" style={{ color: '#848E9C' }}>
|
||||||
<div className="text-2xl mb-2">🧠</div>
|
<Brain className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||||
<div className="text-sm">暂无已配置的AI模型</div>
|
<div className="text-sm">暂无已配置的AI模型</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -566,8 +571,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
|
|
||||||
{/* Exchanges */}
|
{/* Exchanges */}
|
||||||
<div className="binance-card p-4">
|
<div className="binance-card p-4">
|
||||||
<h3 className="text-lg font-semibold mb-3" style={{ color: '#EAECEF' }}>
|
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||||
🏦 {t('exchanges', language)}
|
<Landmark className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||||
|
{t('exchanges', language)}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{configuredExchanges.map(exchange => {
|
{configuredExchanges.map(exchange => {
|
||||||
@@ -598,7 +604,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
})}
|
})}
|
||||||
{configuredExchanges.length === 0 && (
|
{configuredExchanges.length === 0 && (
|
||||||
<div className="text-center py-8" style={{ color: '#848E9C' }}>
|
<div className="text-center py-8" style={{ color: '#848E9C' }}>
|
||||||
<div className="text-2xl mb-2">🏦</div>
|
<Landmark className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||||
<div className="text-sm">暂无已配置的交易所</div>
|
<div className="text-sm">暂无已配置的交易所</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -610,23 +616,24 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
<div className="binance-card p-6">
|
<div className="binance-card p-6">
|
||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<h2 className="text-xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
<h2 className="text-xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||||
👥 {t('currentTraders', language)}
|
<Users className="w-6 h-6" style={{ color: '#F0B90B' }} />
|
||||||
|
{t('currentTraders', language)}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{traders && traders.length > 0 ? (
|
{traders && traders.length > 0 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{traders.map(trader => (
|
{traders.map(trader => (
|
||||||
<div key={trader.trader_id}
|
<div key={trader.trader_id}
|
||||||
className="flex items-center justify-between p-4 rounded transition-all hover:translate-y-[-1px]"
|
className="flex items-center justify-between p-4 rounded transition-all hover:translate-y-[-1px]"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-12 h-12 rounded-full flex items-center justify-center text-xl"
|
<div className="w-12 h-12 rounded-full flex items-center justify-center"
|
||||||
style={{
|
style={{
|
||||||
background: trader.ai_model.includes('deepseek') ? '#60a5fa' : '#c084fc',
|
background: trader.ai_model.includes('deepseek') ? '#60a5fa' : '#c084fc',
|
||||||
color: '#fff'
|
color: '#fff'
|
||||||
}}>
|
}}>
|
||||||
🤖
|
<Bot className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-bold text-lg" style={{ color: '#EAECEF' }}>
|
<div className="font-bold text-lg" style={{ color: '#EAECEF' }}>
|
||||||
@@ -658,12 +665,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => onTraderSelect?.(trader.trader_id)}
|
onClick={() => 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' }}
|
style={{ background: 'rgba(99, 102, 241, 0.1)', color: '#6366F1' }}
|
||||||
>
|
>
|
||||||
📊 查看
|
<BarChart3 className="w-4 h-4" />
|
||||||
|
查看
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEditTrader(trader.trader_id)}
|
onClick={() => handleEditTrader(trader.trader_id)}
|
||||||
disabled={trader.is_running}
|
disabled={trader.is_running}
|
||||||
@@ -679,20 +687,20 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleToggleTrader(trader.trader_id, trader.is_running || false)}
|
onClick={() => handleToggleTrader(trader.trader_id, trader.is_running || false)}
|
||||||
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"
|
||||||
style={trader.is_running
|
style={trader.is_running
|
||||||
? { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
? { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
||||||
: { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
|
: { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{trader.is_running ? t('stop', language) : t('start', language)}
|
{trader.is_running ? t('stop', language) : t('start', language)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteTrader(trader.trader_id)}
|
onClick={() => handleDeleteTrader(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"
|
||||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
||||||
>
|
>
|
||||||
🗑️
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -701,7 +709,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
||||||
<div className="text-6xl mb-4 opacity-50">🤖</div>
|
<Bot className="w-24 h-24 mx-auto mb-4 opacity-50" />
|
||||||
<div className="text-lg font-semibold mb-2">{t('noTraders', language)}</div>
|
<div className="text-lg font-semibold mb-2">{t('noTraders', language)}</div>
|
||||||
<div className="text-sm mb-4">{t('createFirstTrader', language)}</div>
|
<div className="text-sm mb-4">{t('createFirstTrader', language)}</div>
|
||||||
{(configuredModels.length === 0 || configuredExchanges.length === 0) && (
|
{(configuredModels.length === 0 || configuredExchanges.length === 0) && (
|
||||||
@@ -946,7 +954,7 @@ function ModelConfigModal({
|
|||||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
||||||
title="删除配置"
|
title="删除配置"
|
||||||
>
|
>
|
||||||
🗑️
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1138,7 +1146,7 @@ function ExchangeConfigModal({
|
|||||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
||||||
title="删除配置"
|
title="删除配置"
|
||||||
>
|
>
|
||||||
🗑️
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import useSWR from 'swr';
|
|||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
import { t } from '../i18n/translations';
|
import { t } from '../i18n/translations';
|
||||||
|
import { AlertTriangle, BarChart3, DollarSign, Percent, TrendingUp as ArrowUp, TrendingDown as ArrowDown } from 'lucide-react'
|
||||||
|
|
||||||
interface EquityPoint {
|
interface EquityPoint {
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
@@ -52,16 +53,26 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="binance-card p-6">
|
<div className='binance-card p-6'>
|
||||||
<div className="flex items-center gap-3 p-4 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', border: '1px solid rgba(246, 70, 93, 0.2)' }}>
|
<div
|
||||||
<div className="text-2xl">⚠️</div>
|
className='flex items-center gap-3 p-4 rounded'
|
||||||
|
style={{
|
||||||
|
background: 'rgba(246, 70, 93, 0.1)',
|
||||||
|
border: '1px solid rgba(246, 70, 93, 0.2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertTriangle className='w-6 h-6' style={{ color: '#F6465D' }} />
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold" style={{ color: '#F6465D' }}>{t('loadingError', language)}</div>
|
<div className='font-semibold' style={{ color: '#F6465D' }}>
|
||||||
<div className="text-sm" style={{ color: '#848E9C' }}>{error.message}</div>
|
{t('loadingError', language)}
|
||||||
|
</div>
|
||||||
|
<div className='text-sm' style={{ color: '#848E9C' }}>
|
||||||
|
{error.message}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤掉无效数据:total_equity为0或小于1的数据点(API失败导致)
|
// 过滤掉无效数据:total_equity为0或小于1的数据点(API失败导致)
|
||||||
@@ -69,15 +80,21 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
|||||||
|
|
||||||
if (!validHistory || validHistory.length === 0) {
|
if (!validHistory || validHistory.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="binance-card p-6">
|
<div className='binance-card p-6'>
|
||||||
<h3 className="text-lg font-semibold mb-6" style={{ color: '#EAECEF' }}>{t('accountEquityCurve', language)}</h3>
|
<h3 className='text-lg font-semibold mb-6' style={{ color: '#EAECEF' }}>
|
||||||
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
{t('accountEquityCurve', language)}
|
||||||
<div className="text-6xl mb-4 opacity-50">📊</div>
|
</h3>
|
||||||
<div className="text-lg font-semibold mb-2">{t('noHistoricalData', language)}</div>
|
<div className='text-center py-16' style={{ color: '#848E9C' }}>
|
||||||
<div className="text-sm">{t('dataWillAppear', language)}</div>
|
<div className='mb-4 flex justify-center opacity-50'>
|
||||||
|
<BarChart3 className='w-16 h-16' />
|
||||||
|
</div>
|
||||||
|
<div className='text-lg font-semibold mb-2'>
|
||||||
|
{t('noHistoricalData', language)}
|
||||||
|
</div>
|
||||||
|
<div className='text-sm'>{t('dataWillAppear', language)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 限制显示最近的数据点(性能优化)
|
// 限制显示最近的数据点(性能优化)
|
||||||
@@ -161,142 +178,238 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="binance-card p-3 sm:p-5 animate-fade-in">
|
<div className='binance-card p-3 sm:p-5 animate-fade-in'>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-4">
|
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-4'>
|
||||||
<div className="flex-1">
|
<div className='flex-1'>
|
||||||
<h3 className="text-base sm:text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>{t('accountEquityCurve', language)}</h3>
|
<h3
|
||||||
<div className="flex flex-col sm:flex-row sm:items-baseline gap-2 sm:gap-4">
|
className='text-base sm:text-lg font-bold mb-2'
|
||||||
<span className="text-2xl sm:text-3xl font-bold mono" style={{ color: '#EAECEF' }}>
|
style={{ color: '#EAECEF' }}
|
||||||
|
>
|
||||||
|
{t('accountEquityCurve', language)}
|
||||||
|
</h3>
|
||||||
|
<div className='flex flex-col sm:flex-row sm:items-baseline gap-2 sm:gap-4'>
|
||||||
|
<span
|
||||||
|
className='text-2xl sm:text-3xl font-bold mono'
|
||||||
|
style={{ color: '#EAECEF' }}
|
||||||
|
>
|
||||||
{account?.total_equity.toFixed(2) || '0.00'}
|
{account?.total_equity.toFixed(2) || '0.00'}
|
||||||
<span className="text-base sm:text-lg ml-1" style={{ color: '#848E9C' }}>USDT</span>
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<span
|
<span
|
||||||
className="text-sm sm:text-lg font-bold mono px-2 sm:px-3 py-1 rounded"
|
className='text-base sm:text-lg ml-1'
|
||||||
|
style={{ color: '#848E9C' }}
|
||||||
|
>
|
||||||
|
USDT
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div className='flex items-center gap-2 flex-wrap'>
|
||||||
|
<span
|
||||||
|
className='text-sm sm:text-lg font-bold mono px-2 sm:px-3 py-1 rounded flex items-center gap-1'
|
||||||
style={{
|
style={{
|
||||||
color: isProfit ? '#0ECB81' : '#F6465D',
|
color: isProfit ? '#0ECB81' : '#F6465D',
|
||||||
background: isProfit ? 'rgba(14, 203, 129, 0.1)' : 'rgba(246, 70, 93, 0.1)',
|
background: isProfit
|
||||||
border: `1px solid ${isProfit ? 'rgba(14, 203, 129, 0.2)' : 'rgba(246, 70, 93, 0.2)'}`
|
? 'rgba(14, 203, 129, 0.1)'
|
||||||
|
: 'rgba(246, 70, 93, 0.1)',
|
||||||
|
border: `1px solid ${
|
||||||
|
isProfit
|
||||||
|
? 'rgba(14, 203, 129, 0.2)'
|
||||||
|
: 'rgba(246, 70, 93, 0.2)'
|
||||||
|
}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isProfit ? '▲' : '▼'} {isProfit ? '+' : ''}
|
{isProfit ? <ArrowUp className="w-4 h-4" /> : <ArrowDown className="w-4 h-4" />}
|
||||||
|
{isProfit ? '+' : ''}
|
||||||
{currentValue.raw_pnl_pct}%
|
{currentValue.raw_pnl_pct}%
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs sm:text-sm mono" style={{ color: '#848E9C' }}>
|
<span
|
||||||
({isProfit ? '+' : ''}{currentValue.raw_pnl.toFixed(2)} USDT)
|
className='text-xs sm:text-sm mono'
|
||||||
|
style={{ color: '#848E9C' }}
|
||||||
|
>
|
||||||
|
({isProfit ? '+' : ''}
|
||||||
|
{currentValue.raw_pnl.toFixed(2)} USDT)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Display Mode Toggle */}
|
{/* Display Mode Toggle */}
|
||||||
<div className="flex gap-0.5 sm:gap-1 rounded p-0.5 sm:p-1 self-start sm:self-auto" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
<div
|
||||||
|
className='flex gap-0.5 sm:gap-1 rounded p-0.5 sm:p-1 self-start sm:self-auto'
|
||||||
|
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setDisplayMode('dollar')}
|
onClick={() => setDisplayMode('dollar')}
|
||||||
className="px-3 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-bold transition-all"
|
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'
|
style={
|
||||||
? { background: '#F0B90B', color: '#000', boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)' }
|
displayMode === 'dollar'
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
? {
|
||||||
|
background: '#F0B90B',
|
||||||
|
color: '#000',
|
||||||
|
boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)',
|
||||||
|
}
|
||||||
|
: { background: 'transparent', color: '#848E9C' }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
💵 USDT
|
<DollarSign className='w-4 h-4' /> USDT
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setDisplayMode('percent')}
|
onClick={() => setDisplayMode('percent')}
|
||||||
className="px-3 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-bold transition-all"
|
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'
|
style={
|
||||||
? { background: '#F0B90B', color: '#000', boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)' }
|
displayMode === 'percent'
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
? {
|
||||||
|
background: '#F0B90B',
|
||||||
|
color: '#000',
|
||||||
|
boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)',
|
||||||
|
}
|
||||||
|
: { background: 'transparent', color: '#848E9C' }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
📊 %
|
<Percent className='w-4 h-4' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chart */}
|
{/* Chart */}
|
||||||
<div className="my-2" style={{ borderRadius: '8px', overflow: 'hidden' }}>
|
<div className='my-2' style={{ borderRadius: '8px', overflow: 'hidden' }}>
|
||||||
<ResponsiveContainer width="100%" height={280}>
|
<ResponsiveContainer width='100%' height={280}>
|
||||||
<LineChart data={chartData} margin={{ top: 10, right: 20, left: 5, bottom: 30 }}>
|
<LineChart
|
||||||
<defs>
|
data={chartData}
|
||||||
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
margin={{ top: 10, right: 20, left: 5, bottom: 30 }}
|
||||||
<stop offset="5%" stopColor="#F0B90B" stopOpacity={0.8} />
|
>
|
||||||
<stop offset="95%" stopColor="#FCD535" stopOpacity={0.2} />
|
<defs>
|
||||||
</linearGradient>
|
<linearGradient id='colorGradient' x1='0' y1='0' x2='0' y2='1'>
|
||||||
</defs>
|
<stop offset='5%' stopColor='#F0B90B' stopOpacity={0.8} />
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#2B3139" />
|
<stop offset='95%' stopColor='#FCD535' stopOpacity={0.2} />
|
||||||
<XAxis
|
</linearGradient>
|
||||||
dataKey="time"
|
</defs>
|
||||||
stroke="#5E6673"
|
<CartesianGrid strokeDasharray='3 3' stroke='#2B3139' />
|
||||||
tick={{ fill: '#848E9C', fontSize: 11 }}
|
<XAxis
|
||||||
tickLine={{ stroke: '#2B3139' }}
|
dataKey='time'
|
||||||
interval={Math.floor(chartData.length / 10)}
|
stroke='#5E6673'
|
||||||
angle={-15}
|
tick={{ fill: '#848E9C', fontSize: 11 }}
|
||||||
textAnchor="end"
|
tickLine={{ stroke: '#2B3139' }}
|
||||||
height={60}
|
interval={Math.floor(chartData.length / 10)}
|
||||||
/>
|
angle={-15}
|
||||||
<YAxis
|
textAnchor='end'
|
||||||
stroke="#5E6673"
|
height={60}
|
||||||
tick={{ fill: '#848E9C', fontSize: 12 }}
|
/>
|
||||||
tickLine={{ stroke: '#2B3139' }}
|
<YAxis
|
||||||
domain={calculateYDomain()}
|
stroke='#5E6673'
|
||||||
tickFormatter={(value) =>
|
tick={{ fill: '#848E9C', fontSize: 12 }}
|
||||||
displayMode === 'dollar' ? `$${value.toFixed(0)}` : `${value}%`
|
tickLine={{ stroke: '#2B3139' }}
|
||||||
}
|
domain={calculateYDomain()}
|
||||||
/>
|
tickFormatter={(value) =>
|
||||||
<Tooltip content={<CustomTooltip />} />
|
displayMode === 'dollar' ? `$${value.toFixed(0)}` : `${value}%`
|
||||||
<ReferenceLine
|
}
|
||||||
y={displayMode === 'dollar' ? initialBalance : 0}
|
/>
|
||||||
stroke="#474D57"
|
<Tooltip content={<CustomTooltip />} />
|
||||||
strokeDasharray="3 3"
|
<ReferenceLine
|
||||||
label={{
|
y={displayMode === 'dollar' ? initialBalance : 0}
|
||||||
value: displayMode === 'dollar' ? t('initialBalance', language).split(' ')[0] : '0%',
|
stroke='#474D57'
|
||||||
fill: '#848E9C',
|
strokeDasharray='3 3'
|
||||||
fontSize: 12,
|
label={{
|
||||||
}}
|
value:
|
||||||
/>
|
displayMode === 'dollar'
|
||||||
<Line
|
? t('initialBalance', language).split(' ')[0]
|
||||||
type="natural"
|
: '0%',
|
||||||
dataKey="value"
|
fill: '#848E9C',
|
||||||
stroke="url(#colorGradient)"
|
fontSize: 12,
|
||||||
strokeWidth={3}
|
}}
|
||||||
dot={chartData.length > 50 ? false : { fill: '#F0B90B', r: 3 }}
|
/>
|
||||||
activeDot={{ r: 6, fill: '#FCD535', stroke: '#F0B90B', strokeWidth: 2 }}
|
<Line
|
||||||
connectNulls={true}
|
type='natural'
|
||||||
/>
|
dataKey='value'
|
||||||
</LineChart>
|
stroke='url(#colorGradient)'
|
||||||
</ResponsiveContainer>
|
strokeWidth={3}
|
||||||
|
dot={chartData.length > 50 ? false : { fill: '#F0B90B', r: 3 }}
|
||||||
|
activeDot={{
|
||||||
|
r: 6,
|
||||||
|
fill: '#FCD535',
|
||||||
|
stroke: '#F0B90B',
|
||||||
|
strokeWidth: 2,
|
||||||
|
}}
|
||||||
|
connectNulls={true}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Stats */}
|
{/* Footer Stats */}
|
||||||
<div className="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3 pt-3" style={{ borderTop: '1px solid #2B3139' }}>
|
<div
|
||||||
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
className='mt-3 grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3 pt-3'
|
||||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('initialBalance', language)}</div>
|
style={{ borderTop: '1px solid #2B3139' }}
|
||||||
<div className="text-xs sm:text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
>
|
||||||
|
<div
|
||||||
|
className='p-2 rounded transition-all hover:bg-opacity-50'
|
||||||
|
style={{ background: 'rgba(240, 185, 11, 0.05)' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='text-xs mb-1 uppercase tracking-wider'
|
||||||
|
style={{ color: '#848E9C' }}
|
||||||
|
>
|
||||||
|
{t('initialBalance', language)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='text-xs sm:text-sm font-bold mono'
|
||||||
|
style={{ color: '#EAECEF' }}
|
||||||
|
>
|
||||||
{initialBalance.toFixed(2)} USDT
|
{initialBalance.toFixed(2)} USDT
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
<div
|
||||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('currentEquity', language)}</div>
|
className='p-2 rounded transition-all hover:bg-opacity-50'
|
||||||
<div className="text-xs sm:text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
style={{ background: 'rgba(240, 185, 11, 0.05)' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='text-xs mb-1 uppercase tracking-wider'
|
||||||
|
style={{ color: '#848E9C' }}
|
||||||
|
>
|
||||||
|
{t('currentEquity', language)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='text-xs sm:text-sm font-bold mono'
|
||||||
|
style={{ color: '#EAECEF' }}
|
||||||
|
>
|
||||||
{currentValue.raw_equity.toFixed(2)} USDT
|
{currentValue.raw_equity.toFixed(2)} USDT
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
<div
|
||||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('historicalCycles', language)}</div>
|
className='p-2 rounded transition-all hover:bg-opacity-50'
|
||||||
<div className="text-xs sm:text-sm font-bold mono" style={{ color: '#EAECEF' }}>{validHistory.length} {t('cycles', language)}</div>
|
style={{ background: 'rgba(240, 185, 11, 0.05)' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='text-xs mb-1 uppercase tracking-wider'
|
||||||
|
style={{ color: '#848E9C' }}
|
||||||
|
>
|
||||||
|
{t('historicalCycles', language)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='text-xs sm:text-sm font-bold mono'
|
||||||
|
style={{ color: '#EAECEF' }}
|
||||||
|
>
|
||||||
|
{validHistory.length} {t('cycles', language)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
<div
|
||||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('displayRange', language)}</div>
|
className='p-2 rounded transition-all hover:bg-opacity-50'
|
||||||
<div className="text-xs sm:text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
style={{ background: 'rgba(240, 185, 11, 0.05)' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='text-xs mb-1 uppercase tracking-wider'
|
||||||
|
style={{ color: '#848E9C' }}
|
||||||
|
>
|
||||||
|
{t('displayRange', language)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='text-xs sm:text-sm font-bold mono'
|
||||||
|
style={{ color: '#EAECEF' }}
|
||||||
|
>
|
||||||
{validHistory.length > MAX_DISPLAY_POINTS
|
{validHistory.length > MAX_DISPLAY_POINTS
|
||||||
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
|
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
|
||||||
: t('allData', language)
|
: t('allData', language)}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user