diff --git a/api/server.go b/api/server.go index 0098a7a3..d83e3887 100644 --- a/api/server.go +++ b/api/server.go @@ -88,7 +88,7 @@ func (s *Server) setupRoutes() { // 系统提示词模板管理(无需认证) api.GET("/prompt-templates", s.handleGetPromptTemplates) api.GET("/prompt-templates/:name", s.handleGetPromptTemplate) - + // 公开的竞赛数据(无需认证) api.GET("/traders", s.handlePublicTraderList) api.GET("/competition", s.handlePublicCompetition) @@ -168,7 +168,7 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) { if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 { altcoinLeverage = val } - + // 获取内测模式配置 betaModeStr, _ := s.database.GetSystemConfig("beta_mode") betaMode := betaModeStr == "true" @@ -531,14 +531,14 @@ func (s *Server) handleDeleteTrader(c *gin.Context) { func (s *Server) handleStartTrader(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") - + // 校验交易员是否属于当前用户 _, _, _, err := s.database.GetTraderConfig(userID, traderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"}) return } - + trader, err := s.traderManager.GetTrader(traderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) @@ -574,14 +574,14 @@ func (s *Server) handleStartTrader(c *gin.Context) { func (s *Server) handleStopTrader(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") - + // 校验交易员是否属于当前用户 _, _, _, err := s.database.GetTraderConfig(userID, traderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"}) return } - + trader, err := s.traderManager.GetTrader(traderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) @@ -796,7 +796,7 @@ func (s *Server) handleTraderList(c *gin.Context) { result = append(result, map[string]interface{}{ "trader_id": trader.ID, "trader_name": trader.Name, - "ai_model": trader.AIModelID, // 使用完整 ID + "ai_model": trader.AIModelID, // 使用完整 ID "exchange_id": trader.ExchangeID, "is_running": isRunning, "initial_balance": trader.InitialBalance, @@ -1574,7 +1574,7 @@ func (s *Server) handlePublicCompetition(c *gin.Context) { }) return } - + c.JSON(http.StatusOK, competition) } @@ -1587,7 +1587,7 @@ func (s *Server) handleTopTraders(c *gin.Context) { }) return } - + c.JSON(http.StatusOK, topTraders) } @@ -1596,7 +1596,7 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) { var requestBody struct { TraderIDs []string `json:"trader_ids"` } - + // 尝试解析POST请求的JSON body if err := c.ShouldBindJSON(&requestBody); err != nil { // 如果JSON解析失败,尝试从query参数获取(兼容GET请求) @@ -1610,13 +1610,13 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) { }) return } - + traders, ok := topTraders["traders"].([]map[string]interface{}) if !ok { c.JSON(http.StatusInternalServerError, gin.H{"error": "交易员数据格式错误"}) return } - + // 提取trader IDs traderIDs := make([]string, 0, len(traders)) for _, trader := range traders { @@ -1624,24 +1624,24 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) { traderIDs = append(traderIDs, traderID) } } - + result := s.getEquityHistoryForTraders(traderIDs) c.JSON(http.StatusOK, result) return } - + // 解析逗号分隔的trader IDs requestBody.TraderIDs = strings.Split(traderIDsParam, ",") for i := range requestBody.TraderIDs { requestBody.TraderIDs[i] = strings.TrimSpace(requestBody.TraderIDs[i]) } } - + // 限制最多20个交易员,防止请求过大 if len(requestBody.TraderIDs) > 20 { requestBody.TraderIDs = requestBody.TraderIDs[:20] } - + result := s.getEquityHistoryForTraders(requestBody.TraderIDs) c.JSON(http.StatusOK, result) } @@ -1651,31 +1651,31 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]inter result := make(map[string]interface{}) histories := make(map[string]interface{}) errors := make(map[string]string) - + for _, traderID := range traderIDs { if traderID == "" { continue } - + trader, err := s.traderManager.GetTrader(traderID) if err != nil { errors[traderID] = "交易员不存在" continue } - + // 获取历史数据(用于对比展示,限制数据量) records, err := trader.GetDecisionLogger().GetLatestRecords(500) if err != nil { errors[traderID] = fmt.Sprintf("获取历史数据失败: %v", err) continue } - + // 构建收益率历史数据 history := make([]map[string]interface{}, 0, len(records)) for _, record := range records { // 计算总权益(余额+未实现盈亏) totalEquity := record.AccountState.TotalBalance + record.AccountState.TotalUnrealizedProfit - + history = append(history, map[string]interface{}{ "timestamp": record.Timestamp, "total_equity": totalEquity, @@ -1683,16 +1683,16 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]inter "balance": record.AccountState.TotalBalance, }) } - + histories[traderID] = history } - + result["histories"] = histories result["count"] = len(histories) if len(errors) > 0 { result["errors"] = errors } - + return result } @@ -1726,4 +1726,3 @@ func (s *Server) handleGetPublicTraderConfig(c *gin.Context) { c.JSON(http.StatusOK, result) } - diff --git a/config/database.go b/config/database.go index 651c425d..052a52ff 100644 --- a/config/database.go +++ b/config/database.go @@ -258,17 +258,17 @@ func (d *Database) initDefaultData() error { // 初始化系统配置 - 创建所有字段,设置默认值,后续由config.json同步更新 systemConfigs := map[string]string{ - "admin_mode": "true", // 默认开启管理员模式,便于首次使用 - "beta_mode": "false", // 默认关闭内测模式 - "api_server_port": "8080", // 默认API端口 - "use_default_coins": "true", // 默认使用内置币种列表 - "default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式) - "max_daily_loss": "10.0", // 最大日损失百分比 - "max_drawdown": "20.0", // 最大回撤百分比 - "stop_trading_minutes": "60", // 停止交易时间(分钟) - "btc_eth_leverage": "5", // BTC/ETH杠杆倍数 - "altcoin_leverage": "5", // 山寨币杠杆倍数 - "jwt_secret": "", // JWT密钥,默认为空,由config.json或系统生成 + "admin_mode": "true", // 默认开启管理员模式,便于首次使用 + "beta_mode": "false", // 默认关闭内测模式 + "api_server_port": "8080", // 默认API端口 + "use_default_coins": "true", // 默认使用内置币种列表 + "default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式) + "max_daily_loss": "10.0", // 最大日损失百分比 + "max_drawdown": "20.0", // 最大回撤百分比 + "stop_trading_minutes": "60", // 停止交易时间(分钟) + "btc_eth_leverage": "5", // BTC/ETH杠杆倍数 + "altcoin_leverage": "5", // 山寨币杠杆倍数 + "jwt_secret": "", // JWT密钥,默认为空,由config.json或系统生成 } for key, value := range systemConfigs { @@ -1037,7 +1037,7 @@ func (d *Database) LoadBetaCodesFromFile(filePath string) error { log.Printf("插入内测码 %s 失败: %v", code, err) continue } - + if rowsAffected, _ := result.RowsAffected(); rowsAffected > 0 { insertedCount++ } diff --git a/main.go b/main.go index 8aa83dde..9e9d1aa7 100644 --- a/main.go +++ b/main.go @@ -64,15 +64,15 @@ func syncConfigToDatabase(database *config.Database) error { // 同步各配置项到数据库 configs := map[string]string{ - "admin_mode": fmt.Sprintf("%t", configFile.AdminMode), - "beta_mode": fmt.Sprintf("%t", configFile.BetaMode), - "api_server_port": strconv.Itoa(configFile.APIServerPort), - "use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins), - "coin_pool_api_url": configFile.CoinPoolAPIURL, - "oi_top_api_url": configFile.OITopAPIURL, - "max_daily_loss": fmt.Sprintf("%.1f", configFile.MaxDailyLoss), - "max_drawdown": fmt.Sprintf("%.1f", configFile.MaxDrawdown), - "stop_trading_minutes": strconv.Itoa(configFile.StopTradingMinutes), + "admin_mode": fmt.Sprintf("%t", configFile.AdminMode), + "beta_mode": fmt.Sprintf("%t", configFile.BetaMode), + "api_server_port": strconv.Itoa(configFile.APIServerPort), + "use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins), + "coin_pool_api_url": configFile.CoinPoolAPIURL, + "oi_top_api_url": configFile.OITopAPIURL, + "max_daily_loss": fmt.Sprintf("%.1f", configFile.MaxDailyLoss), + "max_drawdown": fmt.Sprintf("%.1f", configFile.MaxDrawdown), + "stop_trading_minutes": strconv.Itoa(configFile.StopTradingMinutes), } // 同步default_coins(转换为JSON字符串存储) @@ -112,7 +112,7 @@ func syncConfigToDatabase(database *config.Database) error { // loadBetaCodesToDatabase 加载内测码文件到数据库 func loadBetaCodesToDatabase(database *config.Database) error { betaCodeFile := "beta_codes.txt" - + // 检查内测码文件是否存在 if _, err := os.Stat(betaCodeFile); os.IsNotExist(err) { log.Printf("📄 内测码文件 %s 不存在,跳过加载", betaCodeFile) @@ -126,7 +126,7 @@ func loadBetaCodesToDatabase(database *config.Database) error { } log.Printf("🔄 发现内测码文件 %s (%.1f KB),开始加载...", betaCodeFile, float64(fileInfo.Size())/1024) - + // 加载内测码到数据库 err = database.LoadBetaCodesFromFile(betaCodeFile) if err != nil { diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 4ebcf20b..ccb68bf0 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -23,9 +23,9 @@ type CompetitionCache struct { // TraderManager 管理多个trader实例 type TraderManager struct { - traders map[string]*trader.AutoTrader // key: trader ID + traders map[string]*trader.AutoTrader // key: trader ID competitionCache *CompetitionCache - mu sync.RWMutex + mu sync.RWMutex } // NewTraderManager 创建trader管理器 @@ -506,19 +506,19 @@ func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) { tm.competitionCache.mu.RUnlock() tm.mu.RLock() - + // 获取所有交易员列表 allTraders := make([]*trader.AutoTrader, 0, len(tm.traders)) for _, t := range tm.traders { allTraders = append(allTraders, t) } tm.mu.RUnlock() - + log.Printf("🔄 重新获取竞赛数据,交易员数量: %d", len(allTraders)) - + // 并发获取交易员数据 traders := tm.getConcurrentTraderData(allTraders) - + // 按收益率排序(降序) sort.Slice(traders, func(i, j int) bool { pnlPctI, okI := traders[i]["total_pnl_pct"].(float64) @@ -531,14 +531,14 @@ func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) { } return pnlPctI > pnlPctJ }) - + // 限制返回前50名 totalCount := len(traders) limit := 50 if len(traders) > limit { traders = traders[:limit] } - + comparison := make(map[string]interface{}) comparison["traders"] = traders comparison["count"] = len(traders) @@ -559,21 +559,21 @@ func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) [ index int data map[string]interface{} } - + // 创建结果通道 resultChan := make(chan traderResult, len(traders)) - + // 并发获取每个交易员的数据 for i, t := range traders { go func(index int, trader *trader.AutoTrader) { // 设置单个交易员的超时时间为3秒 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - + // 使用通道来实现超时控制 accountChan := make(chan map[string]interface{}, 1) errorChan := make(chan error, 1) - + go func() { account, err := trader.GetAccountInfo() if err != nil { @@ -582,10 +582,10 @@ func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) [ accountChan <- account } }() - + status := trader.GetStatus() var traderData map[string]interface{} - + select { case account := <-accountChan: // 成功获取账户信息 @@ -634,18 +634,18 @@ func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) [ "error": "获取超时", } } - + resultChan <- traderResult{index: index, data: traderData} }(i, t) } - + // 收集所有结果 results := make([]map[string]interface{}, len(traders)) for i := 0; i < len(traders); i++ { result := <-resultChan results[result.index] = result.data } - + return results } @@ -656,20 +656,20 @@ func (tm *TraderManager) GetTopTradersData() (map[string]interface{}, error) { if err != nil { return nil, err } - + // 从竞赛数据中提取前5名 allTraders, ok := competitionData["traders"].([]map[string]interface{}) if !ok { return nil, fmt.Errorf("竞赛数据格式错误") } - + // 限制返回前5名 limit := 5 topTraders := allTraders if len(allTraders) > limit { topTraders = allTraders[:limit] } - + result := map[string]interface{}{ "traders": topTraders, "count": len(topTraders), diff --git a/mcp/client.go b/mcp/client.go index 5ef090e8..aa1d5435 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -280,8 +280,8 @@ func isRetryableError(err error) bool { "connection refused", "temporary failure", "no such host", - "stream error", // HTTP/2 stream 错误 - "INTERNAL_ERROR", // 服务端内部错误 + "stream error", // HTTP/2 stream 错误 + "INTERNAL_ERROR", // 服务端内部错误 } for _, retryable := range retryableErrors { if strings.Contains(errStr, retryable) {