diff --git a/api/server.go b/api/server.go index 9a8adde4..77de482e 100644 --- a/api/server.go +++ b/api/server.go @@ -92,7 +92,10 @@ func (s *Server) setupRoutes() { // 公开的竞赛数据(无需认证) api.GET("/traders", s.handlePublicTraderList) api.GET("/competition", s.handlePublicCompetition) + api.GET("/top-traders", s.handleTopTraders) api.GET("/equity-history", s.handleEquityHistory) + api.GET("/equity-history-batch", s.handleEquityHistoryBatch) + api.GET("/traders/:id/public-config", s.handleGetPublicTraderConfig) // 需要认证的路由 protected := api.Group("/", s.authMiddleware()) @@ -513,8 +516,16 @@ func (s *Server) handleDeleteTrader(c *gin.Context) { // handleStartTrader 启动交易员 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": "交易员不存在"}) @@ -537,7 +548,6 @@ func (s *Server) handleStartTrader(c *gin.Context) { }() // 更新数据库中的运行状态 - userID := c.GetString("user_id") err = s.database.UpdateTraderStatus(userID, traderID, true) if err != nil { log.Printf("⚠️ 更新交易员状态失败: %v", err) @@ -549,8 +559,16 @@ func (s *Server) handleStartTrader(c *gin.Context) { // handleStopTrader 停止交易员 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": "交易员不存在"}) @@ -568,7 +586,6 @@ func (s *Server) handleStopTrader(c *gin.Context) { trader.Stop() // 更新数据库中的运行状态 - userID := c.GetString("user_id") err = s.database.UpdateTraderStatus(userID, traderID, false) if err != nil { log.Printf("⚠️ 更新交易员状态失败: %v", err) @@ -1441,9 +1458,12 @@ func (s *Server) Start() error { log.Printf("🌐 API服务器启动在 http://localhost%s", addr) log.Printf("📊 API文档:") log.Printf(" • GET /api/health - 健康检查") - log.Printf(" • GET /api/traders - 公开的AI交易员列表(无需认证)") + log.Printf(" • GET /api/traders - 公开的AI交易员排行榜前50名(无需认证)") log.Printf(" • GET /api/competition - 公开的竞赛数据(无需认证)") + log.Printf(" • GET /api/top-traders - 前10名交易员数据(无需认证,表现对比用)") log.Printf(" • GET /api/equity-history?trader_id=xxx - 公开的收益率历史数据(无需认证,竞赛用)") + log.Printf(" • GET /api/equity-history-batch?trader_ids=a,b,c - 批量获取历史数据(无需认证,表现对比优化)") + log.Printf(" • GET /api/traders/:id/public-config - 公开的交易员配置(无需认证,不含敏感信息)") log.Printf(" • POST /api/traders - 创建新的AI交易员") log.Printf(" • DELETE /api/traders/:id - 删除AI交易员") log.Printf(" • POST /api/traders/:id/start - 启动AI交易员") @@ -1557,3 +1577,145 @@ func (s *Server) handlePublicCompetition(c *gin.Context) { c.JSON(http.StatusOK, competition) } +// handleTopTraders 获取前10名交易员数据(无需认证,用于表现对比) +func (s *Server) handleTopTraders(c *gin.Context) { + topTraders, err := s.traderManager.GetTopTradersData() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("获取前10名交易员数据失败: %v", err), + }) + return + } + + c.JSON(http.StatusOK, topTraders) +} + +// handleEquityHistoryBatch 批量获取多个交易员的收益率历史数据(无需认证,用于表现对比) +func (s *Server) handleEquityHistoryBatch(c *gin.Context) { + // 获取trader_ids参数,支持逗号分隔的多个ID + traderIDsParam := c.Query("trader_ids") + if traderIDsParam == "" { + // 如果没有指定trader_ids,则返回前10名的历史数据 + topTraders, err := s.traderManager.GetTopTradersData() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("获取前10名交易员失败: %v", err), + }) + 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 { + if traderID, ok := trader["trader_id"].(string); ok { + traderIDs = append(traderIDs, traderID) + } + } + + result := s.getEquityHistoryForTraders(traderIDs) + c.JSON(http.StatusOK, result) + return + } + + // 解析逗号分隔的trader IDs + traderIDs := strings.Split(traderIDsParam, ",") + for i := range traderIDs { + traderIDs[i] = strings.TrimSpace(traderIDs[i]) + } + + // 限制最多20个交易员,防止请求过大 + if len(traderIDs) > 20 { + traderIDs = traderIDs[:20] + } + + result := s.getEquityHistoryForTraders(traderIDs) + c.JSON(http.StatusOK, result) +} + +// getEquityHistoryForTraders 获取多个交易员的历史数据 +func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]interface{} { + 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, + "total_pnl": record.AccountState.TotalUnrealizedProfit, + "balance": record.AccountState.TotalBalance, + }) + } + + histories[traderID] = history + } + + result["histories"] = histories + result["count"] = len(histories) + if len(errors) > 0 { + result["errors"] = errors + } + + return result +} + +// handleGetPublicTraderConfig 获取公开的交易员配置信息(无需认证,不包含敏感信息) +func (s *Server) handleGetPublicTraderConfig(c *gin.Context) { + traderID := c.Param("id") + if traderID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "交易员ID不能为空"}) + return + } + + trader, err := s.traderManager.GetTrader(traderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) + return + } + + // 获取交易员的状态信息 + status := trader.GetStatus() + + // 只返回公开的配置信息,不包含API密钥等敏感数据 + result := map[string]interface{}{ + "trader_id": trader.GetID(), + "trader_name": trader.GetName(), + "ai_model": trader.GetAIModel(), + "exchange": trader.GetExchange(), + "is_running": status["is_running"], + "ai_provider": status["ai_provider"], + "start_time": status["start_time"], + } + + c.JSON(http.StatusOK, result) +} + diff --git a/manager/trader_manager.go b/manager/trader_manager.go index d3861cdb..0493be45 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -6,6 +6,7 @@ import ( "log" "nofx/config" "nofx/trader" + "sort" "strconv" "strings" "sync" @@ -525,12 +526,108 @@ func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) { traders = append(traders, traderData) } + + // 按收益率排序(降序) + sort.Slice(traders, func(i, j int) bool { + pnlPctI, okI := traders[i]["total_pnl_pct"].(float64) + pnlPctJ, okJ := traders[j]["total_pnl_pct"].(float64) + if !okI { + pnlPctI = 0 + } + if !okJ { + pnlPctJ = 0 + } + return pnlPctI > pnlPctJ + }) + + // 限制返回前50名 + totalCount := len(traders) + limit := 50 + if len(traders) > limit { + traders = traders[:limit] + } + comparison["traders"] = traders comparison["count"] = len(traders) + comparison["total_count"] = totalCount // 总交易员数量 return comparison, nil } +// GetTopTradersData 获取前10名交易员数据(用于表现对比) +func (tm *TraderManager) GetTopTradersData() (map[string]interface{}, error) { + tm.mu.RLock() + defer tm.mu.RUnlock() + + traders := make([]map[string]interface{}, 0) + + // 获取全平台所有交易员 + for _, t := range tm.traders { + account, err := t.GetAccountInfo() + status := t.GetStatus() + + var traderData map[string]interface{} + + if err != nil { + // 如果获取账户信息失败,使用默认值 + 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"], + } + } 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) + } + + // 按收益率排序(降序) + sort.Slice(traders, func(i, j int) bool { + pnlPctI, okI := traders[i]["total_pnl_pct"].(float64) + pnlPctJ, okJ := traders[j]["total_pnl_pct"].(float64) + if !okI { + pnlPctI = 0 + } + if !okJ { + pnlPctJ = 0 + } + return pnlPctI > pnlPctJ + }) + + // 限制返回前10名 + limit := 10 + if len(traders) > limit { + traders = traders[:limit] + } + + result := map[string]interface{}{ + "traders": traders, + "count": len(traders), + } + + return result, nil +} + // isUserTrader 检查trader是否属于指定用户 func isUserTrader(traderID, userID string) bool { // trader ID格式: userID_traderName 或 randomUUID_modelName