diff --git a/README.md b/README.md index 8bd3d5e1..8bbceb52 100644 --- a/README.md +++ b/README.md @@ -470,9 +470,11 @@ Open your browser and visit: **🌐 http://localhost:3000** { "id": "hyperliquid_trader", "name": "My Hyperliquid Trader", + "enabled": true, "ai_model": "deepseek", "exchange": "hyperliquid", "hyperliquid_private_key": "your_private_key_without_0x", + "hyperliquid_wallet_addr": "your_ethereum_address", "hyperliquid_testnet": false, "deepseek_key": "sk-xxxxxxxxxxxxx", "initial_balance": 1000.0, @@ -522,13 +524,14 @@ Open your browser and visit: **🌐 http://localhost:3000** { "id": "aster_deepseek", "name": "Aster DeepSeek Trader", + "enabled": true, "ai_model": "deepseek", "exchange": "aster", - + "aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e", "aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0", "aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1", - + "deepseek_key": "sk-xxxxxxxxxxxxx", "initial_balance": 1000.0, "scan_interval_minutes": 3 @@ -610,9 +613,14 @@ For running multiple AI traders competing against each other: |-------|-------------|---------------|-----------| | `id` | Unique identifier for this trader | `"my_trader"` | ✅ Yes | | `name` | Display name | `"My AI Trader"` | ✅ Yes | -| `ai_model` | AI provider to use | `"deepseek"` or `"qwen"` | ✅ Yes | -| `binance_api_key` | Binance API key | `"abc123..."` | ✅ Yes | -| `binance_secret_key` | Binance Secret key | `"xyz789..."` | ✅ Yes | +| `enabled` | Whether this trader is enabled
Set to `false` to skip startup | `true` or `false` | ✅ Yes | +| `ai_model` | AI provider to use | `"deepseek"` or `"qwen"` or `"custom"` | ✅ Yes | +| `exchange` | Exchange to use | `"binance"` or `"hyperliquid"` or `"aster"` | ✅ Yes | +| `binance_api_key` | Binance API key | `"abc123..."` | Required when using Binance | +| `binance_secret_key` | Binance Secret key | `"xyz789..."` | Required when using Binance | +| `hyperliquid_private_key` | Hyperliquid private key
⚠️ Remove `0x` prefix | `"your_key..."` | Required when using Hyperliquid | +| `hyperliquid_wallet_addr` | Hyperliquid wallet address | `"0xabc..."` | Required when using Hyperliquid | +| `hyperliquid_testnet` | Use testnet | `true` or `false` | ❌ No (defaults to false) | | `use_qwen` | Whether to use Qwen | `true` or `false` | ✅ Yes | | `deepseek_key` | DeepSeek API key | `"sk-xxx"` | If using DeepSeek | | `qwen_key` | Qwen API key | `"sk-xxx"` | If using Qwen | diff --git a/README.ru.md b/README.ru.md index 51d8054a..4761e106 100644 --- a/README.ru.md +++ b/README.ru.md @@ -398,9 +398,11 @@ cp config.json.example config.json { "id": "hyperliquid_trader", "name": "My Hyperliquid Trader", + "enabled": true, "ai_model": "deepseek", "exchange": "hyperliquid", "hyperliquid_private_key": "your_private_key_without_0x", + "hyperliquid_wallet_addr": "your_ethereum_address", "hyperliquid_testnet": false, "deepseek_key": "sk-xxxxxxxxxxxxx", "initial_balance": 1000.0, @@ -450,13 +452,14 @@ cp config.json.example config.json { "id": "aster_deepseek", "name": "Aster DeepSeek Trader", + "enabled": true, "ai_model": "deepseek", "exchange": "aster", - + "aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e", "aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0", "aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1", - + "deepseek_key": "sk-xxxxxxxxxxxxx", "initial_balance": 1000.0, "scan_interval_minutes": 3 @@ -536,9 +539,14 @@ cp config.json.example config.json |------|----------|-----------------|--------------| | `id` | Уникальный идентификатор для этого трейдера | `"my_trader"` | ✅ Да | | `name` | Отображаемое имя | `"Мой AI Трейдер"` | ✅ Да | -| `ai_model` | Используемый AI провайдер | `"deepseek"` или `"qwen"` | ✅ Да | -| `binance_api_key` | Binance API ключ | `"abc123..."` | ✅ Да | -| `binance_secret_key` | Binance Secret ключ | `"xyz789..."` | ✅ Да | +| `enabled` | Включен ли этот трейдер
Установите в `false` для пропуска запуска | `true` или `false` | ✅ Да | +| `ai_model` | Используемый AI провайдер | `"deepseek"` или `"qwen"` или `"custom"` | ✅ Да | +| `exchange` | Используемая биржа | `"binance"` или `"hyperliquid"` или `"aster"` | ✅ Да | +| `binance_api_key` | Binance API ключ | `"abc123..."` | Требуется при использовании Binance | +| `binance_secret_key` | Binance Secret ключ | `"xyz789..."` | Требуется при использовании Binance | +| `hyperliquid_private_key` | Hyperliquid приватный ключ
⚠️ Удалите префикс `0x` | `"your_key..."` | Требуется при использовании Hyperliquid | +| `hyperliquid_wallet_addr` | Hyperliquid адрес кошелька | `"0xabc..."` | Требуется при использовании Hyperliquid | +| `hyperliquid_testnet` | Использовать тестнет | `true` или `false` | ❌ Нет (по умолчанию false) | | `use_qwen` | Использовать ли Qwen | `true` или `false` | ✅ Да | | `deepseek_key` | DeepSeek API ключ | `"sk-xxx"` | Требуется при использовании DeepSeek | | `qwen_key` | Qwen API ключ | `"sk-xxx"` | Требуется при использовании Qwen | diff --git a/README.uk.md b/README.uk.md index d47c5921..55ef2584 100644 --- a/README.uk.md +++ b/README.uk.md @@ -398,9 +398,11 @@ cp config.json.example config.json { "id": "hyperliquid_trader", "name": "My Hyperliquid Trader", + "enabled": true, "ai_model": "deepseek", "exchange": "hyperliquid", "hyperliquid_private_key": "your_private_key_without_0x", + "hyperliquid_wallet_addr": "your_ethereum_address", "hyperliquid_testnet": false, "deepseek_key": "sk-xxxxxxxxxxxxx", "initial_balance": 1000.0, @@ -450,13 +452,14 @@ cp config.json.example config.json { "id": "aster_deepseek", "name": "Aster DeepSeek Trader", + "enabled": true, "ai_model": "deepseek", "exchange": "aster", - + "aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e", "aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0", "aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1", - + "deepseek_key": "sk-xxxxxxxxxxxxx", "initial_balance": 1000.0, "scan_interval_minutes": 3 @@ -536,9 +539,14 @@ cp config.json.example config.json |------|------|------------------|--------------| | `id` | Унікальний ідентифікатор для цього трейдера | `"my_trader"` | ✅ Так | | `name` | Відображуване ім'я | `"Мій AI Трейдер"` | ✅ Так | -| `ai_model` | Використовуваний AI провайдер | `"deepseek"` або `"qwen"` | ✅ Так | -| `binance_api_key` | Binance API ключ | `"abc123..."` | ✅ Так | -| `binance_secret_key` | Binance Secret ключ | `"xyz789..."` | ✅ Так | +| `enabled` | Чи увімкнений цей трейдер
Встановіть в `false` для пропуску запуску | `true` або `false` | ✅ Так | +| `ai_model` | Використовуваний AI провайдер | `"deepseek"` або `"qwen"` або `"custom"` | ✅ Так | +| `exchange` | Використовувана біржа | `"binance"` або `"hyperliquid"` або `"aster"` | ✅ Так | +| `binance_api_key` | Binance API ключ | `"abc123..."` | Потрібно при використанні Binance | +| `binance_secret_key` | Binance Secret ключ | `"xyz789..."` | Потрібно при використанні Binance | +| `hyperliquid_private_key` | Hyperliquid приватний ключ
⚠️ Видаліть префікс `0x` | `"your_key..."` | Потрібно при використанні Hyperliquid | +| `hyperliquid_wallet_addr` | Hyperliquid адреса гаманця | `"0xabc..."` | Потрібно при використанні Hyperliquid | +| `hyperliquid_testnet` | Використовувати тестнет | `true` або `false` | ❌ Ні (за замовчуванням false) | | `use_qwen` | Використовувати чи Qwen | `true` або `false` | ✅ Так | | `deepseek_key` | DeepSeek API ключ | `"sk-xxx"` | Потрібно при використанні DeepSeek | | `qwen_key` | Qwen API ключ | `"sk-xxx"` | Потрібно при використанні Qwen | diff --git a/README.zh-CN.md b/README.zh-CN.md index 9b47fa04..dbdb2df9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -461,9 +461,11 @@ cp config.json.example config.json { "id": "hyperliquid_trader", "name": "My Hyperliquid Trader", + "enabled": true, "ai_model": "deepseek", "exchange": "hyperliquid", "hyperliquid_private_key": "your_private_key_without_0x", + "hyperliquid_wallet_addr": "your_ethereum_address", "hyperliquid_testnet": false, "deepseek_key": "sk-xxxxxxxxxxxxx", "initial_balance": 1000.0, @@ -513,13 +515,14 @@ cp config.json.example config.json { "id": "aster_deepseek", "name": "Aster DeepSeek Trader", + "enabled": true, "ai_model": "deepseek", "exchange": "aster", - + "aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e", "aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0", "aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1", - + "deepseek_key": "sk-xxxxxxxxxxxxx", "initial_balance": 1000.0, "scan_interval_minutes": 3 @@ -599,9 +602,14 @@ cp config.json.example config.json |-----|------|--------|-----------| | `id` | 此trader的唯一标识符 | `"my_trader"` | ✅ 是 | | `name` | 显示名称 | `"我的AI交易员"` | ✅ 是 | -| `ai_model` | 使用的AI提供商 | `"deepseek"` 或 `"qwen"` | ✅ 是 | -| `binance_api_key` | 币安API密钥 | `"abc123..."` | ✅ 是 | -| `binance_secret_key` | 币安Secret密钥 | `"xyz789..."` | ✅ 是 | +| `enabled` | 是否启用此trader
设为`false`可跳过启动 | `true` 或 `false` | ✅ 是 | +| `ai_model` | 使用的AI提供商 | `"deepseek"` 或 `"qwen"` 或 `"custom"` | ✅ 是 | +| `exchange` | 使用的交易所 | `"binance"` 或 `"hyperliquid"` 或 `"aster"` | ✅ 是 | +| `binance_api_key` | 币安API密钥 | `"abc123..."` | 使用Binance时必填 | +| `binance_secret_key` | 币安Secret密钥 | `"xyz789..."` | 使用Binance时必填 | +| `hyperliquid_private_key` | Hyperliquid私钥
⚠️ 去掉`0x`前缀 | `"your_key..."` | 使用Hyperliquid时必填 | +| `hyperliquid_wallet_addr` | Hyperliquid钱包地址 | `"0xabc..."` | 使用Hyperliquid时必填 | +| `hyperliquid_testnet` | 是否使用测试网 | `true` 或 `false` | ❌ 否(默认false) | | `use_qwen` | 是否使用Qwen | `true` 或 `false` | ✅ 是 | | `deepseek_key` | DeepSeek API密钥 | `"sk-xxx"` | 使用DeepSeek时必填 | | `qwen_key` | Qwen API密钥 | `"sk-xxx"` | 使用Qwen时必填 | diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..75298663 --- /dev/null +++ b/config/config.go @@ -0,0 +1,201 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + "time" +) + +// TraderConfig 单个trader的配置 +type TraderConfig struct { + ID string `json:"id"` + Name string `json:"name"` + Enabled bool `json:"enabled"` // 是否启用该trader + AIModel string `json:"ai_model"` // "qwen" or "deepseek" + + // 交易平台选择(二选一) + Exchange string `json:"exchange"` // "binance" or "hyperliquid" + + // 币安配置 + BinanceAPIKey string `json:"binance_api_key,omitempty"` + BinanceSecretKey string `json:"binance_secret_key,omitempty"` + + // Hyperliquid配置 + HyperliquidPrivateKey string `json:"hyperliquid_private_key,omitempty"` + HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr,omitempty"` + HyperliquidTestnet bool `json:"hyperliquid_testnet,omitempty"` + + // Aster配置 + AsterUser string `json:"aster_user,omitempty"` // Aster主钱包地址 + AsterSigner string `json:"aster_signer,omitempty"` // Aster API钱包地址 + AsterPrivateKey string `json:"aster_private_key,omitempty"` // Aster API钱包私钥 + + // AI配置 + QwenKey string `json:"qwen_key,omitempty"` + DeepSeekKey string `json:"deepseek_key,omitempty"` + + // 自定义AI API配置(支持任何OpenAI格式的API) + CustomAPIURL string `json:"custom_api_url,omitempty"` + CustomAPIKey string `json:"custom_api_key,omitempty"` + CustomModelName string `json:"custom_model_name,omitempty"` + + InitialBalance float64 `json:"initial_balance"` + ScanIntervalMinutes int `json:"scan_interval_minutes"` +} + +// LeverageConfig 杠杆配置 +type LeverageConfig struct { + BTCETHLeverage int `json:"btc_eth_leverage"` // BTC和ETH的杠杆倍数(主账户建议5-50,子账户≤5) + AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币的杠杆倍数(主账户建议5-20,子账户≤5) +} + +// Config 总配置 +type Config struct { + Traders []TraderConfig `json:"traders"` + UseDefaultCoins bool `json:"use_default_coins"` // 是否使用默认主流币种列表 + DefaultCoins []string `json:"default_coins"` // 默认主流币种池 + CoinPoolAPIURL string `json:"coin_pool_api_url"` + OITopAPIURL string `json:"oi_top_api_url"` + APIServerPort int `json:"api_server_port"` + MaxDailyLoss float64 `json:"max_daily_loss"` + MaxDrawdown float64 `json:"max_drawdown"` + StopTradingMinutes int `json:"stop_trading_minutes"` + Leverage LeverageConfig `json:"leverage"` // 杠杆配置 +} + +// LoadConfig 从文件加载配置 +func LoadConfig(filename string) (*Config, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("读取配置文件失败: %w", err) + } + + var config Config + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("解析配置文件失败: %w", err) + } + + // 设置默认值:如果use_default_coins未设置(为false)且没有配置coin_pool_api_url,则默认使用默认币种列表 + if !config.UseDefaultCoins && config.CoinPoolAPIURL == "" { + config.UseDefaultCoins = true + } + + // 设置默认币种池 + if len(config.DefaultCoins) == 0 { + config.DefaultCoins = []string{ + "BTCUSDT", + "ETHUSDT", + "SOLUSDT", + "BNBUSDT", + "XRPUSDT", + "DOGEUSDT", + "ADAUSDT", + "HYPEUSDT", + } + } + + // 验证配置 + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("配置验证失败: %w", err) + } + + return &config, nil +} + +// Validate 验证配置有效性 +func (c *Config) Validate() error { + if len(c.Traders) == 0 { + return fmt.Errorf("至少需要配置一个trader") + } + + traderIDs := make(map[string]bool) + for i, trader := range c.Traders { + if trader.ID == "" { + return fmt.Errorf("trader[%d]: ID不能为空", i) + } + if traderIDs[trader.ID] { + return fmt.Errorf("trader[%d]: ID '%s' 重复", i, trader.ID) + } + traderIDs[trader.ID] = true + + if trader.Name == "" { + return fmt.Errorf("trader[%d]: Name不能为空", i) + } + if trader.AIModel != "qwen" && trader.AIModel != "deepseek" && trader.AIModel != "custom" { + return fmt.Errorf("trader[%d]: ai_model必须是 'qwen', 'deepseek' 或 'custom'", i) + } + + // 验证交易平台配置 + if trader.Exchange == "" { + trader.Exchange = "binance" // 默认使用币安 + } + if trader.Exchange != "binance" && trader.Exchange != "hyperliquid" && trader.Exchange != "aster" { + return fmt.Errorf("trader[%d]: exchange必须是 'binance', 'hyperliquid' 或 'aster'", i) + } + + // 根据平台验证对应的密钥 + if trader.Exchange == "binance" { + if trader.BinanceAPIKey == "" || trader.BinanceSecretKey == "" { + return fmt.Errorf("trader[%d]: 使用币安时必须配置binance_api_key和binance_secret_key", i) + } + } else if trader.Exchange == "hyperliquid" { + if trader.HyperliquidPrivateKey == "" { + return fmt.Errorf("trader[%d]: 使用Hyperliquid时必须配置hyperliquid_private_key", i) + } + } else if trader.Exchange == "aster" { + if trader.AsterUser == "" || trader.AsterSigner == "" || trader.AsterPrivateKey == "" { + return fmt.Errorf("trader[%d]: 使用Aster时必须配置aster_user, aster_signer和aster_private_key", i) + } + } + + if trader.AIModel == "qwen" && trader.QwenKey == "" { + return fmt.Errorf("trader[%d]: 使用Qwen时必须配置qwen_key", i) + } + if trader.AIModel == "deepseek" && trader.DeepSeekKey == "" { + return fmt.Errorf("trader[%d]: 使用DeepSeek时必须配置deepseek_key", i) + } + if trader.AIModel == "custom" { + if trader.CustomAPIURL == "" { + return fmt.Errorf("trader[%d]: 使用自定义API时必须配置custom_api_url", i) + } + if trader.CustomAPIKey == "" { + return fmt.Errorf("trader[%d]: 使用自定义API时必须配置custom_api_key", i) + } + if trader.CustomModelName == "" { + return fmt.Errorf("trader[%d]: 使用自定义API时必须配置custom_model_name", i) + } + } + if trader.InitialBalance <= 0 { + return fmt.Errorf("trader[%d]: initial_balance必须大于0", i) + } + if trader.ScanIntervalMinutes <= 0 { + trader.ScanIntervalMinutes = 3 // 默认3分钟 + } + } + + if c.APIServerPort <= 0 { + c.APIServerPort = 8080 // 默认8080端口 + } + + // 设置杠杆默认值(适配币安子账户限制,最大5倍) + if c.Leverage.BTCETHLeverage <= 0 { + c.Leverage.BTCETHLeverage = 5 // 默认5倍(安全值,适配子账户) + } + if c.Leverage.BTCETHLeverage > 5 { + fmt.Printf("⚠️ 警告: BTC/ETH杠杆设置为%dx,如果使用子账户可能会失败(子账户限制≤5x)\n", c.Leverage.BTCETHLeverage) + } + if c.Leverage.AltcoinLeverage <= 0 { + c.Leverage.AltcoinLeverage = 5 // 默认5倍(安全值,适配子账户) + } + if c.Leverage.AltcoinLeverage > 5 { + fmt.Printf("⚠️ 警告: 山寨币杠杆设置为%dx,如果使用子账户可能会失败(子账户限制≤5x)\n", c.Leverage.AltcoinLeverage) + } + + return nil +} + +// GetScanInterval 获取扫描间隔 +func (tc *TraderConfig) GetScanInterval() time.Duration { + return time.Duration(tc.ScanIntervalMinutes) * time.Minute +} diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index b3364eb2..fdd646e0 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -2,6 +2,7 @@ package trader import ( "context" + "encoding/json" "fmt" "log" "strconv" @@ -83,9 +84,13 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { // 解析余额信息(MarginSummary字段都是string) result := make(map[string]interface{}) - accountValue, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64) - totalMarginUsed, _ := strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64) - availableBalance, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64) + // 🔍 调试:打印API返回的完整CrossMarginSummary结构 + summaryJSON, _ := json.MarshalIndent(accountState.MarginSummary, " ", " ") + log.Printf("🔍 [DEBUG] Hyperliquid API CrossMarginSummary完整数据:") + log.Printf("%s", string(summaryJSON)) + + accountValue, _ := strconv.ParseFloat(accountState.MarginSummary.AccountValue, 64) + totalMarginUsed, _ := strconv.ParseFloat(accountState.MarginSummary.TotalMarginUsed, 64) // ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏 totalUnrealizedPnl := 0.0 @@ -95,19 +100,23 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { } // ✅ 正确理解Hyperliquid字段: - // AccountValue = 账户净值(包含未实现盈亏)= 这是真正的总资产 - // 钱包余额(已实现)= AccountValue - 未实现盈亏 - walletBalance := accountValue - totalUnrealizedPnl + // AccountValue = 总账户净值(已包含空闲资金+持仓价值+未实现盈亏) + // TotalMarginUsed = 持仓占用的保证金(已包含在AccountValue中,仅用于显示) + // + // 为了兼容auto_trader.go的计算逻辑(totalEquity = totalWalletBalance + totalUnrealizedProfit) + // 需要返回"不包含未实现盈亏的钱包余额" + walletBalanceWithoutUnrealized := accountValue - totalUnrealizedPnl - result["totalWalletBalance"] = walletBalance // 钱包余额(已实现部分) - result["availableBalance"] = availableBalance - totalMarginUsed // 可用余额 - result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏 + result["totalWalletBalance"] = walletBalanceWithoutUnrealized // 钱包余额(不含未实现盈亏) + result["availableBalance"] = accountValue - totalMarginUsed // 可用余额(总净值 - 占用保证金) + result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏 - log.Printf("✓ Hyperliquid API返回: 账户净值=%.2f, 钱包余额=%.2f, 可用=%.2f, 未实现盈亏=%.2f", + log.Printf("✓ Hyperliquid 账户: 总净值=%.2f (钱包%.2f+未实现%.2f), 可用=%.2f, 保证金占用=%.2f", accountValue, - result["totalWalletBalance"], + walletBalanceWithoutUnrealized, + totalUnrealizedPnl, result["availableBalance"], - result["totalUnrealizedProfit"]) + totalMarginUsed) return result, nil } diff --git a/web/src/App.tsx b/web/src/App.tsx index 050baf26..42a04b8f 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -47,10 +47,38 @@ function App() { const { user, token, logout, isLoading } = useAuth(); const { config: systemConfig, loading: configLoading } = useSystemConfig(); const [route, setRoute] = useState(window.location.pathname); - const [currentPage, setCurrentPage] = useState('competition'); + + // 从URL hash读取初始页面状态(支持刷新保持页面) + const getInitialPage = (): Page => { + const hash = window.location.hash.slice(1); // 去掉 # + return hash === 'trader' || hash === 'details' ? 'trader' : 'competition'; + }; + + const [currentPage, setCurrentPage] = useState(getInitialPage()); const [selectedTraderId, setSelectedTraderId] = useState(); const [lastUpdate, setLastUpdate] = useState('--:--:--'); + // 监听URL hash变化,同步页面状态 + useEffect(() => { + const handleHashChange = () => { + const hash = window.location.hash.slice(1); + if (hash === 'trader' || hash === 'details') { + setCurrentPage('trader'); + } else if (hash === 'competition' || hash === '') { + setCurrentPage('competition'); + } + }; + + window.addEventListener('hashchange', handleHashChange); + return () => window.removeEventListener('hashchange', handleHashChange); + }, []); + + // 切换页面时更新URL hash + const navigateToPage = (page: Page) => { + setCurrentPage(page); + window.location.hash = page === 'competition' ? '' : 'trader'; + }; + // 获取trader列表 const { data: traders } = useSWR('traders', api.getTraders, { refreshInterval: 10000, @@ -274,25 +302,6 @@ function App() { 退出 )} - - {/* Status Indicator (only show on trader page) */} - {currentPage === 'trader' && status && ( -
-
- - {t(status.is_running ? 'running' : 'stopped', language)} - -
- )}
diff --git a/web/src/components/ComparisonChart.tsx b/web/src/components/ComparisonChart.tsx index 906eeb9d..b333c04f 100644 --- a/web/src/components/ComparisonChart.tsx +++ b/web/src/components/ComparisonChart.tsx @@ -13,6 +13,7 @@ import { import useSWR from 'swr'; import { api } from '../lib/api'; import type { CompetitionTraderData } from '../types'; +import { getTraderColor } from '../utils/traderColors'; interface ComparisonChartProps { traders: CompetitionTraderData[]; @@ -171,15 +172,8 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { ]; }; - // Trader颜色配置 - 使用更鲜艳对比度更高的颜色 - const getTraderColor = (traderId: string) => { - const trader = traders.find((t) => t.trader_id === traderId); - if (trader?.ai_model === 'qwen') { - return '#c084fc'; // purple-400 (更亮) - } else { - return '#60a5fa'; // blue-400 (更亮) - } - }; + // 使用统一的颜色分配逻辑(与Leaderboard保持一致) + const traderColor = (traderId: string) => getTraderColor(traders, traderId); // 自定义Tooltip - Binance Style const CustomTooltip = ({ active, payload }: any) => { @@ -199,7 +193,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{trader.trader_name}
@@ -240,8 +234,8 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { x2="0" y2="1" > - - + + ))} @@ -288,10 +282,10 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { key={trader.trader_id} type="monotone" dataKey={`${trader.trader_id}_pnl_pct`} - stroke={getTraderColor(trader.trader_id)} + stroke={traderColor(trader.trader_id)} strokeWidth={3} - dot={displayData.length < 50 ? { fill: getTraderColor(trader.trader_id), r: 3 } : false} - activeDot={{ r: 6, fill: getTraderColor(trader.trader_id), stroke: '#fff', strokeWidth: 2 }} + dot={displayData.length < 50 ? { fill: traderColor(trader.trader_id), r: 3 } : false} + activeDot={{ r: 6, fill: traderColor(trader.trader_id), stroke: '#fff', strokeWidth: 2 }} name={trader.trader_name} connectNulls /> diff --git a/web/src/components/CompetitionPage.tsx b/web/src/components/CompetitionPage.tsx index 0bd700e7..2a638b83 100644 --- a/web/src/components/CompetitionPage.tsx +++ b/web/src/components/CompetitionPage.tsx @@ -2,6 +2,7 @@ import useSWR from 'swr'; import { api } from '../lib/api'; import type { CompetitionData } from '../types'; import { ComparisonChart } from './ComparisonChart'; +import { getTraderColor } from '../utils/traderColors'; export function CompetitionPage() { const { data: competition } = useSWR( @@ -105,7 +106,7 @@ export function CompetitionPage() {
{sortedTraders.map((trader, index) => { const isLeader = index === 0; - const aiModelColor = trader.ai_model === 'qwen' ? '#c084fc' : '#60a5fa'; + const traderColor = getTraderColor(sortedTraders, trader.trader_id); return (
{trader.trader_name}
-
+
{trader.ai_model.toUpperCase()}
@@ -220,7 +221,7 @@ export function CompetitionPage() {
{trader.trader_name}
diff --git a/web/src/utils/traderColors.ts b/web/src/utils/traderColors.ts new file mode 100644 index 00000000..fb884ded --- /dev/null +++ b/web/src/utils/traderColors.ts @@ -0,0 +1,31 @@ +// Trader颜色配置 - 统一的颜色分配逻辑 +// 用于 ComparisonChart 和 Leaderboard,确保颜色一致性 + +export const TRADER_COLORS = [ + '#60a5fa', // blue-400 + '#c084fc', // purple-400 + '#34d399', // emerald-400 + '#fb923c', // orange-400 + '#f472b6', // pink-400 + '#fbbf24', // amber-400 + '#38bdf8', // sky-400 + '#a78bfa', // violet-400 + '#4ade80', // green-400 + '#fb7185', // rose-400 +]; + +/** + * 根据trader的索引位置获取颜色 + * @param traders - trader列表 + * @param traderId - 当前trader的ID + * @returns 对应的颜色值 + */ +export function getTraderColor( + traders: Array<{ trader_id: string }>, + traderId: string +): string { + const traderIndex = traders.findIndex((t) => t.trader_id === traderId); + if (traderIndex === -1) return TRADER_COLORS[0]; // 默认返回第一个颜色 + // 如果超出颜色池大小,循环使用 + return TRADER_COLORS[traderIndex % TRADER_COLORS.length]; +}