Merge pull request #151 from Icyoung/dev

Dev 竞赛fix、交易员新增参数
This commit is contained in:
tinkle-community
2025-11-01 02:42:42 +08:00
committed by GitHub
20 changed files with 2094 additions and 670 deletions

View File

@@ -1,12 +1,14 @@
package api
import (
"encoding/json"
"fmt"
"log"
"net/http"
"nofx/auth"
"nofx/config"
"nofx/manager"
"strconv"
"strings"
"time"
@@ -87,7 +89,9 @@ func (s *Server) setupRoutes() {
{
// AI交易员管理
protected.GET("/traders", s.handleTraderList)
protected.GET("/traders/:id/config", s.handleGetTraderConfig)
protected.POST("/traders", s.handleCreateTrader)
protected.PUT("/traders/:id", s.handleUpdateTrader)
protected.DELETE("/traders/:id", s.handleDeleteTrader)
protected.POST("/traders/:id/start", s.handleStartTrader)
protected.POST("/traders/:id/stop", s.handleStopTrader)
@@ -101,6 +105,10 @@ func (s *Server) setupRoutes() {
protected.GET("/exchanges", s.handleGetExchangeConfigs)
protected.PUT("/exchanges", s.handleUpdateExchangeConfigs)
// 用户信号源配置
protected.GET("/user/signal-sources", s.handleGetUserSignalSource)
protected.POST("/user/signal-sources", s.handleSaveUserSignalSource)
// 竞赛总览
protected.GET("/competition", s.handleCompetition)
@@ -127,8 +135,36 @@ func (s *Server) handleHealth(c *gin.Context) {
// handleGetSystemConfig 获取系统配置(客户端需要知道的配置)
func (s *Server) handleGetSystemConfig(c *gin.Context) {
// 获取默认币种
defaultCoinsStr, _ := s.database.GetSystemConfig("default_coins")
var defaultCoins []string
if defaultCoinsStr != "" {
json.Unmarshal([]byte(defaultCoinsStr), &defaultCoins)
}
if len(defaultCoins) == 0 {
// 使用硬编码的默认币种
defaultCoins = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "HYPEUSDT"}
}
// 获取杠杆配置
btcEthLeverageStr, _ := s.database.GetSystemConfig("btc_eth_leverage")
altcoinLeverageStr, _ := s.database.GetSystemConfig("altcoin_leverage")
btcEthLeverage := 5
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
btcEthLeverage = val
}
altcoinLeverage := 5
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
altcoinLeverage = val
}
c.JSON(http.StatusOK, gin.H{
"admin_mode": auth.IsAdminMode(),
"default_coins": defaultCoins,
"btc_eth_leverage": btcEthLeverage,
"altcoin_leverage": altcoinLeverage,
})
}
@@ -164,21 +200,27 @@ func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, str
// AI交易员管理相关结构体
type CreateTraderRequest struct {
Name string `json:"name" binding:"required"`
AIModelID string `json:"ai_model_id" binding:"required"`
ExchangeID string `json:"exchange_id" binding:"required"`
InitialBalance float64 `json:"initial_balance"`
CustomPrompt string `json:"custom_prompt"`
Name string `json:"name" binding:"required"`
AIModelID string `json:"ai_model_id" binding:"required"`
ExchangeID string `json:"exchange_id" binding:"required"`
InitialBalance float64 `json:"initial_balance"`
BTCETHLeverage int `json:"btc_eth_leverage"`
AltcoinLeverage int `json:"altcoin_leverage"`
TradingSymbols string `json:"trading_symbols"`
CustomPrompt string `json:"custom_prompt"`
OverrideBasePrompt bool `json:"override_base_prompt"`
IsCrossMargin *bool `json:"is_cross_margin"` // 指针类型nil表示使用默认值true
IsCrossMargin *bool `json:"is_cross_margin"` // 指针类型nil表示使用默认值true
UseCoinPool bool `json:"use_coin_pool"`
UseOITop bool `json:"use_oi_top"`
}
type ModelConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey,omitempty"`
CustomAPIURL string `json:"customApiUrl,omitempty"`
}
type ExchangeConfig struct {
@@ -193,8 +235,9 @@ type ExchangeConfig struct {
type UpdateModelConfigRequest struct {
Models map[string]struct {
Enabled bool `json:"enabled"`
APIKey string `json:"api_key"`
Enabled bool `json:"enabled"`
APIKey string `json:"api_key"`
CustomAPIURL string `json:"custom_api_url"`
} `json:"models"`
}
@@ -220,6 +263,28 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
return
}
// 校验杠杆值
if req.BTCETHLeverage < 0 || req.BTCETHLeverage > 50 {
c.JSON(http.StatusBadRequest, gin.H{"error": "BTC/ETH杠杆必须在1-50倍之间"})
return
}
if req.AltcoinLeverage < 0 || req.AltcoinLeverage > 20 {
c.JSON(http.StatusBadRequest, gin.H{"error": "山寨币杠杆必须在1-20倍之间"})
return
}
// 校验交易币种格式
if req.TradingSymbols != "" {
symbols := strings.Split(req.TradingSymbols, ",")
for _, symbol := range symbols {
symbol = strings.TrimSpace(symbol)
if symbol != "" && !strings.HasSuffix(strings.ToUpper(symbol), "USDT") {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("无效的币种格式: %s必须以USDT结尾", symbol)})
return
}
}
}
// 生成交易员ID
traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix())
@@ -229,6 +294,30 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
isCrossMargin = *req.IsCrossMargin
}
// 设置杠杆默认值(从系统配置获取)
btcEthLeverage := 5
altcoinLeverage := 5
if req.BTCETHLeverage > 0 {
btcEthLeverage = req.BTCETHLeverage
} else {
// 从系统配置获取默认值
if btcEthLeverageStr, _ := s.database.GetSystemConfig("btc_eth_leverage"); btcEthLeverageStr != "" {
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
btcEthLeverage = val
}
}
}
if req.AltcoinLeverage > 0 {
altcoinLeverage = req.AltcoinLeverage
} else {
// 从系统配置获取默认值
if altcoinLeverageStr, _ := s.database.GetSystemConfig("altcoin_leverage"); altcoinLeverageStr != "" {
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
altcoinLeverage = val
}
}
}
// 创建交易员配置(数据库实体)
trader := &config.TraderRecord{
ID: traderID,
@@ -237,6 +326,11 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
AIModelID: req.AIModelID,
ExchangeID: req.ExchangeID,
InitialBalance: req.InitialBalance,
BTCETHLeverage: btcEthLeverage,
AltcoinLeverage: altcoinLeverage,
TradingSymbols: req.TradingSymbols,
UseCoinPool: req.UseCoinPool,
UseOITop: req.UseOITop,
CustomPrompt: req.CustomPrompt,
OverrideBasePrompt: req.OverrideBasePrompt,
IsCrossMargin: isCrossMargin,
@@ -268,6 +362,108 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
})
}
// UpdateTraderRequest 更新交易员请求
type UpdateTraderRequest struct {
Name string `json:"name" binding:"required"`
AIModelID string `json:"ai_model_id" binding:"required"`
ExchangeID string `json:"exchange_id" binding:"required"`
InitialBalance float64 `json:"initial_balance"`
BTCETHLeverage int `json:"btc_eth_leverage"`
AltcoinLeverage int `json:"altcoin_leverage"`
TradingSymbols string `json:"trading_symbols"`
CustomPrompt string `json:"custom_prompt"`
OverrideBasePrompt bool `json:"override_base_prompt"`
IsCrossMargin *bool `json:"is_cross_margin"`
}
// handleUpdateTrader 更新交易员配置
func (s *Server) handleUpdateTrader(c *gin.Context) {
userID := c.GetString("user_id")
traderID := c.Param("id")
var req UpdateTraderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 检查交易员是否存在且属于当前用户
traders, err := s.database.GetTraders(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取交易员列表失败"})
return
}
var existingTrader *config.TraderRecord
for _, trader := range traders {
if trader.ID == traderID {
existingTrader = trader
break
}
}
if existingTrader == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
return
}
// 设置默认值
isCrossMargin := existingTrader.IsCrossMargin // 保持原值
if req.IsCrossMargin != nil {
isCrossMargin = *req.IsCrossMargin
}
// 设置杠杆默认值
btcEthLeverage := req.BTCETHLeverage
altcoinLeverage := req.AltcoinLeverage
if btcEthLeverage <= 0 {
btcEthLeverage = existingTrader.BTCETHLeverage // 保持原值
}
if altcoinLeverage <= 0 {
altcoinLeverage = existingTrader.AltcoinLeverage // 保持原值
}
// 更新交易员配置
trader := &config.TraderRecord{
ID: traderID,
UserID: userID,
Name: req.Name,
AIModelID: req.AIModelID,
ExchangeID: req.ExchangeID,
InitialBalance: req.InitialBalance,
BTCETHLeverage: btcEthLeverage,
AltcoinLeverage: altcoinLeverage,
TradingSymbols: req.TradingSymbols,
CustomPrompt: req.CustomPrompt,
OverrideBasePrompt: req.OverrideBasePrompt,
IsCrossMargin: isCrossMargin,
ScanIntervalMinutes: existingTrader.ScanIntervalMinutes, // 保持原值
IsRunning: existingTrader.IsRunning, // 保持原值
}
// 更新数据库
err = s.database.UpdateTrader(trader)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易员失败: %v", err)})
return
}
// 重新加载交易员到内存
err = s.traderManager.LoadUserTraders(s.database, userID)
if err != nil {
log.Printf("⚠️ 重新加载用户交易员到内存失败: %v", err)
}
log.Printf("✓ 更新交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID)
c.JSON(http.StatusOK, gin.H{
"trader_id": traderID,
"trader_name": req.Name,
"ai_model": req.AIModelID,
"message": "交易员更新成功",
})
}
// handleDeleteTrader 删除交易员
func (s *Server) handleDeleteTrader(c *gin.Context) {
userID := c.GetString("user_id")
@@ -419,7 +615,7 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
// 更新每个模型的配置
for modelID, modelData := range req.Models {
err := s.database.UpdateAIModel(userID, modelID, modelData.Enabled, modelData.APIKey)
err := s.database.UpdateAIModel(userID, modelID, modelData.Enabled, modelData.APIKey, modelData.CustomAPIURL)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新模型 %s 失败: %v", modelID, err)})
return
@@ -467,6 +663,48 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"})
}
// handleGetUserSignalSource 获取用户信号源配置
func (s *Server) handleGetUserSignalSource(c *gin.Context) {
userID := c.GetString("user_id")
source, err := s.database.GetUserSignalSource(userID)
if err != nil {
// 如果配置不存在返回空配置而不是404错误
c.JSON(http.StatusOK, gin.H{
"coin_pool_url": "",
"oi_top_url": "",
})
return
}
c.JSON(http.StatusOK, gin.H{
"coin_pool_url": source.CoinPoolURL,
"oi_top_url": source.OITopURL,
})
}
// handleSaveUserSignalSource 保存用户信号源配置
func (s *Server) handleSaveUserSignalSource(c *gin.Context) {
userID := c.GetString("user_id")
var req struct {
CoinPoolURL string `json:"coin_pool_url"`
OITopURL string `json:"oi_top_url"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := s.database.CreateUserSignalSource(userID, req.CoinPoolURL, req.OITopURL)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("保存用户信号源配置失败: %v", err)})
return
}
log.Printf("✓ 用户信号源配置已保存: user=%s, coin_pool=%s, oi_top=%s", userID, req.CoinPoolURL, req.OITopURL)
c.JSON(http.StatusOK, gin.H{"message": "用户信号源配置已保存"})
}
// handleTraderList trader列表
func (s *Server) handleTraderList(c *gin.Context) {
userID := c.GetString("user_id")
@@ -500,6 +738,51 @@ func (s *Server) handleTraderList(c *gin.Context) {
c.JSON(http.StatusOK, result)
}
// handleGetTraderConfig 获取交易员详细配置
func (s *Server) handleGetTraderConfig(c *gin.Context) {
userID := c.GetString("user_id")
traderID := c.Param("id")
if traderID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员ID不能为空"})
return
}
traderConfig, _, _, err := s.database.GetTraderConfig(userID, traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("获取交易员配置失败: %v", err)})
return
}
// 获取实时运行状态
isRunning := traderConfig.IsRunning
if at, err := s.traderManager.GetTrader(traderID); err == nil {
status := at.GetStatus()
if running, ok := status["is_running"].(bool); ok {
isRunning = running
}
}
result := map[string]interface{}{
"trader_id": traderConfig.ID,
"trader_name": traderConfig.Name,
"ai_model": traderConfig.AIModelID,
"exchange_id": traderConfig.ExchangeID,
"initial_balance": traderConfig.InitialBalance,
"btc_eth_leverage": traderConfig.BTCETHLeverage,
"altcoin_leverage": traderConfig.AltcoinLeverage,
"trading_symbols": traderConfig.TradingSymbols,
"custom_prompt": traderConfig.CustomPrompt,
"override_base_prompt": traderConfig.OverrideBasePrompt,
"is_cross_margin": traderConfig.IsCrossMargin,
"use_coin_pool": traderConfig.UseCoinPool,
"use_oi_top": traderConfig.UseOITop,
"is_running": isRunning,
}
c.JSON(http.StatusOK, result)
}
// handleStatus 系统状态
func (s *Server) handleStatus(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
@@ -668,7 +951,7 @@ func (s *Server) handleCompetition(c *gin.Context) {
log.Printf("⚠️ 加载用户 %s 的交易员失败: %v", userID, err)
}
competition, err := s.traderManager.GetCompetitionData(userID)
competition, err := s.traderManager.GetCompetitionData()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取竞赛数据失败: %v", err),

View File

@@ -15,8 +15,6 @@
"ADAUSDT",
"HYPEUSDT"
],
"coin_pool_api_url": "",
"oi_top_api_url": "",
"api_server_port": 8080,
"max_daily_loss": 10.0,
"max_drawdown": 20.0,

View File

@@ -55,8 +55,6 @@ 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"`
@@ -76,8 +74,8 @@ func LoadConfig(filename string) (*Config, error) {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
// 设置默认值:如果use_default_coins未设置为false且没有配置coin_pool_api_url则默认使用默认币种列表
if !config.UseDefaultCoins && config.CoinPoolAPIURL == "" {
// 设置默认值:确保使用默认币种列表
if !config.UseDefaultCoins {
config.UseDefaultCoins = true
}

View File

@@ -72,6 +72,18 @@ func (d *Database) createTables() error {
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
// 用户信号源配置表
`CREATE TABLE IF NOT EXISTS user_signal_sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
coin_pool_url TEXT DEFAULT '',
oi_top_url TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id)
)`,
// 交易员配置表
`CREATE TABLE IF NOT EXISTS traders (
id TEXT PRIMARY KEY,
@@ -82,6 +94,11 @@ func (d *Database) createTables() error {
initial_balance REAL NOT NULL,
scan_interval_minutes INTEGER DEFAULT 3,
is_running BOOLEAN DEFAULT 0,
btc_eth_leverage INTEGER DEFAULT 5,
altcoin_leverage INTEGER DEFAULT 5,
trading_symbols TEXT DEFAULT '',
use_coin_pool BOOLEAN DEFAULT 0,
use_oi_top BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
@@ -132,6 +149,12 @@ func (d *Database) createTables() error {
UPDATE traders SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END`,
`CREATE TRIGGER IF NOT EXISTS update_user_signal_sources_updated_at
AFTER UPDATE ON user_signal_sources
BEGIN
UPDATE user_signal_sources SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END`,
`CREATE TRIGGER IF NOT EXISTS update_system_config_updated_at
AFTER UPDATE ON system_config
BEGIN
@@ -154,6 +177,14 @@ func (d *Database) createTables() error {
`ALTER TABLE traders ADD COLUMN custom_prompt TEXT DEFAULT ''`,
`ALTER TABLE traders ADD COLUMN override_base_prompt BOOLEAN DEFAULT 0`,
`ALTER TABLE traders ADD COLUMN is_cross_margin BOOLEAN DEFAULT 1`, // 默认为全仓模式
`ALTER TABLE traders ADD COLUMN use_default_coins BOOLEAN DEFAULT 1`, // 默认使用默认币种
`ALTER TABLE traders ADD COLUMN custom_coins TEXT DEFAULT ''`, // 自定义币种列表JSON格式
`ALTER TABLE traders ADD COLUMN btc_eth_leverage INTEGER DEFAULT 5`, // BTC/ETH杠杆倍数
`ALTER TABLE traders ADD COLUMN altcoin_leverage INTEGER DEFAULT 5`, // 山寨币杠杆倍数
`ALTER TABLE traders ADD COLUMN trading_symbols TEXT DEFAULT ''`, // 交易币种,逗号分隔
`ALTER TABLE traders ADD COLUMN use_coin_pool BOOLEAN DEFAULT 0`, // 是否使用COIN POOL信号源
`ALTER TABLE traders ADD COLUMN use_oi_top BOOLEAN DEFAULT 0`, // 是否使用OI TOP信号源
`ALTER TABLE ai_models ADD COLUMN custom_api_url TEXT DEFAULT ''`, // 自定义API地址
}
for _, query := range alterQueries {
@@ -215,8 +246,6 @@ func (d *Database) initDefaultData() error {
"api_server_port": "8080", // 默认API端口
"use_default_coins": "true", // 默认使用内置币种列表
"default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表JSON格式
"coin_pool_api_url": "", // 币种池API URL默认为空
"oi_top_api_url": "", // 持仓量API URL默认为空
"max_daily_loss": "10.0", // 最大日损失百分比
"max_drawdown": "20.0", // 最大回撤百分比
"stop_trading_minutes": "60", // 停止交易时间(分钟)
@@ -333,14 +362,15 @@ type User struct {
// AIModelConfig AI模型配置
type AIModelConfig struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Name string `json:"name"`
Provider string `json:"provider"`
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID string `json:"id"`
UserID string `json:"user_id"`
Name string `json:"name"`
Provider string `json:"provider"`
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey"`
CustomAPIURL string `json:"customApiUrl"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ExchangeConfig 交易所配置
@@ -373,13 +403,28 @@ type TraderRecord struct {
InitialBalance float64 `json:"initial_balance"`
ScanIntervalMinutes int `json:"scan_interval_minutes"`
IsRunning bool `json:"is_running"`
CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt
OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt
IsCrossMargin bool `json:"is_cross_margin"` // 是否为全仓模式true=全仓false=逐仓)
BTCETHLeverage int `json:"btc_eth_leverage"` // BTC/ETH杠杆倍数
AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币杠杆倍数
TradingSymbols string `json:"trading_symbols"` // 交易币种,逗号分隔
UseCoinPool bool `json:"use_coin_pool"` // 是否使用COIN POOL信号源
UseOITop bool `json:"use_oi_top"` // 是否使用OI TOP信号源
CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt
OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt
IsCrossMargin bool `json:"is_cross_margin"` // 是否为全仓模式true=全仓false=逐仓)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// UserSignalSource 用户信号源配置
type UserSignalSource struct {
ID int `json:"id"`
UserID string `json:"user_id"`
CoinPoolURL string `json:"coin_pool_url"`
OITopURL string `json:"oi_top_url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// GenerateOTPSecret 生成OTP密钥
func GenerateOTPSecret() (string, error) {
secret := make([]byte, 20)
@@ -457,6 +502,25 @@ func (d *Database) GetUserByID(userID string) (*User, error) {
return &user, nil
}
// GetAllUsers 获取所有用户ID列表
func (d *Database) GetAllUsers() ([]string, error) {
rows, err := d.db.Query(`SELECT id FROM users ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var userIDs []string
for rows.Next() {
var userID string
if err := rows.Scan(&userID); err != nil {
return nil, err
}
userIDs = append(userIDs, userID)
}
return userIDs, nil
}
// UpdateUserOTPVerified 更新用户OTP验证状态
func (d *Database) UpdateUserOTPVerified(userID string, verified bool) error {
_, err := d.db.Exec(`UPDATE users SET otp_verified = ? WHERE id = ?`, verified, userID)
@@ -466,7 +530,7 @@ func (d *Database) UpdateUserOTPVerified(userID string, verified bool) error {
// GetAIModels 获取用户的AI模型配置
func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
rows, err := d.db.Query(`
SELECT id, user_id, name, provider, enabled, api_key, created_at, updated_at
SELECT id, user_id, name, provider, enabled, api_key, COALESCE(custom_api_url, '') as custom_api_url, created_at, updated_at
FROM ai_models WHERE user_id = ? ORDER BY id
`, userID)
if err != nil {
@@ -480,7 +544,7 @@ func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
var model AIModelConfig
err := rows.Scan(
&model.ID, &model.UserID, &model.Name, &model.Provider,
&model.Enabled, &model.APIKey,
&model.Enabled, &model.APIKey, &model.CustomAPIURL,
&model.CreatedAt, &model.UpdatedAt,
)
if err != nil {
@@ -493,11 +557,11 @@ func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
}
// UpdateAIModel 更新AI模型配置如果不存在则创建用户特定配置
func (d *Database) UpdateAIModel(userID, id string, enabled bool, apiKey string) error {
func (d *Database) UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL string) error {
// 首先尝试更新现有的用户配置
result, err := d.db.Exec(`
UPDATE ai_models SET enabled = ?, api_key = ? WHERE id = ? AND user_id = ?
`, enabled, apiKey, id, userID)
UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ? WHERE id = ? AND user_id = ?
`, enabled, apiKey, customAPIURL, id, userID)
if err != nil {
return err
}
@@ -532,9 +596,9 @@ func (d *Database) UpdateAIModel(userID, id string, enabled bool, apiKey string)
// 创建用户特定的配置
userModelID := fmt.Sprintf("%s_%s", userID, id)
_, err = d.db.Exec(`
INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`, userModelID, userID, name, provider, enabled, apiKey)
INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`, userModelID, userID, name, provider, enabled, apiKey, customAPIURL)
return err
}
@@ -643,11 +707,11 @@ func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secre
}
// CreateAIModel 创建AI模型配置
func (d *Database) CreateAIModel(userID, id, name, provider string, enabled bool, apiKey string) error {
func (d *Database) CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error {
_, err := d.db.Exec(`
INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled, api_key)
VALUES (?, ?, ?, ?, ?, ?)
`, id, userID, name, provider, enabled, apiKey)
INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, id, userID, name, provider, enabled, apiKey, customAPIURL)
return err
}
@@ -663,9 +727,9 @@ func (d *Database) CreateExchange(userID, id, name, typ string, enabled bool, ap
// CreateTrader 创建交易员
func (d *Database) CreateTrader(trader *TraderRecord) error {
_, err := d.db.Exec(`
INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, custom_prompt, override_base_prompt, is_cross_margin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.CustomPrompt, trader.OverrideBasePrompt, trader.IsCrossMargin)
INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, is_cross_margin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols, trader.UseCoinPool, trader.UseOITop, trader.CustomPrompt, trader.OverrideBasePrompt, trader.IsCrossMargin)
return err
}
@@ -673,6 +737,9 @@ func (d *Database) CreateTrader(trader *TraderRecord) error {
func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
rows, err := d.db.Query(`
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running,
COALESCE(btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(altcoin_leverage, 5) as altcoin_leverage,
COALESCE(trading_symbols, '') as trading_symbols,
COALESCE(use_coin_pool, 0) as use_coin_pool, COALESCE(use_oi_top, 0) as use_oi_top,
COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt,
COALESCE(is_cross_margin, 1) as is_cross_margin, created_at, updated_at
FROM traders WHERE user_id = ? ORDER BY created_at DESC
@@ -688,6 +755,8 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
err := rows.Scan(
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
&trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols,
&trader.UseCoinPool, &trader.UseOITop,
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.IsCrossMargin,
&trader.CreatedAt, &trader.UpdatedAt,
)
@@ -706,6 +775,22 @@ func (d *Database) UpdateTraderStatus(userID, id string, isRunning bool) error {
return err
}
// UpdateTrader 更新交易员配置
func (d *Database) UpdateTrader(trader *TraderRecord) error {
_, err := d.db.Exec(`
UPDATE traders SET
name = ?, ai_model_id = ?, exchange_id = ?, initial_balance = ?,
scan_interval_minutes = ?, btc_eth_leverage = ?, altcoin_leverage = ?,
trading_symbols = ?, custom_prompt = ?, override_base_prompt = ?,
is_cross_margin = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND user_id = ?
`, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance,
trader.ScanIntervalMinutes, trader.BTCETHLeverage, trader.AltcoinLeverage,
trader.TradingSymbols, trader.CustomPrompt, trader.OverrideBasePrompt,
trader.IsCrossMargin, trader.ID, trader.UserID)
return err
}
// UpdateTraderCustomPrompt 更新交易员自定义Prompt
func (d *Database) UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error {
_, err := d.db.Exec(`UPDATE traders SET custom_prompt = ?, override_base_prompt = ? WHERE id = ? AND user_id = ?`, customPrompt, overrideBase, id, userID)
@@ -772,6 +857,40 @@ func (d *Database) SetSystemConfig(key, value string) error {
return err
}
// CreateUserSignalSource 创建用户信号源配置
func (d *Database) CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) error {
_, err := d.db.Exec(`
INSERT OR REPLACE INTO user_signal_sources (user_id, coin_pool_url, oi_top_url, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
`, userID, coinPoolURL, oiTopURL)
return err
}
// GetUserSignalSource 获取用户信号源配置
func (d *Database) GetUserSignalSource(userID string) (*UserSignalSource, error) {
var source UserSignalSource
err := d.db.QueryRow(`
SELECT id, user_id, coin_pool_url, oi_top_url, created_at, updated_at
FROM user_signal_sources WHERE user_id = ?
`, userID).Scan(
&source.ID, &source.UserID, &source.CoinPoolURL, &source.OITopURL,
&source.CreatedAt, &source.UpdatedAt,
)
if err != nil {
return nil, err
}
return &source, nil
}
// UpdateUserSignalSource 更新用户信号源配置
func (d *Database) UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) error {
_, err := d.db.Exec(`
UPDATE user_signal_sources SET coin_pool_url = ?, oi_top_url = ?, updated_at = CURRENT_TIMESTAMP
WHERE user_id = ?
`, coinPoolURL, oiTopURL, userID)
return err
}
// Close 关闭数据库连接
func (d *Database) Close() error {
return d.db.Close()

25
main.go
View File

@@ -208,26 +208,37 @@ func main() {
log.Fatalf("❌ 加载交易员失败: %v", err)
}
// 获取数据库中的所有交易员配置(用于显示使用default用户
traders, err := database.GetTraders("default")
// 获取所有用户的交易员配置(用于显示)
userIDs, err := database.GetAllUsers()
if err != nil {
log.Fatalf(" 获取交易员列表失败: %v", err)
log.Printf("⚠️ 获取用户列表失败: %v", err)
userIDs = []string{"default"} // 回退到default用户
}
var allTraders []*config.TraderRecord
for _, userID := range userIDs {
traders, err := database.GetTraders(userID)
if err != nil {
log.Printf("⚠️ 获取用户 %s 的交易员失败: %v", userID, err)
continue
}
allTraders = append(allTraders, traders...)
}
// 显示加载的交易员信息
fmt.Println()
fmt.Println("🤖 数据库中的AI交易员配置:")
if len(traders) == 0 {
if len(allTraders) == 0 {
fmt.Println(" • 暂无配置的交易员请通过Web界面创建")
} else {
for _, trader := range traders {
for _, trader := range allTraders {
status := "停止"
if trader.IsRunning {
status = "运行中"
}
fmt.Printf(" • %s (%s + %s) - 初始资金: %.0f USDT [%s]\n",
fmt.Printf(" • %s (%s + %s) - 用户: %s - 初始资金: %.0f USDT [%s]\n",
trader.Name, strings.ToUpper(trader.AIModelID), strings.ToUpper(trader.ExchangeID),
trader.InitialBalance, status)
trader.UserID, trader.InitialBalance, status)
}
}

View File

@@ -1,11 +1,13 @@
package manager
import (
"encoding/json"
"fmt"
"log"
"nofx/config"
"nofx/trader"
"strconv"
"strings"
"sync"
"time"
)
@@ -28,28 +30,33 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
tm.mu.Lock()
defer tm.mu.Unlock()
// 根据admin_mode确定用户ID
adminModeStr, _ := database.GetSystemConfig("admin_mode")
userID := "default"
if adminModeStr != "false" { // 默认为true
userID = "admin"
}
// 获取数据库中的所有交易员
traders, err := database.GetTraders(userID)
// 获取所有用户
userIDs, err := database.GetAllUsers()
if err != nil {
return fmt.Errorf("获取交易员列表失败: %w", err)
return fmt.Errorf("获取用户列表失败: %w", err)
}
log.Printf("📋 加载数据库中的交易员配置: %d 个 (用户: %s)", len(traders), userID)
log.Printf("📋 发现 %d 个用户,开始加载所有交易员配置...", len(userIDs))
// 获取系统配置
coinPoolURL, _ := database.GetSystemConfig("coin_pool_api_url")
var allTraders []*config.TraderRecord
for _, userID := range userIDs {
// 获取每个用户的交易员
traders, err := database.GetTraders(userID)
if err != nil {
log.Printf("⚠️ 获取用户 %s 的交易员失败: %v", userID, err)
continue
}
log.Printf("📋 用户 %s: %d 个交易员", userID, len(traders))
allTraders = append(allTraders, traders...)
}
log.Printf("📋 总共加载 %d 个交易员配置", len(allTraders))
// 获取系统配置(不包含信号源,信号源现在为用户级别)
maxDailyLossStr, _ := database.GetSystemConfig("max_daily_loss")
maxDrawdownStr, _ := database.GetSystemConfig("max_drawdown")
stopTradingMinutesStr, _ := database.GetSystemConfig("stop_trading_minutes")
btcEthLeverageStr, _ := database.GetSystemConfig("btc_eth_leverage")
altcoinLeverageStr, _ := database.GetSystemConfig("altcoin_leverage")
defaultCoinsStr, _ := database.GetSystemConfig("default_coins")
// 解析配置
maxDailyLoss := 10.0 // 默认值
@@ -67,20 +74,19 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
stopTradingMinutes = val
}
btcEthLeverage := 5 // 默认值
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
btcEthLeverage = val
}
altcoinLeverage := 5 // 默认值
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
altcoinLeverage = val
// 解析默认币种列表
var defaultCoins []string
if defaultCoinsStr != "" {
if err := json.Unmarshal([]byte(defaultCoinsStr), &defaultCoins); err != nil {
log.Printf("⚠️ 解析默认币种配置失败: %v使用空列表", err)
defaultCoins = []string{}
}
}
// 为每个交易员获取AI模型和交易所配置
for _, traderCfg := range traders {
// 获取AI模型配置
aiModels, err := database.GetAIModels(userID)
for _, traderCfg := range allTraders {
// 获取AI模型配置使用交易员所属的用户ID
aiModels, err := database.GetAIModels(traderCfg.UserID)
if err != nil {
log.Printf("⚠️ 获取AI模型配置失败: %v", err)
continue
@@ -104,8 +110,8 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
continue
}
// 获取交易所配置
exchanges, err := database.GetExchanges(userID)
// 获取交易所配置使用交易员所属的用户ID
exchanges, err := database.GetExchanges(traderCfg.UserID)
if err != nil {
log.Printf("⚠️ 获取交易所配置失败: %v", err)
continue
@@ -129,8 +135,18 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
continue
}
// 获取用户信号源配置
var coinPoolURL, oiTopURL string
if userSignalSource, err := database.GetUserSignalSource(traderCfg.UserID); err == nil {
coinPoolURL = userSignalSource.CoinPoolURL
oiTopURL = userSignalSource.OITopURL
} else {
// 如果用户没有配置信号源,使用空字符串
log.Printf("🔍 用户 %s 暂未配置信号源", traderCfg.UserID)
}
// 添加到TraderManager
err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, btcEthLeverage, altcoinLeverage)
err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins)
if err != nil {
log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err)
continue
@@ -142,11 +158,36 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
}
// addTraderFromConfig 内部方法:从配置添加交易员(不加锁,因为调用方已加锁)
func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes, btcEthLeverage, altcoinLeverage int) error {
func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string) error {
if _, exists := tm.traders[traderCfg.ID]; exists {
return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID)
}
// 处理交易币种列表
var tradingCoins []string
if traderCfg.TradingSymbols != "" {
// 解析逗号分隔的交易币种列表
symbols := strings.Split(traderCfg.TradingSymbols, ",")
for _, symbol := range symbols {
symbol = strings.TrimSpace(symbol)
if symbol != "" {
tradingCoins = append(tradingCoins, symbol)
}
}
}
// 如果没有指定交易币种,使用默认币种
if len(tradingCoins) == 0 {
tradingCoins = defaultCoins
}
// 根据交易员配置决定是否使用信号源
var effectiveCoinPoolURL string
if traderCfg.UseCoinPool && coinPoolURL != "" {
effectiveCoinPoolURL = coinPoolURL
log.Printf("✓ 交易员 %s 启用 COIN POOL 信号源: %s", traderCfg.Name, coinPoolURL)
}
// 构建AutoTraderConfig
traderConfig := trader.AutoTraderConfig{
ID: traderCfg.ID,
@@ -157,18 +198,20 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
BinanceSecretKey: "",
HyperliquidPrivateKey: "",
HyperliquidTestnet: exchangeCfg.Testnet,
CoinPoolAPIURL: coinPoolURL,
CoinPoolAPIURL: effectiveCoinPoolURL,
UseQwen: aiModelCfg.Provider == "qwen",
DeepSeekKey: "",
QwenKey: "",
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
InitialBalance: traderCfg.InitialBalance,
BTCETHLeverage: btcEthLeverage,
AltcoinLeverage: altcoinLeverage,
BTCETHLeverage: traderCfg.BTCETHLeverage,
AltcoinLeverage: traderCfg.AltcoinLeverage,
MaxDailyLoss: maxDailyLoss,
MaxDrawdown: maxDrawdown,
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
IsCrossMargin: traderCfg.IsCrossMargin,
DefaultCoins: defaultCoins,
TradingCoins: tradingCoins,
}
// 根据交易所类型设置API密钥
@@ -216,7 +259,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
// AddTrader 从数据库配置添加trader (移除旧版兼容性)
// AddTraderFromDB 从数据库配置添加trader
func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes, btcEthLeverage, altcoinLeverage int) error {
func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string) error {
tm.mu.Lock()
defer tm.mu.Unlock()
@@ -224,6 +267,31 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel
return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID)
}
// 处理交易币种列表
var tradingCoins []string
if traderCfg.TradingSymbols != "" {
// 解析逗号分隔的交易币种列表
symbols := strings.Split(traderCfg.TradingSymbols, ",")
for _, symbol := range symbols {
symbol = strings.TrimSpace(symbol)
if symbol != "" {
tradingCoins = append(tradingCoins, symbol)
}
}
}
// 如果没有指定交易币种,使用默认币种
if len(tradingCoins) == 0 {
tradingCoins = defaultCoins
}
// 根据交易员配置决定是否使用信号源
var effectiveCoinPoolURL string
if traderCfg.UseCoinPool && coinPoolURL != "" {
effectiveCoinPoolURL = coinPoolURL
log.Printf("✓ 交易员 %s 启用 COIN POOL 信号源: %s", traderCfg.Name, coinPoolURL)
}
// 构建AutoTraderConfig
traderConfig := trader.AutoTraderConfig{
ID: traderCfg.ID,
@@ -234,18 +302,20 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel
BinanceSecretKey: "",
HyperliquidPrivateKey: "",
HyperliquidTestnet: exchangeCfg.Testnet,
CoinPoolAPIURL: coinPoolURL,
CoinPoolAPIURL: effectiveCoinPoolURL,
UseQwen: aiModelCfg.Provider == "qwen",
DeepSeekKey: "",
QwenKey: "",
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
InitialBalance: traderCfg.InitialBalance,
BTCETHLeverage: btcEthLeverage,
AltcoinLeverage: altcoinLeverage,
BTCETHLeverage: traderCfg.BTCETHLeverage,
AltcoinLeverage: traderCfg.AltcoinLeverage,
MaxDailyLoss: maxDailyLoss,
MaxDrawdown: maxDrawdown,
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
IsCrossMargin: traderCfg.IsCrossMargin,
DefaultCoins: defaultCoins,
TradingCoins: tradingCoins,
}
// 根据交易所类型设置API密钥
@@ -373,6 +443,7 @@ func (tm *TraderManager) GetComparisonData() (map[string]interface{}, error) {
"trader_id": t.GetID(),
"trader_name": t.GetName(),
"ai_model": t.GetAIModel(),
"exchange": t.GetExchange(),
"total_equity": account["total_equity"],
"total_pnl": account["total_pnl"],
"total_pnl_pct": account["total_pnl_pct"],
@@ -389,42 +460,55 @@ func (tm *TraderManager) GetComparisonData() (map[string]interface{}, error) {
return comparison, nil
}
// GetCompetitionData 获取竞赛数据(特定用户的所有交易员)
func (tm *TraderManager) GetCompetitionData(userID string) (map[string]interface{}, error) {
// GetCompetitionData 获取竞赛数据(全平台所有交易员)
func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) {
tm.mu.RLock()
defer tm.mu.RUnlock()
comparison := make(map[string]interface{})
traders := make([]map[string]interface{}, 0)
// 获取该用户的交易员
for traderID, t := range tm.traders {
// 检查trader是否属于该用户通过ID前缀判断
// 格式userID_traderName
if !isUserTrader(traderID, userID) {
continue
}
// 获取全平台所有交易员
for _, t := range tm.traders {
account, err := t.GetAccountInfo()
if err != nil {
log.Printf("⚠️ 获取交易员 %s 账户信息失败: %v", traderID, err)
continue
}
status := t.GetStatus()
traders = append(traders, map[string]interface{}{
"trader_id": t.GetID(),
"trader_name": t.GetName(),
"ai_model": t.GetAIModel(),
"total_equity": account["total_equity"],
"total_pnl": account["total_pnl"],
"total_pnl_pct": account["total_pnl_pct"],
"position_count": account["position_count"],
"margin_used_pct": account["margin_used_pct"],
"is_running": status["is_running"],
})
var traderData map[string]interface{}
if err != nil {
// 如果获取账户信息失败,使用默认值但仍然显示交易员
log.Printf("⚠️ 获取交易员 %s 账户信息失败: %v", t.GetID(), err)
traderData = map[string]interface{}{
"trader_id": t.GetID(),
"trader_name": t.GetName(),
"ai_model": t.GetAIModel(),
"exchange": t.GetExchange(),
"total_equity": 0.0,
"total_pnl": 0.0,
"total_pnl_pct": 0.0,
"position_count": 0,
"margin_used_pct": 0.0,
"is_running": status["is_running"],
"error": "账户数据获取失败",
}
} else {
// 正常情况下使用真实账户数据
traderData = map[string]interface{}{
"trader_id": t.GetID(),
"trader_name": t.GetName(),
"ai_model": t.GetAIModel(),
"exchange": t.GetExchange(),
"total_equity": account["total_equity"],
"total_pnl": account["total_pnl"],
"total_pnl_pct": account["total_pnl_pct"],
"position_count": account["position_count"],
"margin_used_pct": account["margin_used_pct"],
"is_running": status["is_running"],
}
}
traders = append(traders, traderData)
}
comparison["traders"] = traders
comparison["count"] = len(traders)
@@ -474,13 +558,21 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
log.Printf("📋 为用户 %s 加载交易员配置: %d 个", userID, len(traders))
// 获取系统配置
coinPoolURL, _ := database.GetSystemConfig("coin_pool_api_url")
// 获取系统配置(不包含信号源,信号源现在为用户级别)
maxDailyLossStr, _ := database.GetSystemConfig("max_daily_loss")
maxDrawdownStr, _ := database.GetSystemConfig("max_drawdown")
stopTradingMinutesStr, _ := database.GetSystemConfig("stop_trading_minutes")
btcEthLeverageStr, _ := database.GetSystemConfig("btc_eth_leverage")
altcoinLeverageStr, _ := database.GetSystemConfig("altcoin_leverage")
defaultCoinsStr, _ := database.GetSystemConfig("default_coins")
// 获取用户信号源配置
var coinPoolURL, oiTopURL string
if userSignalSource, err := database.GetUserSignalSource(userID); err == nil {
coinPoolURL = userSignalSource.CoinPoolURL
oiTopURL = userSignalSource.OITopURL
log.Printf("📡 加载用户 %s 的信号源配置: COIN POOL=%s, OI TOP=%s", userID, coinPoolURL, oiTopURL)
} else {
log.Printf("🔍 用户 %s 暂未配置信号源", userID)
}
// 解析配置
maxDailyLoss := 10.0 // 默认值
@@ -498,14 +590,13 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
stopTradingMinutes = val
}
btcEthLeverage := 5 // 默认值
if val, err := strconv.Atoi(btcEthLeverageStr); err == nil && val > 0 {
btcEthLeverage = val
}
altcoinLeverage := 5 // 默认值
if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 {
altcoinLeverage = val
// 解析默认币种列表
var defaultCoins []string
if defaultCoinsStr != "" {
if err := json.Unmarshal([]byte(defaultCoinsStr), &defaultCoins); err != nil {
log.Printf("⚠️ 解析默认币种配置失败: %v使用空列表", err)
defaultCoins = []string{}
}
}
// 为每个交易员获取AI模型和交易所配置
@@ -567,7 +658,7 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
}
// 使用现有的方法加载交易员
err = tm.loadSingleTrader(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, btcEthLeverage, altcoinLeverage)
err = tm.loadSingleTrader(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins)
if err != nil {
log.Printf("⚠️ 加载交易员 %s 失败: %v", traderCfg.Name, err)
}
@@ -577,7 +668,32 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
}
// loadSingleTrader 加载单个交易员(从现有代码提取的公共逻辑)
func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes, btcEthLeverage, altcoinLeverage int) error {
func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string) error {
// 处理交易币种列表
var tradingCoins []string
if traderCfg.TradingSymbols != "" {
// 解析逗号分隔的交易币种列表
symbols := strings.Split(traderCfg.TradingSymbols, ",")
for _, symbol := range symbols {
symbol = strings.TrimSpace(symbol)
if symbol != "" {
tradingCoins = append(tradingCoins, symbol)
}
}
}
// 如果没有指定交易币种,使用默认币种
if len(tradingCoins) == 0 {
tradingCoins = defaultCoins
}
// 根据交易员配置决定是否使用信号源
var effectiveCoinPoolURL string
if traderCfg.UseCoinPool && coinPoolURL != "" {
effectiveCoinPoolURL = coinPoolURL
log.Printf("✓ 交易员 %s 启用 COIN POOL 信号源: %s", traderCfg.Name, coinPoolURL)
}
// 构建AutoTraderConfig
traderConfig := trader.AutoTraderConfig{
ID: traderCfg.ID,
@@ -585,14 +701,16 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode
AIModel: aiModelCfg.Provider, // 使用provider作为模型标识
Exchange: exchangeCfg.ID, // 使用exchange ID
InitialBalance: traderCfg.InitialBalance,
BTCETHLeverage: traderCfg.BTCETHLeverage,
AltcoinLeverage: traderCfg.AltcoinLeverage,
ScanInterval: time.Duration(traderCfg.ScanIntervalMinutes) * time.Minute,
CoinPoolAPIURL: coinPoolURL,
BTCETHLeverage: btcEthLeverage,
AltcoinLeverage: altcoinLeverage,
CoinPoolAPIURL: effectiveCoinPoolURL,
MaxDailyLoss: maxDailyLoss,
MaxDrawdown: maxDrawdown,
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
IsCrossMargin: traderCfg.IsCrossMargin,
DefaultCoins: defaultCoins,
TradingCoins: tradingCoins,
}
// 根据交易所类型设置API密钥

View File

@@ -66,6 +66,10 @@ type AutoTraderConfig struct {
// 仓位模式
IsCrossMargin bool // true=全仓模式, false=逐仓模式
// 币种配置
DefaultCoins []string // 默认币种列表(从数据库获取)
TradingCoins []string // 实际交易币种列表
}
// AutoTrader 自动交易器
@@ -82,6 +86,8 @@ type AutoTrader struct {
dailyPnL float64
customPrompt string // 自定义交易策略prompt
overrideBasePrompt bool // 是否覆盖基础prompt
defaultCoins []string // 默认币种列表(从数据库获取)
tradingCoins []string // 实际交易币种列表
lastResetTime time.Time
stopUntil time.Time
isRunning bool
@@ -184,6 +190,8 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
mcpClient: mcpClient,
decisionLogger: decisionLogger,
initialBalance: config.InitialBalance,
defaultCoins: config.DefaultCoins,
tradingCoins: config.TradingCoins,
lastResetTime: time.Now(),
startTime: time.Now(),
callCount: 0,
@@ -486,30 +494,12 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
}
}
// 3. 获取合并的候选币种池AI500 + OI Top去重
// 无论有没有持仓都分析相同数量的币种让AI看到所有好机会
// AI会根据保证金使用率和现有持仓情况自己决定是否要换仓
const ai500Limit = 20 // AI500取前20个评分最高的币种
// 获取合并后的币种池AI500 + OI Top
mergedPool, err := pool.GetMergedCoinPool(ai500Limit)
// 3. 获取交易员的候选币种池
candidateCoins, err := at.getCandidateCoins()
if err != nil {
return nil, fmt.Errorf("获取合并币种失败: %w", err)
return nil, fmt.Errorf("获取候选币种失败: %w", err)
}
// 构建候选币种列表(包含来源信息)
var candidateCoins []decision.CandidateCoin
for _, symbol := range mergedPool.AllSymbols {
sources := mergedPool.SymbolSources[symbol]
candidateCoins = append(candidateCoins, decision.CandidateCoin{
Symbol: symbol,
Sources: sources, // "ai500" 和/或 "oi_top"
})
}
log.Printf("📋 合并币种池: AI500前%d + OI_Top20 = 总计%d个候选币种",
ai500Limit, len(candidateCoins))
// 4. 计算总盈亏
totalPnL := totalEquity - at.initialBalance
totalPnLPct := 0.0
@@ -759,6 +749,11 @@ func (at *AutoTrader) GetAIModel() string {
return at.aiModel
}
// GetExchange 获取交易所
func (at *AutoTrader) GetExchange() string {
return at.exchange
}
// SetCustomPrompt 设置自定义交易策略prompt
func (at *AutoTrader) SetCustomPrompt(prompt string) {
at.customPrompt = prompt
@@ -968,3 +963,74 @@ func sortDecisionsByPriority(decisions []decision.Decision) []decision.Decision
return sorted
}
// getCandidateCoins 获取交易员的候选币种列表
func (at *AutoTrader) getCandidateCoins() ([]decision.CandidateCoin, error) {
if len(at.tradingCoins) == 0 {
// 使用数据库配置的默认币种列表
var candidateCoins []decision.CandidateCoin
if len(at.defaultCoins) > 0 {
// 使用数据库中配置的默认币种
for _, coin := range at.defaultCoins {
symbol := normalizeSymbol(coin)
candidateCoins = append(candidateCoins, decision.CandidateCoin{
Symbol: symbol,
Sources: []string{"default"}, // 标记为数据库默认币种
})
}
log.Printf("📋 [%s] 使用数据库默认币种: %d个币种 %v",
at.name, len(candidateCoins), at.defaultCoins)
return candidateCoins, nil
} else {
// 如果数据库中没有配置默认币种则使用AI500+OI Top作为fallback
const ai500Limit = 20 // AI500取前20个评分最高的币种
mergedPool, err := pool.GetMergedCoinPool(ai500Limit)
if err != nil {
return nil, fmt.Errorf("获取合并币种池失败: %w", err)
}
// 构建候选币种列表(包含来源信息)
for _, symbol := range mergedPool.AllSymbols {
sources := mergedPool.SymbolSources[symbol]
candidateCoins = append(candidateCoins, decision.CandidateCoin{
Symbol: symbol,
Sources: sources, // "ai500" 和/或 "oi_top"
})
}
log.Printf("📋 [%s] 数据库无默认币种配置使用AI500+OI Top: AI500前%d + OI_Top20 = 总计%d个候选币种",
at.name, ai500Limit, len(candidateCoins))
return candidateCoins, nil
}
} else {
// 使用自定义币种列表
var candidateCoins []decision.CandidateCoin
for _, coin := range at.tradingCoins {
// 确保币种格式正确转为大写USDT交易对
symbol := normalizeSymbol(coin)
candidateCoins = append(candidateCoins, decision.CandidateCoin{
Symbol: symbol,
Sources: []string{"custom"}, // 标记为自定义来源
})
}
log.Printf("📋 [%s] 使用自定义币种: %d个币种 %v",
at.name, len(candidateCoins), at.tradingCoins)
return candidateCoins, nil
}
}
// normalizeSymbol 标准化币种符号确保以USDT结尾
func normalizeSymbol(symbol string) string {
// 转为大写
symbol = strings.ToUpper(strings.TrimSpace(symbol))
// 确保以USDT结尾
if !strings.HasSuffix(symbol, "USDT") {
symbol = symbol + "USDT"
}
return symbol
}

View File

@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/icons/nofx.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NOFX - AI Auto Trading Dashboard</title>
</head>

296
web/public/icons/nofx.svg Normal file
View File

@@ -0,0 +1,296 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100%" viewBox="0 0 1024 1024" enable-background="new 0 0 1024 1024" xml:space="preserve">
<path fill="#FDFDFD" opacity="1.000000" stroke="none"
d="
M500.000000,1025.000000
C333.333344,1025.000000 167.166672,1025.000000 1.000000,1025.000000
C1.000000,683.666687 1.000000,342.333344 1.000000,1.000000
C342.333344,1.000000 683.666687,1.000000 1025.000000,1.000000
C1025.000000,342.333344 1025.000000,683.666687 1025.000000,1025.000000
C850.166687,1025.000000 675.333313,1025.000000 500.000000,1025.000000
M534.031067,584.483826
C534.031067,588.398560 534.031067,592.313354 534.031067,596.807495
C545.055725,596.807495 555.191895,596.895569 565.324890,596.757568
C568.675903,596.711914 570.108704,597.732422 570.067505,601.303345
C569.908142,615.130981 569.858154,628.962708 570.028137,642.789673
C570.077393,646.796997 568.523926,648.072266 564.742004,647.996033
C556.247864,647.824707 547.744873,648.036621 539.253845,647.796204
C535.514526,647.690369 533.817627,648.561218 533.996155,652.742493
C534.308533,660.059753 533.817566,667.412292 534.157166,674.727051
C534.375000,679.418884 533.066345,681.195984 528.085999,681.018494
C517.601807,680.644897 507.094879,680.960999 496.599945,680.810852
C493.243347,680.762817 491.914062,681.787109 491.941986,685.357300
C492.094421,704.850281 492.122192,724.345581 491.971497,743.838440
C491.941101,747.773010 493.228638,749.004578 497.124115,748.978394
C518.949890,748.831421 540.777771,748.821838 562.603271,748.980225
C566.173035,749.006165 567.215881,747.891907 567.153503,744.444214
C566.966858,734.117310 567.222534,723.781128 566.953308,713.457703
C566.847107,709.385132 568.577148,708.202698 572.203125,708.286438
C578.188965,708.424683 584.192078,708.207458 590.160583,708.579163
C594.476196,708.847839 596.032410,707.578918 595.856140,703.035217
C595.503296,693.939453 595.751465,684.820312 595.751465,675.649231
C596.925232,675.398560 597.552307,675.149048 598.180054,675.147583
C615.339722,675.106445 632.499634,675.126099 649.658997,675.034668
C652.399414,675.020081 653.028992,676.300964 653.009949,678.716370
C652.946899,686.712646 653.113281,694.711670 652.973572,702.705811
C652.917664,705.902710 653.994019,707.188904 657.276245,707.107605
C663.269775,706.959106 669.278931,707.315979 675.266113,707.076050
C679.086365,706.922974 680.126648,708.399414 680.068604,712.025635
C679.897949,722.685486 680.140930,733.351624 679.989075,744.012024
C679.937683,747.618469 681.041504,748.997803 684.821899,748.976501
C708.314026,748.844055 731.807556,748.852051 755.299805,748.975403
C758.820862,748.993835 759.868530,747.806763 759.844788,744.357910
C759.709351,724.698547 759.709839,705.037598 759.818420,685.377991
C759.837585,681.910950 758.672913,680.734314 755.189087,680.794067
C744.195374,680.982727 733.194397,680.763184 722.201355,680.970520
C718.237671,681.045227 716.403687,679.967041 716.487549,675.664490
C716.636658,668.012939 716.185547,660.350403 716.298767,652.697144
C716.354187,648.952393 714.940552,647.784058 711.308655,647.875549
C703.149780,648.080994 694.974915,647.740051 686.821289,648.032654
C682.197937,648.198547 680.731934,646.551575 680.854858,641.973938
C681.136780,631.484619 680.943665,620.982361 680.937012,610.485352
C680.928345,596.707458 680.925720,596.708313 694.772095,596.712830
C701.716309,596.715027 708.660583,596.713257 716.156738,596.713257
C716.156738,587.047791 716.274170,578.086670 716.100098,569.131104
C716.028503,565.448486 717.396729,564.077209 721.057495,564.125061
C731.718384,564.264526 742.382324,564.204712 753.044861,564.170166
C755.140137,564.163330 757.234619,563.919800 759.374390,563.782654
C759.374390,541.682739 759.374390,520.113953 759.374390,498.512848
C758.314819,498.283966 757.679260,498.027313 757.043457,498.026611
C732.552490,497.999329 708.061401,498.037842 683.570801,497.939880
C680.143433,497.926178 679.874512,499.756226 679.904175,502.387390
C680.015015,512.215210 679.874512,522.048523 680.153259,531.870544
C680.266418,535.860596 679.224426,537.680786 674.914673,537.479248
C669.263062,537.215027 663.581482,537.623352 657.925659,537.406860
C654.311951,537.268616 652.858887,538.322510 652.991699,542.182861
C653.266357,550.170227 652.971191,558.175842 653.164490,566.167786
C653.254150,569.872559 651.856873,571.069702 648.200317,571.033142
C632.374023,570.874939 616.544556,570.857117 600.718201,571.000671
C597.009949,571.034241 595.702759,569.735596 595.771912,566.091858
C595.923645,558.098816 595.724426,550.099670 595.818298,542.104797
C595.854309,539.035889 594.871948,537.527527 591.554504,537.577759
C585.402100,537.671082 579.222473,537.141418 573.097961,537.541626
C567.969849,537.876709 566.857544,535.717102 566.994751,531.152100
C567.259827,522.329468 567.088135,513.493835 567.107239,504.663605
C567.121216,498.211700 567.128967,498.312378 560.831177,498.362671
C539.515869,498.532898 518.197937,498.814789 496.885956,498.590637
C492.527435,498.544800 491.869690,499.960571 491.895660,503.604095
C492.027496,522.097412 492.121552,540.592773 491.953308,559.084961
C491.917023,563.073425 493.143402,564.285767 497.052917,564.207214
C507.545197,563.996338 518.045471,564.219666 528.540771,564.103271
C532.401672,564.060364 534.411804,565.121643 534.089355,569.498840
C533.747498,574.138367 534.024414,578.823547 534.031067,584.483826
M253.353867,389.499359
C253.353867,425.600555 253.353867,461.701721 253.353867,498.020203
C273.984375,498.020203 294.234894,498.020203 314.879120,498.020203
C314.879120,466.733032 314.879120,435.525452 314.879120,404.231567
C325.188171,404.231567 335.102448,404.231567 345.353882,404.231567
C345.353882,416.582062 345.353882,428.655182 345.353882,441.171600
C356.752899,441.171600 367.816437,441.171600 379.144989,441.171600
C379.144989,452.733887 379.144989,463.845337 379.144989,475.463928
C387.484772,475.463928 395.556122,475.463928 404.001831,475.463928
C404.001831,483.396393 404.001831,490.822571 404.001831,498.353760
C424.266266,498.353760 444.136444,498.353760 464.049652,498.353760
C464.049652,428.578674 464.049652,359.158661 464.049652,289.508362
C443.934937,289.508362 424.057556,289.508362 403.805725,289.508362
C403.805725,326.036926 403.805725,362.284485 403.805725,398.714264
C393.694244,398.714264 383.958435,398.714264 373.754669,398.714264
C373.754669,387.079407 373.754669,375.679657 373.754669,363.763580
C364.226013,363.763580 355.157928,363.763580 345.754578,363.763580
C345.754578,350.975952 345.754578,338.719330 345.754578,326.013428
C335.334686,326.013428 325.264954,326.013428 314.727661,326.013428
C314.727661,313.817993 314.727661,302.078217 314.727661,290.294006
C294.024200,290.294006 273.800537,290.294006 253.353867,290.294006
C253.353867,323.258392 253.353867,355.878815 253.353867,389.499359
M253.324738,540.524414
C253.324738,609.787170 253.324738,679.049988 253.324738,748.416748
C273.913696,748.416748 293.998413,748.416748 314.709351,748.416748
C314.709351,719.517822 314.709351,690.832581 314.709351,661.901611
C350.772034,661.901611 386.208771,661.901611 421.853760,661.901611
C421.853760,645.744873 421.853760,629.843628 421.853760,613.658813
C386.514313,613.658813 351.406067,613.658813 315.981873,613.658813
C315.981873,602.016785 315.981873,590.659973 315.981873,578.921387
C365.640533,578.921387 414.883820,578.921387 464.020874,578.921387
C464.020874,562.126465 464.020874,545.717834 464.020874,529.276367
C393.615936,529.276367 323.565338,529.276367 253.319580,529.276367
C253.319580,532.918762 253.319580,536.226440 253.324738,540.524414
M492.003387,396.500000
C492.003387,418.075806 492.003387,439.651642 492.003387,461.703796
C504.388947,461.703796 516.107178,461.703796 528.313721,461.703796
C528.313721,472.416870 528.313721,482.654999 528.313721,492.669708
C565.543945,492.669708 602.271057,492.669708 639.460571,492.669708
C639.460571,481.874329 639.460571,471.464661 639.460571,460.745056
C652.522522,460.745056 665.122131,460.745056 677.734863,460.745056
C677.734863,427.692261 677.734863,394.973724 677.734863,361.929138
C664.783020,361.929138 652.186462,361.929138 639.150879,361.929138
C639.150879,350.688141 639.150879,339.765076 639.150879,328.555176
C601.986938,328.555176 565.268433,328.555176 527.973755,328.555176
C527.973755,339.860962 527.973755,350.904510 527.973755,362.241943
C515.652466,362.241943 503.905975,362.241943 492.002960,362.241943
C492.002960,373.544617 492.002960,384.522308 492.003387,396.500000
M680.952881,342.679718
C699.195862,342.679718 717.438782,342.679718 735.967041,342.679718
C735.967041,325.691925 736.031189,309.222900 735.806641,292.757812
C735.793213,291.775330 733.476685,289.998322 732.215942,289.983917
C715.887634,289.797485 699.555664,289.801270 683.227722,290.003845
C682.113281,290.017670 680.069885,291.995911 680.064697,293.068268
C679.986694,309.383881 680.176758,325.700775 680.952881,342.679718
z"/>
<path fill="#F9AF07" opacity="1.000000" stroke="none"
d="
M534.031128,583.986328
C534.024414,578.823547 533.747498,574.138367 534.089355,569.498840
C534.411804,565.121643 532.401672,564.060364 528.540771,564.103271
C518.045471,564.219666 507.545197,563.996338 497.052917,564.207214
C493.143402,564.285767 491.917023,563.073425 491.953308,559.084961
C492.121552,540.592773 492.027496,522.097412 491.895660,503.604095
C491.869690,499.960571 492.527435,498.544800 496.885956,498.590637
C518.197937,498.814789 539.515869,498.532898 560.831177,498.362671
C567.128967,498.312378 567.121216,498.211700 567.107239,504.663605
C567.088135,513.493835 567.259827,522.329468 566.994751,531.152100
C566.857544,535.717102 567.969849,537.876709 573.097961,537.541626
C579.222473,537.141418 585.402100,537.671082 591.554504,537.577759
C594.871948,537.527527 595.854309,539.035889 595.818298,542.104797
C595.724426,550.099670 595.923645,558.098816 595.771912,566.091858
C595.702759,569.735596 597.009949,571.034241 600.718201,571.000671
C616.544556,570.857117 632.374023,570.874939 648.200317,571.033142
C651.856873,571.069702 653.254150,569.872559 653.164490,566.167786
C652.971191,558.175842 653.266357,550.170227 652.991699,542.182861
C652.858887,538.322510 654.311951,537.268616 657.925659,537.406860
C663.581482,537.623352 669.263062,537.215027 674.914673,537.479248
C679.224426,537.680786 680.266418,535.860596 680.153259,531.870544
C679.874512,522.048523 680.015015,512.215210 679.904175,502.387390
C679.874512,499.756226 680.143433,497.926178 683.570801,497.939880
C708.061401,498.037842 732.552490,497.999329 757.043457,498.026611
C757.679260,498.027313 758.314819,498.283966 759.374390,498.512848
C759.374390,520.113953 759.374390,541.682739 759.374390,563.782654
C757.234619,563.919800 755.140137,564.163330 753.044861,564.170166
C742.382324,564.204712 731.718384,564.264526 721.057495,564.125061
C717.396729,564.077209 716.028503,565.448486 716.100098,569.131104
C716.274170,578.086670 716.156738,587.047791 716.156738,596.713257
C708.660583,596.713257 701.716309,596.715027 694.772095,596.712830
C680.925720,596.708313 680.928345,596.707458 680.937012,610.485352
C680.943665,620.982361 681.136780,631.484619 680.854858,641.973938
C680.731934,646.551575 682.197937,648.198547 686.821289,648.032654
C694.974915,647.740051 703.149780,648.080994 711.308655,647.875549
C714.940552,647.784058 716.354187,648.952393 716.298767,652.697144
C716.185547,660.350403 716.636658,668.012939 716.487549,675.664490
C716.403687,679.967041 718.237671,681.045227 722.201355,680.970520
C733.194397,680.763184 744.195374,680.982727 755.189087,680.794067
C758.672913,680.734314 759.837585,681.910950 759.818420,685.377991
C759.709839,705.037598 759.709351,724.698547 759.844788,744.357910
C759.868530,747.806763 758.820862,748.993835 755.299805,748.975403
C731.807556,748.852051 708.314026,748.844055 684.821899,748.976501
C681.041504,748.997803 679.937683,747.618469 679.989075,744.012024
C680.140930,733.351624 679.897949,722.685486 680.068604,712.025635
C680.126648,708.399414 679.086365,706.922974 675.266113,707.076050
C669.278931,707.315979 663.269775,706.959106 657.276245,707.107605
C653.994019,707.188904 652.917664,705.902710 652.973572,702.705811
C653.113281,694.711670 652.946899,686.712646 653.009949,678.716370
C653.028992,676.300964 652.399414,675.020081 649.658997,675.034668
C632.499634,675.126099 615.339722,675.106445 598.180054,675.147583
C597.552307,675.149048 596.925232,675.398560 595.751465,675.649231
C595.751465,684.820312 595.503296,693.939453 595.856140,703.035217
C596.032410,707.578918 594.476196,708.847839 590.160583,708.579163
C584.192078,708.207458 578.188965,708.424683 572.203125,708.286438
C568.577148,708.202698 566.847107,709.385132 566.953308,713.457703
C567.222534,723.781128 566.966858,734.117310 567.153503,744.444214
C567.215881,747.891907 566.173035,749.006165 562.603271,748.980225
C540.777771,748.821838 518.949890,748.831421 497.124115,748.978394
C493.228638,749.004578 491.941101,747.773010 491.971497,743.838440
C492.122192,724.345581 492.094421,704.850281 491.941986,685.357300
C491.914062,681.787109 493.243347,680.762817 496.599945,680.810852
C507.094879,680.960999 517.601807,680.644897 528.085999,681.018494
C533.066345,681.195984 534.375000,679.418884 534.157166,674.727051
C533.817566,667.412292 534.308533,660.059753 533.996155,652.742493
C533.817627,648.561218 535.514526,647.690369 539.253845,647.796204
C547.744873,648.036621 556.247864,647.824707 564.742004,647.996033
C568.523926,648.072266 570.077393,646.796997 570.028137,642.789673
C569.858154,628.962708 569.908142,615.130981 570.067505,601.303345
C570.108704,597.732422 568.675903,596.711914 565.324890,596.757568
C555.191895,596.895569 545.055725,596.807495 534.031067,596.807495
C534.031067,592.313354 534.031067,588.398560 534.031128,583.986328
z"/>
<path fill="#020202" opacity="1.000000" stroke="none"
d="
M253.353867,388.999268
C253.353867,355.878815 253.353867,323.258392 253.353867,290.294006
C273.800537,290.294006 294.024200,290.294006 314.727661,290.294006
C314.727661,302.078217 314.727661,313.817993 314.727661,326.013428
C325.264954,326.013428 335.334686,326.013428 345.754578,326.013428
C345.754578,338.719330 345.754578,350.975952 345.754578,363.763580
C355.157928,363.763580 364.226013,363.763580 373.754669,363.763580
C373.754669,375.679657 373.754669,387.079407 373.754669,398.714264
C383.958435,398.714264 393.694244,398.714264 403.805725,398.714264
C403.805725,362.284485 403.805725,326.036926 403.805725,289.508362
C424.057556,289.508362 443.934937,289.508362 464.049652,289.508362
C464.049652,359.158661 464.049652,428.578674 464.049652,498.353760
C444.136444,498.353760 424.266266,498.353760 404.001831,498.353760
C404.001831,490.822571 404.001831,483.396393 404.001831,475.463928
C395.556122,475.463928 387.484772,475.463928 379.144989,475.463928
C379.144989,463.845337 379.144989,452.733887 379.144989,441.171600
C367.816437,441.171600 356.752899,441.171600 345.353882,441.171600
C345.353882,428.655182 345.353882,416.582062 345.353882,404.231567
C335.102448,404.231567 325.188171,404.231567 314.879120,404.231567
C314.879120,435.525452 314.879120,466.733032 314.879120,498.020203
C294.234894,498.020203 273.984375,498.020203 253.353867,498.020203
C253.353867,461.701721 253.353867,425.600555 253.353867,388.999268
z"/>
<path fill="#020202" opacity="1.000000" stroke="none"
d="
M253.322159,540.029297
C253.319580,536.226440 253.319580,532.918762 253.319580,529.276367
C323.565338,529.276367 393.615936,529.276367 464.020874,529.276367
C464.020874,545.717834 464.020874,562.126465 464.020874,578.921387
C414.883820,578.921387 365.640533,578.921387 315.981873,578.921387
C315.981873,590.659973 315.981873,602.016785 315.981873,613.658813
C351.406067,613.658813 386.514313,613.658813 421.853760,613.658813
C421.853760,629.843628 421.853760,645.744873 421.853760,661.901611
C386.208771,661.901611 350.772034,661.901611 314.709351,661.901611
C314.709351,690.832581 314.709351,719.517822 314.709351,748.416748
C293.998413,748.416748 273.913696,748.416748 253.324738,748.416748
C253.324738,679.049988 253.324738,609.787170 253.322159,540.029297
z"/>
<path fill="#020202" opacity="1.000000" stroke="none"
d="
M492.003174,396.000000
C492.002960,384.522308 492.002960,373.544617 492.002960,362.241943
C503.905975,362.241943 515.652466,362.241943 527.973755,362.241943
C527.973755,350.904510 527.973755,339.860962 527.973755,328.555176
C565.268433,328.555176 601.986938,328.555176 639.150879,328.555176
C639.150879,339.765076 639.150879,350.688141 639.150879,361.929138
C652.186462,361.929138 664.783020,361.929138 677.734863,361.929138
C677.734863,394.973724 677.734863,427.692261 677.734863,460.745056
C665.122131,460.745056 652.522522,460.745056 639.460571,460.745056
C639.460571,471.464661 639.460571,481.874329 639.460571,492.669708
C602.271057,492.669708 565.543945,492.669708 528.313721,492.669708
C528.313721,482.654999 528.313721,472.416870 528.313721,461.703796
C516.107178,461.703796 504.388947,461.703796 492.003387,461.703796
C492.003387,439.651642 492.003387,418.075806 492.003174,396.000000
M614.295959,428.499878
C614.295959,410.563873 614.295959,392.627899 614.295959,374.437195
C594.356567,374.437195 574.817383,374.437195 555.216553,374.437195
C555.216553,399.301392 555.216553,423.880280 555.216553,448.470581
C574.879578,448.470581 594.303711,448.470581 614.296204,448.470581
C614.296204,442.071533 614.296204,435.785706 614.295959,428.499878
z"/>
<path fill="#F7AF0C" opacity="1.000000" stroke="none"
d="
M680.628540,342.348541
C680.176758,325.700775 679.986694,309.383881 680.064697,293.068268
C680.069885,291.995911 682.113281,290.017670 683.227722,290.003845
C699.555664,289.801270 715.887634,289.797485 732.215942,289.983917
C733.476685,289.998322 735.793213,291.775330 735.806641,292.757812
C736.031189,309.222900 735.967041,325.691925 735.967041,342.679718
C717.438782,342.679718 699.195862,342.679718 680.628540,342.348541
z"/>
<path fill="#FCFCFC" opacity="1.000000" stroke="none"
d="
M614.296082,428.999878
C614.296204,435.785706 614.296204,442.071533 614.296204,448.470581
C594.303711,448.470581 574.879578,448.470581 555.216553,448.470581
C555.216553,423.880280 555.216553,399.301392 555.216553,374.437195
C574.817383,374.437195 594.356567,374.437195 614.295959,374.437195
C614.295959,392.627899 614.295959,410.563873 614.296082,428.999878
z"/>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -11,7 +11,6 @@ import { LanguageProvider, useLanguage } from './contexts/LanguageContext';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { t, type Language } from './i18n/translations';
import { useSystemConfig } from './hooks/useSystemConfig';
import { Bot, RefreshCw, TrendingUp, BarChart3, Brain, Download, Upload, Check, X, AlertCircle, Zap, TrendingUp as ArrowUp, TrendingDown as ArrowDown } from 'lucide-react';
import type {
SystemStatus,
AccountInfo,
@@ -176,7 +175,10 @@ function App() {
return (
<div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}>
<div className="text-center">
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 mx-auto mb-4 animate-pulse" />
<div className="w-16 h-16 rounded-full mx-auto mb-4 flex items-center justify-center text-3xl animate-spin"
style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
</div>
<p style={{ color: '#EAECEF' }}>...</p>
</div>
</div>
@@ -199,7 +201,9 @@ function App() {
<div className="relative flex items-center">
{/* Left - Logo and Title */}
<div className="flex items-center gap-3">
<img src="/images/logo.png" alt="NoFx Logo" className="w-8 h-8" />
<div className="w-8 h-8 flex items-center justify-center">
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-8 h-8" />
</div>
<div>
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
{t('appTitle', language)}
@@ -220,7 +224,7 @@ function App() {
: { background: 'transparent', color: '#848E9C' }
}
>
{t('aiCompetition', language)}
</button>
<button
onClick={() => setCurrentPage('traders')}
@@ -260,8 +264,7 @@ function App() {
{/* Admin Mode Indicator */}
{systemConfig?.admin_mode && (
<div className="flex items-center gap-2 px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm font-semibold" style={{ color: '#F0B90B' }}></span>
<span className="text-sm font-semibold" style={{ color: '#F0B90B' }}> {t('adminMode', language)}</span>
</div>
)}
@@ -426,16 +429,16 @@ function TraderDetailsPage({
<div className="mb-6 rounded p-6 animate-scale-in" style={{ background: 'linear-gradient(135deg, rgba(240, 185, 11, 0.15) 0%, rgba(252, 213, 53, 0.05) 100%)', border: '1px solid rgba(240, 185, 11, 0.2)', boxShadow: '0 0 30px rgba(240, 185, 11, 0.15)' }}>
<div className="flex items-start justify-between mb-3">
<h2 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
<div className="w-10 h-10 rounded-full flex items-center justify-center" style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
<Bot className="w-6 h-6" style={{ color: '#000' }} />
</div>
<span className="w-10 h-10 rounded-full flex items-center justify-center text-xl" style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
🤖
</span>
{selectedTrader.trader_name}
</h2>
{/* Trader Selector */}
{traders && traders.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-sm" style={{ color: '#848E9C' }}>:</span>
<span className="text-sm" style={{ color: '#848E9C' }}>{t('switchTrader', language)}:</span>
<select
value={selectedTraderId}
onChange={(e) => onTraderSelect(e.target.value)}
@@ -467,9 +470,8 @@ function TraderDetailsPage({
{/* Debug Info */}
{account && (
<div className="mb-4 p-3 rounded text-xs font-mono" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<div className="flex items-center gap-2" style={{ color: '#848E9C' }}>
<RefreshCw className="w-3 h-3" />
Last Update: {lastUpdate} | Total Equity: {account?.total_equity?.toFixed(2) || '0.00'} |
<div style={{ color: '#848E9C' }}>
🔄 Last Update: {lastUpdate} | Total Equity: {account?.total_equity?.toFixed(2) || '0.00'} |
Available: {account?.available_balance?.toFixed(2) || '0.00'} | P&L: {account?.total_pnl?.toFixed(2) || '0.00'}{' '}
({account?.total_pnl_pct?.toFixed(2) || '0.00'}%)
</div>
@@ -515,8 +517,7 @@ function TraderDetailsPage({
<div className="binance-card p-6 animate-slide-in" style={{ animationDelay: '0.15s' }}>
<div className="flex items-center justify-between mb-5">
<h2 className="text-xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
<TrendingUp className="w-5 h-5" style={{ color: '#0ECB81' }} />
{t('currentPositions', language)}
📈 {t('currentPositions', language)}
</h2>
{positions && positions.length > 0 && (
<div className="text-xs px-3 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', color: '#F0B90B', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
@@ -580,9 +581,7 @@ function TraderDetailsPage({
</div>
) : (
<div className="text-center py-16" style={{ color: '#848E9C' }}>
<div className="mb-4 flex justify-center opacity-50">
<BarChart3 className="w-16 h-16" />
</div>
<div className="text-6xl mb-4 opacity-50">📊</div>
<div className="text-lg font-semibold mb-2">{t('noPositions', language)}</div>
<div className="text-sm">{t('noActivePositions', language)}</div>
</div>
@@ -595,11 +594,11 @@ function TraderDetailsPage({
<div className="binance-card p-6 animate-slide-in h-fit lg:sticky lg:top-24 lg:max-h-[calc(100vh-120px)]" style={{ animationDelay: '0.2s' }}>
{/* 标题 */}
<div className="flex items-center gap-3 mb-5 pb-4 border-b" style={{ borderColor: '#2B3139' }}>
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{
<div className="w-10 h-10 rounded-xl flex items-center justify-center text-xl" style={{
background: 'linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%)',
boxShadow: '0 4px 14px rgba(99, 102, 241, 0.4)'
}}>
<Brain className="w-6 h-6" style={{ color: '#FFF' }} />
🧠
</div>
<div>
<h2 className="text-xl font-bold" style={{ color: '#EAECEF' }}>{t('recentDecisions', language)}</h2>
@@ -619,9 +618,7 @@ function TraderDetailsPage({
))
) : (
<div className="py-16 text-center">
<div className="mb-4 flex justify-center opacity-30">
<Brain className="w-16 h-16" style={{ color: '#8B5CF6' }} />
</div>
<div className="text-6xl mb-4 opacity-30">🧠</div>
<div className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>{t('noDecisionsYet', language)}</div>
<div className="text-sm" style={{ color: '#848E9C' }}>{t('aiDecisionsWillAppear', language)}</div>
</div>
@@ -660,11 +657,10 @@ function StatCard({
{change !== undefined && (
<div className="flex items-center gap-1">
<div
className="text-sm mono font-bold flex items-center gap-1"
className="text-sm mono font-bold"
style={{ color: positive ? '#0ECB81' : '#F6465D' }}
>
{positive ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />}
{positive ? '+' : ''}
{positive ? '▲' : '▼'} {positive ? '+' : ''}
{change.toFixed(2)}%
</div>
</div>
@@ -708,8 +704,7 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
className="flex items-center gap-2 text-sm transition-colors"
style={{ color: '#60a5fa' }}
>
<Download className="w-4 h-4" />
<span className="font-semibold">{t('inputPrompt', language)}</span>
<span className="font-semibold">📥 {t('inputPrompt', language)}</span>
<span className="text-xs">{showInputPrompt ? t('collapse', language) : t('expand', language)}</span>
</button>
{showInputPrompt && (
@@ -728,8 +723,7 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
className="flex items-center gap-2 text-sm transition-colors"
style={{ color: '#F0B90B' }}
>
<Upload className="w-4 h-4" />
<span className="font-semibold">{t('aiThinking', language)}</span>
<span className="font-semibold">📤 {t('aiThinking', language)}</span>
<span className="text-xs">{showCoT ? t('collapse', language) : t('expand', language)}</span>
</button>
{showCoT && (
@@ -759,11 +753,9 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
{action.price > 0 && (
<span className="font-mono text-xs" style={{ color: '#848E9C' }}>@{action.price.toFixed(4)}</span>
)}
{action.success ? (
<Check className="w-4 h-4" style={{ color: '#0ECB81' }} />
) : (
<X className="w-4 h-4" style={{ color: '#F6465D' }} />
)}
<span style={{ color: action.success ? '#0ECB81' : '#F6465D' }}>
{action.success ? '✓' : '✗'}
</span>
{action.error && <span className="text-xs ml-2" style={{ color: '#F6465D' }}>{action.error}</span>}
</div>
))}
@@ -797,9 +789,8 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
{/* Error Message */}
{decision.error_message && (
<div className="text-sm rounded px-3 py-2 mt-3 flex items-center gap-2" style={{ color: '#F6465D', background: 'rgba(246, 70, 93, 0.1)' }}>
<AlertCircle className="w-4 h-4" />
{decision.error_message}
<div className="text-sm rounded px-3 py-2 mt-3" style={{ color: '#F6465D', background: 'rgba(246, 70, 93, 0.1)' }}>
{decision.error_message}
</div>
)}
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -14,13 +14,15 @@ import useSWR from 'swr';
import { api } from '../lib/api';
import type { CompetitionTraderData } from '../types';
import { getTraderColor } from '../utils/traderColors';
import { BarChart3 } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations';
interface ComparisonChartProps {
traders: CompetitionTraderData[];
}
export function ComparisonChart({ traders }: ComparisonChartProps) {
const { language } = useLanguage();
// 获取所有trader的历史数据 - 使用单个useSWR并发请求所有trader数据
// 生成唯一的key当traders变化时会触发重新请求
const tradersKey = traders.map(t => t.trader_id).sort().join(',');
@@ -134,11 +136,9 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
if (combinedData.length === 0) {
return (
<div className="text-center py-16" style={{ color: '#848E9C' }}>
<div className="mb-4 flex justify-center opacity-50">
<BarChart3 className="w-16 h-16" />
</div>
<div className="text-lg font-semibold mb-2"></div>
<div className="text-sm">线</div>
<div className="text-6xl mb-4 opacity-50">📊</div>
<div className="text-lg font-semibold mb-2">{t('noHistoricalData', language)}</div>
<div className="text-sm">{t('dataWillAppear', language)}</div>
</div>
);
}
@@ -314,25 +314,25 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{/* Stats */}
<div className="mt-6 grid grid-cols-4 gap-4 pt-5" style={{ borderTop: '1px solid #2B3139' }}>
<div className="p-3 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' }}></div>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('comparisonMode', language)}</div>
<div className="text-base font-bold" style={{ color: '#EAECEF' }}>PnL %</div>
</div>
<div className="p-3 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' }}></div>
<div className="text-base font-bold mono" style={{ color: '#EAECEF' }}>{combinedData.length} </div>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('dataPoints', language)}</div>
<div className="text-base font-bold mono" style={{ color: '#EAECEF' }}>{t('count', language, {count: combinedData.length})}</div>
</div>
<div className="p-3 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' }}></div>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('currentGap', language)}</div>
<div className="text-base font-bold mono" style={{ color: currentGap > 1 ? '#F0B90B' : '#EAECEF' }}>
{currentGap.toFixed(2)}%
</div>
</div>
<div className="p-3 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' }}></div>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('displayRange', language)}</div>
<div className="text-base font-bold mono" style={{ color: '#EAECEF' }}>
{combinedData.length > MAX_DISPLAY_POINTS
? `最近 ${MAX_DISPLAY_POINTS}`
: '全部数据'}
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
: t('allData', language)}
</div>
</div>
</div>

View File

@@ -1,11 +1,18 @@
import { useState } from 'react';
import useSWR from 'swr';
import { api } from '../lib/api';
import type { CompetitionData } from '../types';
import { ComparisonChart } from './ComparisonChart';
import { TraderConfigModal } from './TraderConfigModal';
import { getTraderColor } from '../utils/traderColors';
import { Trophy, Medal, Circle, CircleDot } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations';
export function CompetitionPage() {
const { language } = useLanguage();
const [selectedTrader, setSelectedTrader] = useState<any>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const { data: competition } = useSWR<CompetitionData>(
'competition',
api.getCompetition,
@@ -16,6 +23,21 @@ export function CompetitionPage() {
}
);
const handleTraderClick = async (traderId: string) => {
try {
const traderConfig = await api.getTraderConfig(traderId);
setSelectedTrader(traderConfig);
setIsModalOpen(true);
} catch (error) {
console.error('Failed to fetch trader config:', error);
}
};
const closeModal = () => {
setIsModalOpen(false);
setSelectedTrader(null);
};
if (!competition || !competition.traders) {
return (
<div className="space-y-6">
@@ -52,26 +74,26 @@ export function CompetitionPage() {
{/* Competition Header - 精简版 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl flex items-center justify-center" style={{
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl" style={{
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)'
}}>
<Trophy className="w-7 h-7" style={{ color: '#000' }} />
🏆
</div>
<div>
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
AI竞赛
{t('aiCompetition', language)}
<span className="text-xs font-normal px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}>
{competition.count}
{competition.count} {t('traders', language)}
</span>
</h1>
<p className="text-xs" style={{ color: '#848E9C' }}>
{t('liveBattle', language)}
</p>
</div>
</div>
<div className="text-right">
<div className="text-xs mb-1" style={{ color: '#848E9C' }}></div>
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('leader', language)}</div>
<div className="text-lg font-bold" style={{ color: '#F0B90B' }}>{leader?.trader_name}</div>
<div className="text-sm font-semibold" style={{ color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
@@ -85,10 +107,10 @@ export function CompetitionPage() {
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
{t('performanceComparison', language)}
</h2>
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('realTimePnL', language)}
</div>
</div>
<ComparisonChart traders={sortedTraders} />
@@ -98,10 +120,10 @@ export function CompetitionPage() {
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
{t('leaderboard', language)}
</h2>
<div className="text-xs px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', color: '#F0B90B', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
{t('live', language)}
</div>
</div>
<div className="space-y-2">
@@ -112,7 +134,8 @@ export function CompetitionPage() {
return (
<div
key={trader.trader_id}
className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px]"
onClick={() => handleTraderClick(trader.trader_id)}
className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px] cursor-pointer hover:shadow-lg"
style={{
background: isLeader ? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, #0B0E11 100%)' : '#0B0E11',
border: `1px solid ${isLeader ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
@@ -122,17 +145,13 @@ export function CompetitionPage() {
<div className="flex items-center justify-between">
{/* Rank & Name */}
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full flex items-center justify-center" style={{
background: index === 0 ? 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)' :
index === 1 ? 'linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%)' :
'linear-gradient(135deg, #CD7F32 0%, #8B4513 100%)'
}}>
<Medal className="w-5 h-5" style={{ color: '#000' }} />
<div className="text-2xl w-6">
{index === 0 ? '🥇' : index === 1 ? '🥈' : '🥉'}
</div>
<div>
<div className="font-bold text-sm" style={{ color: '#EAECEF' }}>{trader.trader_name}</div>
<div className="text-xs mono font-semibold" style={{ color: traderColor }}>
{trader.ai_model.toUpperCase()}
{trader.ai_model.toUpperCase()} + {trader.exchange.toUpperCase()}
</div>
</div>
</div>
@@ -141,7 +160,7 @@ export function CompetitionPage() {
<div className="flex items-center gap-3">
{/* Total Equity */}
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}></div>
<div className="text-xs" style={{ color: '#848E9C' }}>{t('equity', language)}</div>
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
{trader.total_equity?.toFixed(2) || '0.00'}
</div>
@@ -149,7 +168,7 @@ export function CompetitionPage() {
{/* P&L */}
<div className="text-right min-w-[90px]">
<div className="text-xs" style={{ color: '#848E9C' }}></div>
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pnl', language)}</div>
<div
className="text-lg font-bold mono"
style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}
@@ -164,7 +183,7 @@ export function CompetitionPage() {
{/* Positions */}
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}></div>
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pos', language)}</div>
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
{trader.position_count}
</div>
@@ -176,17 +195,13 @@ export function CompetitionPage() {
{/* Status */}
<div>
<div
className="px-2 py-1 rounded text-xs font-bold flex items-center justify-center"
className="px-2 py-1 rounded text-xs font-bold"
style={trader.is_running
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
}
>
{trader.is_running ? (
<CircleDot className="w-3 h-3" />
) : (
<Circle className="w-3 h-3" />
)}
{trader.is_running ? '●' : '○'}
</div>
</div>
</div>
@@ -202,7 +217,7 @@ export function CompetitionPage() {
{competition.traders.length === 2 && (
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.3s' }}>
<h2 className="text-lg font-bold mb-4 flex items-center gap-2" style={{ color: '#EAECEF' }}>
{t('headToHead', language)}
</h2>
<div className="grid grid-cols-2 gap-4">
{sortedTraders.map((trader, index) => {
@@ -229,22 +244,25 @@ export function CompetitionPage() {
>
<div className="text-center">
<div
className="text-base font-bold mb-2"
className="text-base font-bold mb-1"
style={{ color: getTraderColor(sortedTraders, trader.trader_id) }}
>
{trader.trader_name}
</div>
<div className="text-xs mono mb-2" style={{ color: '#848E9C' }}>
{trader.ai_model.toUpperCase()} + {trader.exchange.toUpperCase()}
</div>
<div className="text-2xl font-bold mono mb-1" style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
</div>
{isWinning && gap > 0 && (
<div className="text-xs font-semibold" style={{ color: '#0ECB81' }}>
{gap.toFixed(2)}%
{t('leadingBy', language, { gap: gap.toFixed(2) })}
</div>
)}
{!isWinning && gap < 0 && (
<div className="text-xs font-semibold" style={{ color: '#F6465D' }}>
{Math.abs(gap).toFixed(2)}%
{t('behindBy', language, { gap: Math.abs(gap).toFixed(2) })}
</div>
)}
</div>
@@ -254,6 +272,13 @@ export function CompetitionPage() {
</div>
</div>
)}
{/* Trader Config Modal */}
<TraderConfigModal
isOpen={isModalOpen}
onClose={closeModal}
traderData={selectedTrader}
/>
</div>
);
}

View File

@@ -14,8 +14,8 @@ export function Header({ simple = false }: HeaderProps) {
<div className="flex items-center justify-between">
{/* Left - Logo and Title */}
<div className="flex items-center gap-3">
<div className="w-8 h-8 flex items-center justify-center">
<img src="/images/logo.png" alt="NoFx Logo" className="w-full h-full object-contain" />
<div className="flex items-center justify-center">
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="h-10 w-auto" />
</div>
<div>
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>

View File

@@ -3,7 +3,6 @@ import { useAuth } from '../contexts/AuthContext';
import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations';
import { Header } from './Header';
import { Smartphone } from 'lucide-react';
export function LoginPage() {
const { language } = useLanguage();
@@ -59,7 +58,7 @@ export function LoginPage() {
{/* Logo */}
<div className="text-center mb-8">
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<img src="/images/logo.png" alt="NoFx Logo" className="w-full h-full object-contain" />
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-16 h-16" />
</div>
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
{t('loginTitle', language)}
@@ -121,9 +120,7 @@ export function LoginPage() {
) : (
<form onSubmit={handleOTPVerify} className="space-y-4">
<div className="text-center mb-4">
<div className="mb-2 flex justify-center">
<Smartphone className="w-10 h-10" style={{ color: '#F0B90B' }} />
</div>
<div className="text-4xl mb-2">📱</div>
<p className="text-sm" style={{ color: '#848E9C' }}>
{t('scanQRCodeInstructions', language)}<br />
{t('enterOTPCode', language)}

View File

@@ -2,7 +2,6 @@ import React, { useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations';
import { Smartphone, Lock } from 'lucide-react';
export function RegisterPage() {
const { language } = useLanguage();
@@ -77,7 +76,7 @@ export function RegisterPage() {
{/* Logo */}
<div className="text-center mb-8">
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<img src="/images/logo.png" alt="NoFx Logo" className="w-full h-full object-contain" />
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-16 h-16" />
</div>
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
{t('appTitle', language)}
@@ -158,9 +157,7 @@ export function RegisterPage() {
{step === 'setup-otp' && (
<div className="space-y-4">
<div className="text-center">
<div className="mb-2 flex justify-center">
<Smartphone className="w-10 h-10" style={{ color: '#F0B90B' }} />
</div>
<div className="text-4xl mb-2">📱</div>
<h3 className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('setupTwoFactor', language)}
</h3>
@@ -238,9 +235,7 @@ export function RegisterPage() {
{step === 'verify-otp' && (
<form onSubmit={handleOTPVerify} className="space-y-4">
<div className="text-center mb-4">
<div className="mb-2 flex justify-center">
<Lock className="w-10 h-10" style={{ color: '#F0B90B' }} />
</div>
<div className="text-4xl mb-2">🔐</div>
<p className="text-sm" style={{ color: '#848E9C' }}>
{t('enterOTPCode', language)}<br />
{t('completeRegistrationSubtitle', language)}

View File

@@ -0,0 +1,443 @@
import { useState, useEffect } from 'react';
import type { AIModel, Exchange, CreateTraderRequest } from '../types';
// 提取下划线后面的名称部分
function getShortName(fullName: string): string {
const parts = fullName.split('_');
return parts.length > 1 ? parts[parts.length - 1] : fullName;
}
interface TraderConfigData {
trader_id?: string;
trader_name: string;
ai_model: string;
exchange_id: string;
btc_eth_leverage: number;
altcoin_leverage: number;
trading_symbols: string;
custom_prompt: string;
override_base_prompt: boolean;
is_cross_margin: boolean;
use_coin_pool: boolean;
use_oi_top: boolean;
initial_balance: number;
}
interface TraderConfigModalProps {
isOpen: boolean;
onClose: () => void;
traderData?: TraderConfigData | null;
isEditMode?: boolean;
availableModels?: AIModel[];
availableExchanges?: Exchange[];
onSave?: (data: CreateTraderRequest) => Promise<void>;
}
export function TraderConfigModal({
isOpen,
onClose,
traderData,
isEditMode = false,
availableModels = [],
availableExchanges = [],
onSave
}: TraderConfigModalProps) {
const [formData, setFormData] = useState<TraderConfigData>({
trader_name: '',
ai_model: '',
exchange_id: '',
btc_eth_leverage: 5,
altcoin_leverage: 3,
trading_symbols: '',
custom_prompt: '',
override_base_prompt: false,
is_cross_margin: true,
use_coin_pool: false,
use_oi_top: false,
initial_balance: 1000,
});
const [isSaving, setIsSaving] = useState(false);
const [availableCoins, setAvailableCoins] = useState<string[]>([]);
const [selectedCoins, setSelectedCoins] = useState<string[]>([]);
const [showCoinSelector, setShowCoinSelector] = useState(false);
useEffect(() => {
if (traderData) {
setFormData(traderData);
// 设置已选择的币种
if (traderData.trading_symbols) {
const coins = traderData.trading_symbols.split(',').map(s => s.trim()).filter(s => s);
setSelectedCoins(coins);
}
} else if (!isEditMode) {
setFormData({
trader_name: '',
ai_model: availableModels[0]?.id || '',
exchange_id: availableExchanges[0]?.id || '',
btc_eth_leverage: 5,
altcoin_leverage: 3,
trading_symbols: '',
custom_prompt: '',
override_base_prompt: false,
is_cross_margin: true,
use_coin_pool: false,
use_oi_top: false,
initial_balance: 1000,
});
}
}, [traderData, isEditMode, availableModels, availableExchanges]);
// 获取系统配置中的币种列表
useEffect(() => {
const fetchConfig = async () => {
try {
const response = await fetch('/api/config');
const config = await response.json();
if (config.default_coins) {
setAvailableCoins(config.default_coins);
}
} catch (error) {
console.error('Failed to fetch config:', error);
// 使用默认币种列表
setAvailableCoins(['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT', 'DOGEUSDT', 'ADAUSDT']);
}
};
fetchConfig();
}, []);
// 当选择的币种改变时,更新输入框
useEffect(() => {
const symbolsString = selectedCoins.join(',');
setFormData(prev => ({ ...prev, trading_symbols: symbolsString }));
}, [selectedCoins]);
if (!isOpen) return null;
const handleInputChange = (field: keyof TraderConfigData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 如果是直接编辑trading_symbols同步更新selectedCoins
if (field === 'trading_symbols') {
const coins = value.split(',').map((s: string) => s.trim()).filter((s: string) => s);
setSelectedCoins(coins);
}
};
const handleCoinToggle = (coin: string) => {
setSelectedCoins(prev => {
if (prev.includes(coin)) {
return prev.filter(c => c !== coin);
} else {
return [...prev, coin];
}
});
};
const handleSave = async () => {
if (!onSave) return;
setIsSaving(true);
try {
const saveData: CreateTraderRequest = {
name: formData.trader_name,
ai_model_id: formData.ai_model,
exchange_id: formData.exchange_id,
btc_eth_leverage: formData.btc_eth_leverage,
altcoin_leverage: formData.altcoin_leverage,
trading_symbols: formData.trading_symbols,
custom_prompt: formData.custom_prompt,
override_base_prompt: formData.override_base_prompt,
is_cross_margin: formData.is_cross_margin,
use_coin_pool: formData.use_coin_pool,
use_oi_top: formData.use_oi_top,
initial_balance: formData.initial_balance,
};
await onSave(saveData);
onClose();
} catch (error) {
console.error('保存失败:', error);
} finally {
setIsSaving(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm">
<div
className="bg-[#1E2329] border border-[#2B3139] rounded-xl shadow-2xl max-w-3xl w-full mx-4 max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[#F0B90B] to-[#E1A706] flex items-center justify-center">
<span className="text-lg">{isEditMode ? '✏️' : ''}</span>
</div>
<div>
<h2 className="text-xl font-bold text-[#EAECEF]">
{isEditMode ? '修改交易员' : '创建交易员'}
</h2>
<p className="text-sm text-[#848E9C] mt-1">
{isEditMode ? '修改交易员配置参数' : '配置新的AI交易员'}
</p>
</div>
</div>
<button
onClick={onClose}
className="w-8 h-8 rounded-lg text-[#848E9C] hover:text-[#EAECEF] hover:bg-[#2B3139] transition-colors flex items-center justify-center"
>
</button>
</div>
{/* Content */}
<div className="p-6 space-y-8">
{/* Basic Info */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
🤖
</h3>
<div className="space-y-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2"></label>
<input
type="text"
value={formData.trader_name}
onChange={(e) => handleInputChange('trader_name', e.target.value)}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
placeholder="请输入交易员名称"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2">AI模型</label>
<select
value={formData.ai_model}
onChange={(e) => handleInputChange('ai_model', e.target.value)}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
>
{availableModels.map(model => (
<option key={model.id} value={model.id}>
{getShortName(model.name || model.id).toUpperCase()}
</option>
))}
</select>
</div>
<div>
<label className="text-sm text-[#EAECEF] block mb-2"></label>
<select
value={formData.exchange_id}
onChange={(e) => handleInputChange('exchange_id', e.target.value)}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
>
{availableExchanges.map(exchange => (
<option key={exchange.id} value={exchange.id}>
{getShortName(exchange.name || exchange.id).toUpperCase()}
</option>
))}
</select>
</div>
</div>
</div>
</div>
{/* Trading Configuration */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
</h3>
<div className="space-y-4">
{/* 第一行:保证金模式和初始余额 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2"></label>
<div className="flex gap-2">
<button
type="button"
onClick={() => handleInputChange('is_cross_margin', true)}
className={`flex-1 px-3 py-2 rounded text-sm ${
formData.is_cross_margin
? 'bg-[#F0B90B] text-black'
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
}`}
>
</button>
<button
type="button"
onClick={() => handleInputChange('is_cross_margin', false)}
className={`flex-1 px-3 py-2 rounded text-sm ${
!formData.is_cross_margin
? 'bg-[#F0B90B] text-black'
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
}`}
>
</button>
</div>
</div>
<div>
<label className="text-sm text-[#EAECEF] block mb-2"> ($)</label>
<input
type="number"
value={formData.initial_balance}
onChange={(e) => handleInputChange('initial_balance', Number(e.target.value))}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
min="100"
step="100"
/>
</div>
</div>
{/* 第二行:杠杆设置 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2">BTC/ETH </label>
<input
type="number"
value={formData.btc_eth_leverage}
onChange={(e) => handleInputChange('btc_eth_leverage', Number(e.target.value))}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
min="1"
max="125"
/>
</div>
<div>
<label className="text-sm text-[#EAECEF] block mb-2"></label>
<input
type="number"
value={formData.altcoin_leverage}
onChange={(e) => handleInputChange('altcoin_leverage', Number(e.target.value))}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
min="1"
max="75"
/>
</div>
</div>
{/* 第三行:交易币种 */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm text-[#EAECEF]"> (使)</label>
<button
type="button"
onClick={() => setShowCoinSelector(!showCoinSelector)}
className="px-3 py-1 text-xs bg-[#F0B90B] text-black rounded hover:bg-[#E1A706] transition-colors"
>
{showCoinSelector ? '收起选择' : '快速选择'}
</button>
</div>
<input
type="text"
value={formData.trading_symbols}
onChange={(e) => handleInputChange('trading_symbols', e.target.value)}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
placeholder="例如: BTCUSDT,ETHUSDT,ADAUSDT"
/>
{/* 币种选择器 */}
{showCoinSelector && (
<div className="mt-3 p-3 bg-[#0B0E11] border border-[#2B3139] rounded">
<div className="text-xs text-[#848E9C] mb-2"></div>
<div className="flex flex-wrap gap-2">
{availableCoins.map(coin => (
<button
key={coin}
type="button"
onClick={() => handleCoinToggle(coin)}
className={`px-2 py-1 text-xs rounded transition-colors ${
selectedCoins.includes(coin)
? 'bg-[#F0B90B] text-black'
: 'bg-[#1E2329] text-[#848E9C] border border-[#2B3139] hover:border-[#F0B90B]'
}`}
>
{coin.replace('USDT', '')}
</button>
))}
</div>
</div>
)}
</div>
</div>
</div>
{/* Signal Sources */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
📡
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center gap-3">
<input
type="checkbox"
checked={formData.use_coin_pool}
onChange={(e) => handleInputChange('use_coin_pool', e.target.checked)}
className="w-4 h-4"
/>
<label className="text-sm text-[#EAECEF]">使 Coin Pool </label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
checked={formData.use_oi_top}
onChange={(e) => handleInputChange('use_oi_top', e.target.checked)}
className="w-4 h-4"
/>
<label className="text-sm text-[#EAECEF]">使 OI Top </label>
</div>
</div>
</div>
{/* Trading Prompt */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
💬
</h3>
<div className="space-y-4">
<div className="flex items-center gap-3">
<input
type="checkbox"
checked={formData.override_base_prompt}
onChange={(e) => handleInputChange('override_base_prompt', e.target.checked)}
className="w-4 h-4"
/>
<label className="text-sm text-[#EAECEF]"></label>
<span className="text-xs text-[#F0B90B]"> </span>
</div>
<div>
<label className="text-sm text-[#EAECEF] block mb-2">
{formData.override_base_prompt ? '自定义提示词' : '附加提示词'}
</label>
<textarea
value={formData.custom_prompt}
onChange={(e) => handleInputChange('custom_prompt', e.target.value)}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none h-24 resize-none"
placeholder={formData.override_base_prompt ? "输入完整的交易策略提示词..." : "输入额外的交易策略提示..."}
/>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="flex justify-end gap-3 p-6 border-t border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
<button
onClick={onClose}
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
>
</button>
{onSave && (
<button
onClick={handleSave}
disabled={isSaving || !formData.trader_name || !formData.ai_model || !formData.exchange_id}
className="px-8 py-3 bg-gradient-to-r from-[#F0B90B] to-[#E1A706] text-black rounded-lg hover:from-[#E1A706] hover:to-[#D4951E] transition-all duration-200 disabled:bg-[#848E9C] disabled:cursor-not-allowed font-medium shadow-lg"
>
{isSaving ? '保存中...' : (isEditMode ? '保存修改' : '创建交易员')}
</button>
)}
</div>
</div>
</div>
);
}

View File

@@ -3,7 +3,7 @@ export type Language = 'en' | 'zh';
export const translations = {
en: {
// Header
appTitle: 'AI Trading System',
appTitle: 'NOFX',
subtitle: 'Multi-AI Model Trading Platform',
aiTraders: 'AI Traders',
details: 'Details',
@@ -64,11 +64,17 @@ export const translations = {
recent: 'Recent',
allData: 'All Data',
cycles: 'Cycles',
// Comparison Chart
comparisonMode: 'Comparison Mode',
dataPoints: 'Data Points',
currentGap: 'Current Gap',
count: '{count} pts',
// Competition Page
aiCompetition: 'AI Competition',
traders: 'traders',
liveBattle: 'Qwen vs DeepSeek · Live Battle',
liveBattle: 'Live Battle',
leader: 'Leader',
leaderboard: 'Leaderboard',
live: 'LIVE',
@@ -147,6 +153,38 @@ export const translations = {
useTestnet: 'Use Testnet',
enabled: 'Enabled',
save: 'Save',
// AI Model Configuration
officialAPI: 'Official API',
customAPI: 'Custom API',
apiKey: 'API Key',
customAPIURL: 'Custom API URL',
enterAPIKey: 'Enter API Key',
enterCustomAPIURL: 'Enter custom API endpoint URL',
useOfficialAPI: 'Use official API service',
useCustomAPI: 'Use custom API endpoint',
// Trader Configuration
positionMode: 'Position Mode',
crossMarginMode: 'Cross Margin',
isolatedMarginMode: 'Isolated Margin',
crossMarginDescription: 'Cross margin: All positions share account balance as collateral',
isolatedMarginDescription: 'Isolated margin: Each position manages collateral independently, risk isolation',
leverageConfiguration: 'Leverage Configuration',
btcEthLeverage: 'BTC/ETH Leverage',
altcoinLeverage: 'Altcoin Leverage',
leverageRecommendation: 'Recommended: BTC/ETH 5-10x, Altcoins 3-5x for risk control',
tradingSymbols: 'Trading Symbols',
tradingSymbolsPlaceholder: 'Enter symbols, comma separated (e.g., BTCUSDT,ETHUSDT,SOLUSDT)',
selectSymbols: 'Select Symbols',
selectTradingSymbols: 'Select Trading Symbols',
selectedSymbolsCount: 'Selected {count} symbols',
clearSelection: 'Clear All',
confirmSelection: 'Confirm',
tradingSymbolsDescription: 'Empty = use default symbols. Must end with USDT (e.g., BTCUSDT, ETHUSDT)',
btcEthLeverageValidation: 'BTC/ETH leverage must be between 1-50x',
altcoinLeverageValidation: 'Altcoin leverage must be between 1-20x',
invalidSymbolFormat: 'Invalid symbol format: {symbol}, must end with USDT',
// Loading & Error
loading: 'Loading...',
@@ -207,7 +245,7 @@ export const translations = {
},
zh: {
// Header
appTitle: 'AI交易系统',
appTitle: 'NOFX',
subtitle: '多AI模型交易平台',
aiTraders: 'AI交易员',
details: '详情',
@@ -268,22 +306,28 @@ export const translations = {
recent: '最近',
allData: '全部数据',
cycles: '个',
// Comparison Chart
comparisonMode: '对比模式',
dataPoints: '数据点数',
currentGap: '当前差距',
count: '{count} 个',
// Competition Page
aiCompetition: 'AI竞赛',
traders: '交易',
liveBattle: 'Qwen vs DeepSeek · 实时对战',
leader: '🥇 领先者',
leaderboard: '🥇 排行榜',
live: '直播',
performanceComparison: '📈 表现对比',
realTimePnL: '实时盈亏百分比',
headToHead: '⚔️ 正面对决',
traders: '交易',
liveBattle: '实时对战',
leader: '领先者',
leaderboard: '排行榜',
live: '实时',
performanceComparison: '表现对比',
realTimePnL: '实时收益率',
headToHead: '正面对决',
leadingBy: '领先 {gap}%',
behindBy: '落后 {gap}%',
equity: '净值',
pnl: '盈亏',
pos: '仓',
equity: '权益',
pnl: '收益',
pos: '仓',
// AI Learning
aiLearning: 'AI学习与反思',
@@ -351,6 +395,38 @@ export const translations = {
useTestnet: '使用测试网',
enabled: '启用',
save: '保存',
// AI Model Configuration
officialAPI: '官方API',
customAPI: '自定义API',
apiKey: 'API密钥',
customAPIURL: '自定义API地址',
enterAPIKey: '请输入API密钥',
enterCustomAPIURL: '请输入自定义API端点地址',
useOfficialAPI: '使用官方API服务',
useCustomAPI: '使用自定义API端点',
// Trader Configuration
positionMode: '仓位模式',
crossMarginMode: '全仓模式',
isolatedMarginMode: '逐仓模式',
crossMarginDescription: '全仓模式:所有仓位共享账户余额作为保证金',
isolatedMarginDescription: '逐仓模式:每个仓位独立管理保证金,风险隔离',
leverageConfiguration: '杠杆配置',
btcEthLeverage: 'BTC/ETH杠杆',
altcoinLeverage: '山寨币杠杆',
leverageRecommendation: '推荐BTC/ETH 5-10倍山寨币 3-5倍控制风险',
tradingSymbols: '交易币种',
tradingSymbolsPlaceholder: '输入币种逗号分隔BTCUSDT,ETHUSDT,SOLUSDT',
selectSymbols: '选择币种',
selectTradingSymbols: '选择交易币种',
selectedSymbolsCount: '已选择 {count} 个币种',
clearSelection: '清空选择',
confirmSelection: '确认选择',
tradingSymbolsDescription: '留空 = 使用默认币种。必须以USDT结尾BTCUSDT, ETHUSDT',
btcEthLeverageValidation: 'BTC/ETH杠杆必须在1-50倍之间',
altcoinLeverageValidation: '山寨币杠杆必须在1-20倍之间',
invalidSymbolFormat: '无效的币种格式:{symbol}必须以USDT结尾',
// Loading & Error
loading: '加载中...',

View File

@@ -82,6 +82,24 @@ export const api = {
if (!res.ok) throw new Error('更新自定义策略失败');
},
async getTraderConfig(traderId: string): Promise<any> {
const res = await fetch(`${API_BASE}/traders/${traderId}/config`, {
headers: getAuthHeaders(),
});
if (!res.ok) throw new Error('获取交易员配置失败');
return res.json();
},
async updateTrader(traderId: string, request: CreateTraderRequest): Promise<TraderInfo> {
const res = await fetch(`${API_BASE}/traders/${traderId}`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(request),
});
if (!res.ok) throw new Error('更新交易员失败');
return res.json();
},
// AI模型配置接口
async getModelConfigs(): Promise<AIModel[]> {
const res = await fetch(`${API_BASE}/models`, {
@@ -242,4 +260,25 @@ export const api = {
if (!res.ok) throw new Error('获取竞赛数据失败');
return res.json();
},
// 用户信号源配置接口
async getUserSignalSource(): Promise<{coin_pool_url: string, oi_top_url: string}> {
const res = await fetch(`${API_BASE}/user/signal-sources`, {
headers: getAuthHeaders(),
});
if (!res.ok) throw new Error('获取用户信号源配置失败');
return res.json();
},
async saveUserSignalSource(coinPoolUrl: string, oiTopUrl: string): Promise<void> {
const res = await fetch(`${API_BASE}/user/signal-sources`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
coin_pool_url: coinPoolUrl,
oi_top_url: oiTopUrl,
}),
});
if (!res.ok) throw new Error('保存用户信号源配置失败');
},
};

View File

@@ -100,6 +100,7 @@ export interface AIModel {
provider: string;
enabled: boolean;
apiKey?: string;
customApiUrl?: string;
}
export interface Exchange {
@@ -123,9 +124,14 @@ export interface CreateTraderRequest {
ai_model_id: string;
exchange_id: string;
initial_balance: number;
btc_eth_leverage?: number;
altcoin_leverage?: number;
trading_symbols?: string;
custom_prompt?: string;
override_base_prompt?: boolean;
is_cross_margin?: boolean;
use_coin_pool?: boolean;
use_oi_top?: boolean;
}
export interface UpdateModelConfigRequest {
@@ -133,6 +139,7 @@ export interface UpdateModelConfigRequest {
[key: string]: {
enabled: boolean;
api_key: string;
custom_api_url?: string;
};
};
}
@@ -159,6 +166,7 @@ export interface CompetitionTraderData {
trader_id: string;
trader_name: string;
ai_model: string;
exchange: string;
total_equity: number;
total_pnl: number;
total_pnl_pct: number;