mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-06 20:41:14 +08:00
Merge branch 'main' into dev
Integrate latest changes from main branch including: - Documentation updates for trader configuration - Color system improvements for UI components - Logo design files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
18
README.md
18
README.md
@@ -465,9 +465,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,
|
||||
@@ -517,13 +519,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
|
||||
@@ -605,9 +608,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<br>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<br>⚠️ 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 |
|
||||
|
||||
18
README.ru.md
18
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` | Включен ли этот трейдер<br>Установите в `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 приватный ключ<br>⚠️ Удалите префикс `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 |
|
||||
|
||||
18
README.uk.md
18
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` | Чи увімкнений цей трейдер<br>Встановіть в `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 приватний ключ<br>⚠️ Видаліть префікс `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 |
|
||||
|
||||
@@ -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<br>设为`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私钥<br>⚠️ 去掉`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时必填 |
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{
|
||||
"id": "hyperliquid_deepseek",
|
||||
"name": "Hyperliquid DeepSeek Trader",
|
||||
"enabled": true,
|
||||
"ai_model": "deepseek",
|
||||
"exchange": "hyperliquid",
|
||||
"hyperliquid_private_key": "your_ethereum_private_key_without_0x_prefix",
|
||||
@@ -15,6 +16,7 @@
|
||||
{
|
||||
"id": "binance_qwen",
|
||||
"name": "Binance Qwen Trader",
|
||||
"enabled": true,
|
||||
"ai_model": "qwen",
|
||||
"exchange": "binance",
|
||||
"binance_api_key": "your_binance_api_key",
|
||||
@@ -26,6 +28,7 @@
|
||||
{
|
||||
"id": "binance_custom",
|
||||
"name": "Binance Custom API Trader",
|
||||
"enabled": false,
|
||||
"ai_model": "custom",
|
||||
"exchange": "binance",
|
||||
"binance_api_key": "your_binance_api_key",
|
||||
@@ -35,10 +38,11 @@
|
||||
"custom_model_name": "gpt-4o",
|
||||
"initial_balance": 1000,
|
||||
"scan_interval_minutes": 3
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "aster_deepseek",
|
||||
"name": "Aster DeepSeek Trader",
|
||||
"enabled": true,
|
||||
"ai_model": "deepseek",
|
||||
"exchange": "aster",
|
||||
|
||||
@@ -47,7 +51,7 @@
|
||||
// signer: API钱包地址 (点击生成地址后生成的地址)
|
||||
// privateKey: API钱包私钥 (生成地址对应的私钥)
|
||||
|
||||
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
|
||||
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
|
||||
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
|
||||
"aster_private_key": "your_aster_api_wallet_private_key_without_0x_prefix",
|
||||
|
||||
@@ -69,7 +73,7 @@
|
||||
"XRPUSDT",
|
||||
"DOGEUSDT",
|
||||
"ADAUSDT",
|
||||
"HYPEUSDT",
|
||||
"HYPEUSDT"
|
||||
],
|
||||
"coin_pool_api_url": "",
|
||||
"oi_top_api_url": "",
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
type TraderConfig struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"` // 是否启用该trader
|
||||
AIModel string `json:"ai_model"` // "qwen" or "deepseek"
|
||||
|
||||
// 交易平台选择(二选一)
|
||||
|
||||
19
main.go
19
main.go
@@ -56,8 +56,16 @@ func main() {
|
||||
// 创建TraderManager
|
||||
traderManager := manager.NewTraderManager()
|
||||
|
||||
// 添加所有trader
|
||||
// 添加所有启用的trader
|
||||
enabledCount := 0
|
||||
for i, traderCfg := range cfg.Traders {
|
||||
// 跳过未启用的trader
|
||||
if !traderCfg.Enabled {
|
||||
log.Printf("⏭️ [%d/%d] 跳过未启用的 %s", i+1, len(cfg.Traders), traderCfg.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
enabledCount++
|
||||
log.Printf("📦 [%d/%d] 初始化 %s (%s模型)...",
|
||||
i+1, len(cfg.Traders), traderCfg.Name, strings.ToUpper(traderCfg.AIModel))
|
||||
|
||||
@@ -74,9 +82,18 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否至少有一个启用的trader
|
||||
if enabledCount == 0 {
|
||||
log.Fatalf("❌ 没有启用的trader,请在config.json中设置至少一个trader的enabled=true")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("🏁 竞赛参赛者:")
|
||||
for _, traderCfg := range cfg.Traders {
|
||||
// 只显示启用的trader
|
||||
if !traderCfg.Enabled {
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" • %s (%s) - 初始资金: %.0f USDT\n",
|
||||
traderCfg.Name, strings.ToUpper(traderCfg.AIModel), traderCfg.InitialBalance)
|
||||
}
|
||||
|
||||
@@ -427,14 +427,6 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
||||
unrealizedPnl := pos["unRealizedProfit"].(float64)
|
||||
liquidationPrice := pos["liquidationPrice"].(float64)
|
||||
|
||||
// 计算盈亏百分比
|
||||
pnlPct := 0.0
|
||||
if side == "long" {
|
||||
pnlPct = ((markPrice - entryPrice) / entryPrice) * 100
|
||||
} else {
|
||||
pnlPct = ((entryPrice - markPrice) / entryPrice) * 100
|
||||
}
|
||||
|
||||
// 计算占用保证金(估算)
|
||||
leverage := 10 // 默认值,实际应该从持仓信息获取
|
||||
if lev, ok := pos["leverage"].(float64); ok {
|
||||
@@ -443,6 +435,14 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
||||
marginUsed := (quantity * markPrice) / float64(leverage)
|
||||
totalMarginUsed += marginUsed
|
||||
|
||||
// 计算盈亏百分比
|
||||
pnlPct := 0.0
|
||||
if side == "long" {
|
||||
pnlPct = ((markPrice - entryPrice) / entryPrice) * float64(leverage) * 100
|
||||
} else {
|
||||
pnlPct = ((entryPrice - markPrice) / entryPrice) * float64(leverage) * 100
|
||||
}
|
||||
|
||||
// 跟踪持仓首次出现时间
|
||||
posKey := symbol + "_" + side
|
||||
currentPositionKeys[posKey] = true
|
||||
@@ -873,9 +873,9 @@ func (at *AutoTrader) GetPositions() ([]map[string]interface{}, error) {
|
||||
|
||||
pnlPct := 0.0
|
||||
if side == "long" {
|
||||
pnlPct = ((markPrice - entryPrice) / entryPrice) * 100
|
||||
pnlPct = ((markPrice - entryPrice) / entryPrice) * float64(leverage) * 100
|
||||
} else {
|
||||
pnlPct = ((entryPrice - markPrice) / entryPrice) * 100
|
||||
pnlPct = ((entryPrice - markPrice) / entryPrice) * float64(leverage) * 100
|
||||
}
|
||||
|
||||
marginUsed := (quantity * markPrice) / float64(leverage)
|
||||
|
||||
@@ -2,6 +2,7 @@ package trader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
@@ -83,8 +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)
|
||||
// 🔍 调试:打印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
|
||||
@@ -94,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"] = accountValue - 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
|
||||
}
|
||||
|
||||
@@ -19,10 +19,38 @@ type Page = 'competition' | 'trader';
|
||||
|
||||
function App() {
|
||||
const { language, setLanguage } = useLanguage();
|
||||
const [currentPage, setCurrentPage] = useState<Page>('competition');
|
||||
|
||||
// 从URL hash读取初始页面状态(支持刷新保持页面)
|
||||
const getInitialPage = (): Page => {
|
||||
const hash = window.location.hash.slice(1); // 去掉 #
|
||||
return hash === 'trader' || hash === 'details' ? 'trader' : 'competition';
|
||||
};
|
||||
|
||||
const [currentPage, setCurrentPage] = useState<Page>(getInitialPage());
|
||||
const [selectedTraderId, setSelectedTraderId] = useState<string | undefined>();
|
||||
const [lastUpdate, setLastUpdate] = useState<string>('--:--:--');
|
||||
|
||||
// 监听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<TraderInfo[]>('traders', api.getTraders, {
|
||||
refreshInterval: 10000,
|
||||
@@ -180,7 +208,7 @@ function App() {
|
||||
{/* Page Toggle */}
|
||||
<div className="flex gap-0.5 sm:gap-1 rounded p-0.5 sm:p-1" style={{ background: '#1E2329' }}>
|
||||
<button
|
||||
onClick={() => setCurrentPage('competition')}
|
||||
onClick={() => navigateToPage('competition')}
|
||||
className="px-2 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-semibold transition-all"
|
||||
style={currentPage === 'competition'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
@@ -190,7 +218,7 @@ function App() {
|
||||
{t('competition', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('trader')}
|
||||
onClick={() => navigateToPage('trader')}
|
||||
className="px-2 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-semibold transition-all"
|
||||
style={currentPage === 'trader'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
|
||||
@@ -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[];
|
||||
@@ -177,15 +178,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) => {
|
||||
@@ -205,7 +199,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
||||
<div key={trader.trader_id} className="mb-1.5 last:mb-0">
|
||||
<div
|
||||
className="text-xs font-semibold mb-0.5"
|
||||
style={{ color: getTraderColor(trader.trader_id) }}
|
||||
style={{ color: traderColor(trader.trader_id) }}
|
||||
>
|
||||
{trader.trader_name}
|
||||
</div>
|
||||
@@ -246,8 +240,8 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="5%" stopColor={getTraderColor(trader.trader_id)} stopOpacity={0.9} />
|
||||
<stop offset="95%" stopColor={getTraderColor(trader.trader_id)} stopOpacity={0.2} />
|
||||
<stop offset="5%" stopColor={traderColor(trader.trader_id)} stopOpacity={0.9} />
|
||||
<stop offset="95%" stopColor={traderColor(trader.trader_id)} stopOpacity={0.2} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
@@ -294,10 +288,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
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { CompetitionData } from '../types';
|
||||
import { ComparisonChart } from './ComparisonChart';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
import { getTraderColor } from '../utils/traderColors';
|
||||
|
||||
export function CompetitionPage() {
|
||||
const { language } = useLanguage();
|
||||
@@ -108,7 +109,7 @@ export function CompetitionPage() {
|
||||
<div className="space-y-2">
|
||||
{sortedTraders.map((trader, index) => {
|
||||
const isLeader = index === 0;
|
||||
const aiModelColor = trader.ai_model === 'qwen' ? '#c084fc' : '#60a5fa';
|
||||
const traderColor = getTraderColor(sortedTraders, trader.trader_id);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -128,7 +129,7 @@ export function CompetitionPage() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm" style={{ color: '#EAECEF' }}>{trader.trader_name}</div>
|
||||
<div className="text-xs mono font-semibold" style={{ color: aiModelColor }}>
|
||||
<div className="text-xs mono font-semibold" style={{ color: traderColor }}>
|
||||
{trader.ai_model.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,7 +224,7 @@ export function CompetitionPage() {
|
||||
<div className="text-center">
|
||||
<div
|
||||
className="text-base font-bold mb-2"
|
||||
style={{ color: trader.ai_model === 'qwen' ? '#c084fc' : '#60a5fa' }}
|
||||
style={{ color: getTraderColor(sortedTraders, trader.trader_id) }}
|
||||
>
|
||||
{trader.trader_name}
|
||||
</div>
|
||||
|
||||
31
web/src/utils/traderColors.ts
Normal file
31
web/src/utils/traderColors.ts
Normal file
@@ -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];
|
||||
}
|
||||
Reference in New Issue
Block a user