mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 09:11:03 +08:00
Merge branch 'main' of github.com:Icyoung/nofx
# Conflicts: # config.json.example # config/config.go # main.go # trader/auto_trader.go # web/src/App.tsx # web/src/components/CompetitionPage.tsx
This commit is contained in:
14
README.md
14
README.md
@@ -470,9 +470,11 @@ Open your browser and visit: **🌐 http://localhost:3000**
|
|||||||
{
|
{
|
||||||
"id": "hyperliquid_trader",
|
"id": "hyperliquid_trader",
|
||||||
"name": "My Hyperliquid Trader",
|
"name": "My Hyperliquid Trader",
|
||||||
|
"enabled": true,
|
||||||
"ai_model": "deepseek",
|
"ai_model": "deepseek",
|
||||||
"exchange": "hyperliquid",
|
"exchange": "hyperliquid",
|
||||||
"hyperliquid_private_key": "your_private_key_without_0x",
|
"hyperliquid_private_key": "your_private_key_without_0x",
|
||||||
|
"hyperliquid_wallet_addr": "your_ethereum_address",
|
||||||
"hyperliquid_testnet": false,
|
"hyperliquid_testnet": false,
|
||||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||||
"initial_balance": 1000.0,
|
"initial_balance": 1000.0,
|
||||||
@@ -522,6 +524,7 @@ Open your browser and visit: **🌐 http://localhost:3000**
|
|||||||
{
|
{
|
||||||
"id": "aster_deepseek",
|
"id": "aster_deepseek",
|
||||||
"name": "Aster DeepSeek Trader",
|
"name": "Aster DeepSeek Trader",
|
||||||
|
"enabled": true,
|
||||||
"ai_model": "deepseek",
|
"ai_model": "deepseek",
|
||||||
"exchange": "aster",
|
"exchange": "aster",
|
||||||
|
|
||||||
@@ -610,9 +613,14 @@ For running multiple AI traders competing against each other:
|
|||||||
|-------|-------------|---------------|-----------|
|
|-------|-------------|---------------|-----------|
|
||||||
| `id` | Unique identifier for this trader | `"my_trader"` | ✅ Yes |
|
| `id` | Unique identifier for this trader | `"my_trader"` | ✅ Yes |
|
||||||
| `name` | Display name | `"My AI Trader"` | ✅ Yes |
|
| `name` | Display name | `"My AI Trader"` | ✅ Yes |
|
||||||
| `ai_model` | AI provider to use | `"deepseek"` or `"qwen"` | ✅ Yes |
|
| `enabled` | Whether this trader is enabled<br>Set to `false` to skip startup | `true` or `false` | ✅ Yes |
|
||||||
| `binance_api_key` | Binance API key | `"abc123..."` | ✅ Yes |
|
| `ai_model` | AI provider to use | `"deepseek"` or `"qwen"` or `"custom"` | ✅ Yes |
|
||||||
| `binance_secret_key` | Binance Secret key | `"xyz789..."` | ✅ 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 |
|
| `use_qwen` | Whether to use Qwen | `true` or `false` | ✅ Yes |
|
||||||
| `deepseek_key` | DeepSeek API key | `"sk-xxx"` | If using DeepSeek |
|
| `deepseek_key` | DeepSeek API key | `"sk-xxx"` | If using DeepSeek |
|
||||||
| `qwen_key` | Qwen API key | `"sk-xxx"` | If using Qwen |
|
| `qwen_key` | Qwen API key | `"sk-xxx"` | If using Qwen |
|
||||||
|
|||||||
14
README.ru.md
14
README.ru.md
@@ -398,9 +398,11 @@ cp config.json.example config.json
|
|||||||
{
|
{
|
||||||
"id": "hyperliquid_trader",
|
"id": "hyperliquid_trader",
|
||||||
"name": "My Hyperliquid Trader",
|
"name": "My Hyperliquid Trader",
|
||||||
|
"enabled": true,
|
||||||
"ai_model": "deepseek",
|
"ai_model": "deepseek",
|
||||||
"exchange": "hyperliquid",
|
"exchange": "hyperliquid",
|
||||||
"hyperliquid_private_key": "your_private_key_without_0x",
|
"hyperliquid_private_key": "your_private_key_without_0x",
|
||||||
|
"hyperliquid_wallet_addr": "your_ethereum_address",
|
||||||
"hyperliquid_testnet": false,
|
"hyperliquid_testnet": false,
|
||||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||||
"initial_balance": 1000.0,
|
"initial_balance": 1000.0,
|
||||||
@@ -450,6 +452,7 @@ cp config.json.example config.json
|
|||||||
{
|
{
|
||||||
"id": "aster_deepseek",
|
"id": "aster_deepseek",
|
||||||
"name": "Aster DeepSeek Trader",
|
"name": "Aster DeepSeek Trader",
|
||||||
|
"enabled": true,
|
||||||
"ai_model": "deepseek",
|
"ai_model": "deepseek",
|
||||||
"exchange": "aster",
|
"exchange": "aster",
|
||||||
|
|
||||||
@@ -536,9 +539,14 @@ cp config.json.example config.json
|
|||||||
|------|----------|-----------------|--------------|
|
|------|----------|-----------------|--------------|
|
||||||
| `id` | Уникальный идентификатор для этого трейдера | `"my_trader"` | ✅ Да |
|
| `id` | Уникальный идентификатор для этого трейдера | `"my_trader"` | ✅ Да |
|
||||||
| `name` | Отображаемое имя | `"Мой AI Трейдер"` | ✅ Да |
|
| `name` | Отображаемое имя | `"Мой AI Трейдер"` | ✅ Да |
|
||||||
| `ai_model` | Используемый AI провайдер | `"deepseek"` или `"qwen"` | ✅ Да |
|
| `enabled` | Включен ли этот трейдер<br>Установите в `false` для пропуска запуска | `true` или `false` | ✅ Да |
|
||||||
| `binance_api_key` | Binance API ключ | `"abc123..."` | ✅ Да |
|
| `ai_model` | Используемый AI провайдер | `"deepseek"` или `"qwen"` или `"custom"` | ✅ Да |
|
||||||
| `binance_secret_key` | Binance Secret ключ | `"xyz789..."` | ✅ Да |
|
| `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` | ✅ Да |
|
| `use_qwen` | Использовать ли Qwen | `true` или `false` | ✅ Да |
|
||||||
| `deepseek_key` | DeepSeek API ключ | `"sk-xxx"` | Требуется при использовании DeepSeek |
|
| `deepseek_key` | DeepSeek API ключ | `"sk-xxx"` | Требуется при использовании DeepSeek |
|
||||||
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Требуется при использовании Qwen |
|
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Требуется при использовании Qwen |
|
||||||
|
|||||||
14
README.uk.md
14
README.uk.md
@@ -398,9 +398,11 @@ cp config.json.example config.json
|
|||||||
{
|
{
|
||||||
"id": "hyperliquid_trader",
|
"id": "hyperliquid_trader",
|
||||||
"name": "My Hyperliquid Trader",
|
"name": "My Hyperliquid Trader",
|
||||||
|
"enabled": true,
|
||||||
"ai_model": "deepseek",
|
"ai_model": "deepseek",
|
||||||
"exchange": "hyperliquid",
|
"exchange": "hyperliquid",
|
||||||
"hyperliquid_private_key": "your_private_key_without_0x",
|
"hyperliquid_private_key": "your_private_key_without_0x",
|
||||||
|
"hyperliquid_wallet_addr": "your_ethereum_address",
|
||||||
"hyperliquid_testnet": false,
|
"hyperliquid_testnet": false,
|
||||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||||
"initial_balance": 1000.0,
|
"initial_balance": 1000.0,
|
||||||
@@ -450,6 +452,7 @@ cp config.json.example config.json
|
|||||||
{
|
{
|
||||||
"id": "aster_deepseek",
|
"id": "aster_deepseek",
|
||||||
"name": "Aster DeepSeek Trader",
|
"name": "Aster DeepSeek Trader",
|
||||||
|
"enabled": true,
|
||||||
"ai_model": "deepseek",
|
"ai_model": "deepseek",
|
||||||
"exchange": "aster",
|
"exchange": "aster",
|
||||||
|
|
||||||
@@ -536,9 +539,14 @@ cp config.json.example config.json
|
|||||||
|------|------|------------------|--------------|
|
|------|------|------------------|--------------|
|
||||||
| `id` | Унікальний ідентифікатор для цього трейдера | `"my_trader"` | ✅ Так |
|
| `id` | Унікальний ідентифікатор для цього трейдера | `"my_trader"` | ✅ Так |
|
||||||
| `name` | Відображуване ім'я | `"Мій AI Трейдер"` | ✅ Так |
|
| `name` | Відображуване ім'я | `"Мій AI Трейдер"` | ✅ Так |
|
||||||
| `ai_model` | Використовуваний AI провайдер | `"deepseek"` або `"qwen"` | ✅ Так |
|
| `enabled` | Чи увімкнений цей трейдер<br>Встановіть в `false` для пропуску запуску | `true` або `false` | ✅ Так |
|
||||||
| `binance_api_key` | Binance API ключ | `"abc123..."` | ✅ Так |
|
| `ai_model` | Використовуваний AI провайдер | `"deepseek"` або `"qwen"` або `"custom"` | ✅ Так |
|
||||||
| `binance_secret_key` | Binance Secret ключ | `"xyz789..."` | ✅ Так |
|
| `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` | ✅ Так |
|
| `use_qwen` | Використовувати чи Qwen | `true` або `false` | ✅ Так |
|
||||||
| `deepseek_key` | DeepSeek API ключ | `"sk-xxx"` | Потрібно при використанні DeepSeek |
|
| `deepseek_key` | DeepSeek API ключ | `"sk-xxx"` | Потрібно при використанні DeepSeek |
|
||||||
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Потрібно при використанні Qwen |
|
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Потрібно при використанні Qwen |
|
||||||
|
|||||||
@@ -461,9 +461,11 @@ cp config.json.example config.json
|
|||||||
{
|
{
|
||||||
"id": "hyperliquid_trader",
|
"id": "hyperliquid_trader",
|
||||||
"name": "My Hyperliquid Trader",
|
"name": "My Hyperliquid Trader",
|
||||||
|
"enabled": true,
|
||||||
"ai_model": "deepseek",
|
"ai_model": "deepseek",
|
||||||
"exchange": "hyperliquid",
|
"exchange": "hyperliquid",
|
||||||
"hyperliquid_private_key": "your_private_key_without_0x",
|
"hyperliquid_private_key": "your_private_key_without_0x",
|
||||||
|
"hyperliquid_wallet_addr": "your_ethereum_address",
|
||||||
"hyperliquid_testnet": false,
|
"hyperliquid_testnet": false,
|
||||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||||
"initial_balance": 1000.0,
|
"initial_balance": 1000.0,
|
||||||
@@ -513,6 +515,7 @@ cp config.json.example config.json
|
|||||||
{
|
{
|
||||||
"id": "aster_deepseek",
|
"id": "aster_deepseek",
|
||||||
"name": "Aster DeepSeek Trader",
|
"name": "Aster DeepSeek Trader",
|
||||||
|
"enabled": true,
|
||||||
"ai_model": "deepseek",
|
"ai_model": "deepseek",
|
||||||
"exchange": "aster",
|
"exchange": "aster",
|
||||||
|
|
||||||
@@ -599,9 +602,14 @@ cp config.json.example config.json
|
|||||||
|-----|------|--------|-----------|
|
|-----|------|--------|-----------|
|
||||||
| `id` | 此trader的唯一标识符 | `"my_trader"` | ✅ 是 |
|
| `id` | 此trader的唯一标识符 | `"my_trader"` | ✅ 是 |
|
||||||
| `name` | 显示名称 | `"我的AI交易员"` | ✅ 是 |
|
| `name` | 显示名称 | `"我的AI交易员"` | ✅ 是 |
|
||||||
| `ai_model` | 使用的AI提供商 | `"deepseek"` 或 `"qwen"` | ✅ 是 |
|
| `enabled` | 是否启用此trader<br>设为`false`可跳过启动 | `true` 或 `false` | ✅ 是 |
|
||||||
| `binance_api_key` | 币安API密钥 | `"abc123..."` | ✅ 是 |
|
| `ai_model` | 使用的AI提供商 | `"deepseek"` 或 `"qwen"` 或 `"custom"` | ✅ 是 |
|
||||||
| `binance_secret_key` | 币安Secret密钥 | `"xyz789..."` | ✅ 是 |
|
| `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` | ✅ 是 |
|
| `use_qwen` | 是否使用Qwen | `true` 或 `false` | ✅ 是 |
|
||||||
| `deepseek_key` | DeepSeek API密钥 | `"sk-xxx"` | 使用DeepSeek时必填 |
|
| `deepseek_key` | DeepSeek API密钥 | `"sk-xxx"` | 使用DeepSeek时必填 |
|
||||||
| `qwen_key` | Qwen API密钥 | `"sk-xxx"` | 使用Qwen时必填 |
|
| `qwen_key` | Qwen API密钥 | `"sk-xxx"` | 使用Qwen时必填 |
|
||||||
|
|||||||
201
config/config.go
Normal file
201
config/config.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package trader
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -83,9 +84,13 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
|||||||
// 解析余额信息(MarginSummary字段都是string)
|
// 解析余额信息(MarginSummary字段都是string)
|
||||||
result := make(map[string]interface{})
|
result := make(map[string]interface{})
|
||||||
|
|
||||||
accountValue, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64)
|
// 🔍 调试:打印API返回的完整CrossMarginSummary结构
|
||||||
totalMarginUsed, _ := strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64)
|
summaryJSON, _ := json.MarshalIndent(accountState.MarginSummary, " ", " ")
|
||||||
availableBalance, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64)
|
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
|
totalUnrealizedPnl := 0.0
|
||||||
@@ -95,19 +100,23 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 正确理解Hyperliquid字段:
|
// ✅ 正确理解Hyperliquid字段:
|
||||||
// AccountValue = 账户净值(包含未实现盈亏)= 这是真正的总资产
|
// AccountValue = 总账户净值(已包含空闲资金+持仓价值+未实现盈亏)
|
||||||
// 钱包余额(已实现)= AccountValue - 未实现盈亏
|
// TotalMarginUsed = 持仓占用的保证金(已包含在AccountValue中,仅用于显示)
|
||||||
walletBalance := accountValue - totalUnrealizedPnl
|
//
|
||||||
|
// 为了兼容auto_trader.go的计算逻辑(totalEquity = totalWalletBalance + totalUnrealizedProfit)
|
||||||
|
// 需要返回"不包含未实现盈亏的钱包余额"
|
||||||
|
walletBalanceWithoutUnrealized := accountValue - totalUnrealizedPnl
|
||||||
|
|
||||||
result["totalWalletBalance"] = walletBalance // 钱包余额(已实现部分)
|
result["totalWalletBalance"] = walletBalanceWithoutUnrealized // 钱包余额(不含未实现盈亏)
|
||||||
result["availableBalance"] = availableBalance - totalMarginUsed // 可用余额
|
result["availableBalance"] = accountValue - totalMarginUsed // 可用余额(总净值 - 占用保证金)
|
||||||
result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏
|
result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏
|
||||||
|
|
||||||
log.Printf("✓ Hyperliquid API返回: 账户净值=%.2f, 钱包余额=%.2f, 可用=%.2f, 未实现盈亏=%.2f",
|
log.Printf("✓ Hyperliquid 账户: 总净值=%.2f (钱包%.2f+未实现%.2f), 可用=%.2f, 保证金占用=%.2f",
|
||||||
accountValue,
|
accountValue,
|
||||||
result["totalWalletBalance"],
|
walletBalanceWithoutUnrealized,
|
||||||
|
totalUnrealizedPnl,
|
||||||
result["availableBalance"],
|
result["availableBalance"],
|
||||||
result["totalUnrealizedProfit"])
|
totalMarginUsed)
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,10 +47,38 @@ function App() {
|
|||||||
const { user, token, logout, isLoading } = useAuth();
|
const { user, token, logout, isLoading } = useAuth();
|
||||||
const { config: systemConfig, loading: configLoading } = useSystemConfig();
|
const { config: systemConfig, loading: configLoading } = useSystemConfig();
|
||||||
const [route, setRoute] = useState(window.location.pathname);
|
const [route, setRoute] = useState(window.location.pathname);
|
||||||
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 [selectedTraderId, setSelectedTraderId] = useState<string | undefined>();
|
||||||
const [lastUpdate, setLastUpdate] = useState<string>('--:--:--');
|
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列表
|
// 获取trader列表
|
||||||
const { data: traders } = useSWR<TraderInfo[]>('traders', api.getTraders, {
|
const { data: traders } = useSWR<TraderInfo[]>('traders', api.getTraders, {
|
||||||
refreshInterval: 10000,
|
refreshInterval: 10000,
|
||||||
@@ -274,25 +302,6 @@ function App() {
|
|||||||
退出
|
退出
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Status Indicator (only show on trader page) */}
|
|
||||||
{currentPage === 'trader' && status && (
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 px-3 py-2 rounded"
|
|
||||||
style={status.is_running
|
|
||||||
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81', border: '1px solid rgba(14, 203, 129, 0.2)' }
|
|
||||||
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`w-2 h-2 rounded-full ${status.is_running ? 'pulse-glow' : ''}`}
|
|
||||||
style={{ background: status.is_running ? '#0ECB81' : '#F6465D' }}
|
|
||||||
/>
|
|
||||||
<span className="font-semibold mono text-xs">
|
|
||||||
{t(status.is_running ? 'running' : 'stopped', language)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { CompetitionTraderData } from '../types';
|
import type { CompetitionTraderData } from '../types';
|
||||||
|
import { getTraderColor } from '../utils/traderColors';
|
||||||
|
|
||||||
interface ComparisonChartProps {
|
interface ComparisonChartProps {
|
||||||
traders: CompetitionTraderData[];
|
traders: CompetitionTraderData[];
|
||||||
@@ -171,15 +172,8 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Trader颜色配置 - 使用更鲜艳对比度更高的颜色
|
// 使用统一的颜色分配逻辑(与Leaderboard保持一致)
|
||||||
const getTraderColor = (traderId: string) => {
|
const traderColor = (traderId: string) => getTraderColor(traders, traderId);
|
||||||
const trader = traders.find((t) => t.trader_id === traderId);
|
|
||||||
if (trader?.ai_model === 'qwen') {
|
|
||||||
return '#c084fc'; // purple-400 (更亮)
|
|
||||||
} else {
|
|
||||||
return '#60a5fa'; // blue-400 (更亮)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 自定义Tooltip - Binance Style
|
// 自定义Tooltip - Binance Style
|
||||||
const CustomTooltip = ({ active, payload }: any) => {
|
const CustomTooltip = ({ active, payload }: any) => {
|
||||||
@@ -199,7 +193,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
|||||||
<div key={trader.trader_id} className="mb-1.5 last:mb-0">
|
<div key={trader.trader_id} className="mb-1.5 last:mb-0">
|
||||||
<div
|
<div
|
||||||
className="text-xs font-semibold mb-0.5"
|
className="text-xs font-semibold mb-0.5"
|
||||||
style={{ color: getTraderColor(trader.trader_id) }}
|
style={{ color: traderColor(trader.trader_id) }}
|
||||||
>
|
>
|
||||||
{trader.trader_name}
|
{trader.trader_name}
|
||||||
</div>
|
</div>
|
||||||
@@ -240,8 +234,8 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
|||||||
x2="0"
|
x2="0"
|
||||||
y2="1"
|
y2="1"
|
||||||
>
|
>
|
||||||
<stop offset="5%" stopColor={getTraderColor(trader.trader_id)} stopOpacity={0.9} />
|
<stop offset="5%" stopColor={traderColor(trader.trader_id)} stopOpacity={0.9} />
|
||||||
<stop offset="95%" stopColor={getTraderColor(trader.trader_id)} stopOpacity={0.2} />
|
<stop offset="95%" stopColor={traderColor(trader.trader_id)} stopOpacity={0.2} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
))}
|
))}
|
||||||
</defs>
|
</defs>
|
||||||
@@ -288,10 +282,10 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
|
|||||||
key={trader.trader_id}
|
key={trader.trader_id}
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey={`${trader.trader_id}_pnl_pct`}
|
dataKey={`${trader.trader_id}_pnl_pct`}
|
||||||
stroke={getTraderColor(trader.trader_id)}
|
stroke={traderColor(trader.trader_id)}
|
||||||
strokeWidth={3}
|
strokeWidth={3}
|
||||||
dot={displayData.length < 50 ? { fill: getTraderColor(trader.trader_id), r: 3 } : false}
|
dot={displayData.length < 50 ? { fill: traderColor(trader.trader_id), r: 3 } : false}
|
||||||
activeDot={{ r: 6, fill: getTraderColor(trader.trader_id), stroke: '#fff', strokeWidth: 2 }}
|
activeDot={{ r: 6, fill: traderColor(trader.trader_id), stroke: '#fff', strokeWidth: 2 }}
|
||||||
name={trader.trader_name}
|
name={trader.trader_name}
|
||||||
connectNulls
|
connectNulls
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import useSWR from 'swr';
|
|||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { CompetitionData } from '../types';
|
import type { CompetitionData } from '../types';
|
||||||
import { ComparisonChart } from './ComparisonChart';
|
import { ComparisonChart } from './ComparisonChart';
|
||||||
|
import { getTraderColor } from '../utils/traderColors';
|
||||||
|
|
||||||
export function CompetitionPage() {
|
export function CompetitionPage() {
|
||||||
const { data: competition } = useSWR<CompetitionData>(
|
const { data: competition } = useSWR<CompetitionData>(
|
||||||
@@ -105,7 +106,7 @@ export function CompetitionPage() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{sortedTraders.map((trader, index) => {
|
{sortedTraders.map((trader, index) => {
|
||||||
const isLeader = index === 0;
|
const isLeader = index === 0;
|
||||||
const aiModelColor = trader.ai_model === 'qwen' ? '#c084fc' : '#60a5fa';
|
const traderColor = getTraderColor(sortedTraders, trader.trader_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -125,7 +126,7 @@ export function CompetitionPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-bold text-sm" style={{ color: '#EAECEF' }}>{trader.trader_name}</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()}
|
{trader.ai_model.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,7 +221,7 @@ export function CompetitionPage() {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div
|
<div
|
||||||
className="text-base font-bold mb-2"
|
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}
|
{trader.trader_name}
|
||||||
</div>
|
</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