mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 16:56:56 +08:00
Enhance leaderboard and security for trader management
Features: - Limit leaderboard to top 50 traders sorted by PnL percentage - Add top 10 traders endpoint for performance comparison - Create batch equity history endpoint to optimize frontend performance - Add public trader config endpoint without sensitive data Security: - Add user ownership validation for start/stop trader operations - Prevent users from controlling other users' traders - Maintain consistent error messages for security Performance: - Reduce API calls from 10 to 1 for performance comparison page - Add data limits and error handling for batch operations - Sort traders by performance across all endpoints API Changes: - GET /api/traders - now returns top 50 sorted traders - GET /api/top-traders - new endpoint for top 10 traders - GET /api/equity-history-batch - batch endpoint for multiple trader histories - GET /api/traders/:id/public-config - public config without secrets - POST /api/traders/:id/start - now validates user ownership - POST /api/traders/:id/stop - now validates user ownership 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
168
api/server.go
168
api/server.go
@@ -92,7 +92,10 @@ func (s *Server) setupRoutes() {
|
|||||||
// 公开的竞赛数据(无需认证)
|
// 公开的竞赛数据(无需认证)
|
||||||
api.GET("/traders", s.handlePublicTraderList)
|
api.GET("/traders", s.handlePublicTraderList)
|
||||||
api.GET("/competition", s.handlePublicCompetition)
|
api.GET("/competition", s.handlePublicCompetition)
|
||||||
|
api.GET("/top-traders", s.handleTopTraders)
|
||||||
api.GET("/equity-history", s.handleEquityHistory)
|
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())
|
protected := api.Group("/", s.authMiddleware())
|
||||||
@@ -513,8 +516,16 @@ func (s *Server) handleDeleteTrader(c *gin.Context) {
|
|||||||
|
|
||||||
// handleStartTrader 启动交易员
|
// handleStartTrader 启动交易员
|
||||||
func (s *Server) handleStartTrader(c *gin.Context) {
|
func (s *Server) handleStartTrader(c *gin.Context) {
|
||||||
|
userID := c.GetString("user_id")
|
||||||
traderID := c.Param("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)
|
trader, err := s.traderManager.GetTrader(traderID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
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)
|
err = s.database.UpdateTraderStatus(userID, traderID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("⚠️ 更新交易员状态失败: %v", err)
|
log.Printf("⚠️ 更新交易员状态失败: %v", err)
|
||||||
@@ -549,8 +559,16 @@ func (s *Server) handleStartTrader(c *gin.Context) {
|
|||||||
|
|
||||||
// handleStopTrader 停止交易员
|
// handleStopTrader 停止交易员
|
||||||
func (s *Server) handleStopTrader(c *gin.Context) {
|
func (s *Server) handleStopTrader(c *gin.Context) {
|
||||||
|
userID := c.GetString("user_id")
|
||||||
traderID := c.Param("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)
|
trader, err := s.traderManager.GetTrader(traderID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
||||||
@@ -568,7 +586,6 @@ func (s *Server) handleStopTrader(c *gin.Context) {
|
|||||||
trader.Stop()
|
trader.Stop()
|
||||||
|
|
||||||
// 更新数据库中的运行状态
|
// 更新数据库中的运行状态
|
||||||
userID := c.GetString("user_id")
|
|
||||||
err = s.database.UpdateTraderStatus(userID, traderID, false)
|
err = s.database.UpdateTraderStatus(userID, traderID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("⚠️ 更新交易员状态失败: %v", err)
|
log.Printf("⚠️ 更新交易员状态失败: %v", err)
|
||||||
@@ -1441,9 +1458,12 @@ func (s *Server) Start() error {
|
|||||||
log.Printf("🌐 API服务器启动在 http://localhost%s", addr)
|
log.Printf("🌐 API服务器启动在 http://localhost%s", addr)
|
||||||
log.Printf("📊 API文档:")
|
log.Printf("📊 API文档:")
|
||||||
log.Printf(" • GET /api/health - 健康检查")
|
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/competition - 公开的竞赛数据(无需认证)")
|
||||||
|
log.Printf(" • GET /api/top-traders - 前10名交易员数据(无需认证,表现对比用)")
|
||||||
log.Printf(" • GET /api/equity-history?trader_id=xxx - 公开的收益率历史数据(无需认证,竞赛用)")
|
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(" • POST /api/traders - 创建新的AI交易员")
|
||||||
log.Printf(" • DELETE /api/traders/:id - 删除AI交易员")
|
log.Printf(" • DELETE /api/traders/:id - 删除AI交易员")
|
||||||
log.Printf(" • POST /api/traders/:id/start - 启动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)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"nofx/config"
|
"nofx/config"
|
||||||
"nofx/trader"
|
"nofx/trader"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -525,12 +526,108 @@ func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) {
|
|||||||
|
|
||||||
traders = append(traders, traderData)
|
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["traders"] = traders
|
||||||
comparison["count"] = len(traders)
|
comparison["count"] = len(traders)
|
||||||
|
comparison["total_count"] = totalCount // 总交易员数量
|
||||||
|
|
||||||
return comparison, nil
|
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是否属于指定用户
|
// isUserTrader 检查trader是否属于指定用户
|
||||||
func isUserTrader(traderID, userID string) bool {
|
func isUserTrader(traderID, userID string) bool {
|
||||||
// trader ID格式: userID_traderName 或 randomUUID_modelName
|
// trader ID格式: userID_traderName 或 randomUUID_modelName
|
||||||
|
|||||||
Reference in New Issue
Block a user