diff --git a/api/server.go b/api/server.go index ced629d7..e65a8e90 100644 --- a/api/server.go +++ b/api/server.go @@ -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), diff --git a/config.json.example b/config.json.example index 6b0a32e1..ac9d5ac6 100644 --- a/config.json.example +++ b/config.json.example @@ -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, diff --git a/config/config.go b/config/config.go index 75298663..97fcc84d 100644 --- a/config/config.go +++ b/config/config.go @@ -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 } diff --git a/config/database.go b/config/database.go index 20b08d5f..70da76b6 100644 --- a/config/database.go +++ b/config/database.go @@ -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() diff --git a/main.go b/main.go index 547c0677..e4179872 100644 --- a/main.go +++ b/main.go @@ -192,26 +192,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) } } diff --git a/manager/trader_manager.go b/manager/trader_manager.go index f6253a5d..014eefd1 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -1,11 +1,13 @@ package manager import ( + "encoding/json" "fmt" "log" "nofx/config" "nofx/trader" "strconv" + "strings" "sync" "time" ) @@ -28,26 +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") + defaultCoinsStr, _ := database.GetSystemConfig("default_coins") // 解析配置 maxDailyLoss := 10.0 // 默认值 @@ -65,10 +74,19 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro stopTradingMinutes = 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 @@ -92,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 @@ -117,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) + err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins) if err != nil { log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err) continue @@ -130,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 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, @@ -145,16 +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: traderCfg.BTCETHLeverage, + AltcoinLeverage: traderCfg.AltcoinLeverage, MaxDailyLoss: maxDailyLoss, MaxDrawdown: maxDrawdown, StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute, IsCrossMargin: traderCfg.IsCrossMargin, + DefaultCoins: defaultCoins, + TradingCoins: tradingCoins, } // 根据交易所类型设置API密钥 @@ -202,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 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() @@ -210,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, @@ -220,16 +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: traderCfg.BTCETHLeverage, + AltcoinLeverage: traderCfg.AltcoinLeverage, MaxDailyLoss: maxDailyLoss, MaxDrawdown: maxDrawdown, StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute, IsCrossMargin: traderCfg.IsCrossMargin, + DefaultCoins: defaultCoins, + TradingCoins: tradingCoins, } // 根据交易所类型设置API密钥 @@ -357,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"], @@ -373,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) @@ -458,11 +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") + 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 // 默认值 @@ -480,6 +590,15 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin stopTradingMinutes = 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 { // 检查是否已经加载过这个交易员 @@ -539,7 +658,7 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin } // 使用现有的方法加载交易员 - err = tm.loadSingleTrader(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, maxDailyLoss, maxDrawdown, stopTradingMinutes) + err = tm.loadSingleTrader(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins) if err != nil { log.Printf("⚠️ 加载交易员 %s 失败: %v", traderCfg.Name, err) } @@ -549,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 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, @@ -557,12 +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, + CoinPoolAPIURL: effectiveCoinPoolURL, MaxDailyLoss: maxDailyLoss, MaxDrawdown: maxDrawdown, StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute, IsCrossMargin: traderCfg.IsCrossMargin, + DefaultCoins: defaultCoins, + TradingCoins: tradingCoins, } // 根据交易所类型设置API密钥 diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 93889f12..35472ab5 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -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 +} diff --git a/web/index.html b/web/index.html index 0c8675ba..badfe608 100644 --- a/web/index.html +++ b/web/index.html @@ -2,7 +2,7 @@ - + NOFX - AI Auto Trading Dashboard diff --git a/web/public/icons/nofx.svg b/web/public/icons/nofx.svg new file mode 100644 index 00000000..444d5fd2 --- /dev/null +++ b/web/public/icons/nofx.svg @@ -0,0 +1,296 @@ + + + + + + + + + \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx index ddc6d028..228c87ef 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -201,8 +201,8 @@ function App() {
{/* Left - Logo and Title */}
-
- ⚡ +
+ NOFX

@@ -224,7 +224,7 @@ function App() { : { background: 'transparent', color: '#848E9C' } } > - 竞赛 + {t('aiCompetition', language)} - -

- {isCrossMargin - ? '全仓模式:所有仓位共享账户余额作为保证金' - : '逐仓模式:每个仓位独立管理保证金,风险隔离'} + 用于获取币种池数据的API地址,留空则不使用此信号源
- - {/* Advanced Settings Toggle */} -
- -
- - {/* Custom Prompt Field - Show when advanced is toggled */} - {showAdvanced && ( -
- -