Files
nofx/api/server.go
T
tinkle-community 5aa50d35d7 Initial commit: NOFX AI Trading System
- Multi-AI competition mode (Qwen vs DeepSeek)
- Binance Futures integration
- AI self-learning mechanism
- Professional web dashboard
- Complete risk management system
2025-10-28 15:47:34 +08:00

395 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"fmt"
"log"
"net/http"
"nofx/manager"
"github.com/gin-gonic/gin"
)
// Server HTTP API服务器
type Server struct {
router *gin.Engine
traderManager *manager.TraderManager
port int
}
// NewServer 创建API服务器
func NewServer(traderManager *manager.TraderManager, port int) *Server {
// 设置为Release模式(减少日志输出)
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
// 启用CORS
router.Use(corsMiddleware())
s := &Server{
router: router,
traderManager: traderManager,
port: port,
}
// 设置路由
s.setupRoutes()
return s
}
// corsMiddleware CORS中间件
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusOK)
return
}
c.Next()
}
}
// setupRoutes 设置路由
func (s *Server) setupRoutes() {
// 健康检查
s.router.GET("/health", s.handleHealth)
// API路由组
api := s.router.Group("/api")
{
// 竞赛总览
api.GET("/competition", s.handleCompetition)
// Trader列表
api.GET("/traders", s.handleTraderList)
// 指定trader的数据(使用query参数 ?trader_id=xxx
api.GET("/status", s.handleStatus)
api.GET("/account", s.handleAccount)
api.GET("/positions", s.handlePositions)
api.GET("/decisions", s.handleDecisions)
api.GET("/decisions/latest", s.handleLatestDecisions)
api.GET("/statistics", s.handleStatistics)
api.GET("/equity-history", s.handleEquityHistory)
}
}
// handleHealth 健康检查
func (s *Server) handleHealth(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"time": c.Request.Context().Value("time"),
})
}
// getTraderFromQuery 从query参数获取trader
func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, string, error) {
traderID := c.Query("trader_id")
if traderID == "" {
// 如果没有指定trader_id,返回第一个trader
ids := s.traderManager.GetTraderIDs()
if len(ids) == 0 {
return nil, "", fmt.Errorf("没有可用的trader")
}
traderID = ids[0]
}
return s.traderManager, traderID, nil
}
// handleCompetition 竞赛总览(对比所有trader)
func (s *Server) handleCompetition(c *gin.Context) {
comparison, err := s.traderManager.GetComparisonData()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取对比数据失败: %v", err),
})
return
}
c.JSON(http.StatusOK, comparison)
}
// handleTraderList trader列表
func (s *Server) handleTraderList(c *gin.Context) {
traders := s.traderManager.GetAllTraders()
result := make([]map[string]interface{}, 0, len(traders))
for _, t := range traders {
result = append(result, map[string]interface{}{
"trader_id": t.GetID(),
"trader_name": t.GetName(),
"ai_model": t.GetAIModel(),
})
}
c.JSON(http.StatusOK, result)
}
// handleStatus 系统状态
func (s *Server) handleStatus(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
status := trader.GetStatus()
c.JSON(http.StatusOK, status)
}
// handleAccount 账户信息
func (s *Server) handleAccount(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
log.Printf("📊 收到账户信息请求 [%s]", trader.GetName())
account, err := trader.GetAccountInfo()
if err != nil {
log.Printf("❌ 获取账户信息失败 [%s]: %v", trader.GetName(), err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取账户信息失败: %v", err),
})
return
}
log.Printf("✓ 返回账户信息 [%s]: 净值=%.2f, 可用=%.2f, 盈亏=%.2f (%.2f%%)",
trader.GetName(),
account["total_equity"],
account["available_balance"],
account["total_pnl"],
account["total_pnl_pct"])
c.JSON(http.StatusOK, account)
}
// handlePositions 持仓列表
func (s *Server) handlePositions(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
positions, err := trader.GetPositions()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取持仓列表失败: %v", err),
})
return
}
c.JSON(http.StatusOK, positions)
}
// handleDecisions 决策日志列表
func (s *Server) handleDecisions(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
// 获取所有历史决策记录(无限制)
records, err := trader.GetDecisionLogger().GetLatestRecords(10000)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取决策日志失败: %v", err),
})
return
}
c.JSON(http.StatusOK, records)
}
// handleLatestDecisions 最新决策日志(最近5条,最新的在前)
func (s *Server) handleLatestDecisions(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
records, err := trader.GetDecisionLogger().GetLatestRecords(5)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取决策日志失败: %v", err),
})
return
}
// 反转数组,让最新的在前面(用于列表显示)
// GetLatestRecords返回的是从旧到新(用于图表),这里需要从新到旧
for i, j := 0, len(records)-1; i < j; i, j = i+1, j-1 {
records[i], records[j] = records[j], records[i]
}
c.JSON(http.StatusOK, records)
}
// handleStatistics 统计信息
func (s *Server) handleStatistics(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
stats, err := trader.GetDecisionLogger().GetStatistics()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取统计信息失败: %v", err),
})
return
}
c.JSON(http.StatusOK, stats)
}
// handleEquityHistory 收益率历史数据
func (s *Server) handleEquityHistory(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
// 获取尽可能多的历史数据(几天的数据)
// 每3分钟一个周期:10000条 = 约20天的数据
records, err := trader.GetDecisionLogger().GetLatestRecords(10000)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取历史数据失败: %v", err),
})
return
}
// 构建收益率历史数据点
type EquityPoint struct {
Timestamp string `json:"timestamp"`
TotalEquity float64 `json:"total_equity"` // 账户净值(wallet + unrealized
AvailableBalance float64 `json:"available_balance"` // 可用余额
TotalPnL float64 `json:"total_pnl"` // 总盈亏(相对初始余额)
TotalPnLPct float64 `json:"total_pnl_pct"` // 总盈亏百分比
PositionCount int `json:"position_count"` // 持仓数量
MarginUsedPct float64 `json:"margin_used_pct"` // 保证金使用率
CycleNumber int `json:"cycle_number"`
}
// 从AutoTrader获取初始余额(用于计算盈亏百分比)
initialBalance := 0.0
if status := trader.GetStatus(); status != nil {
if ib, ok := status["initial_balance"].(float64); ok && ib > 0 {
initialBalance = ib
}
}
// 如果无法从status获取,且有历史记录,则从第一条记录获取
if initialBalance == 0 && len(records) > 0 {
// 第一条记录的equity作为初始余额
initialBalance = records[0].AccountState.TotalBalance
}
// 如果还是无法获取,返回错误
if initialBalance == 0 {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "无法获取初始余额",
})
return
}
var history []EquityPoint
for _, record := range records {
// TotalBalance字段实际存储的是TotalEquity
totalEquity := record.AccountState.TotalBalance
// TotalUnrealizedProfit字段实际存储的是TotalPnL(相对初始余额)
totalPnL := record.AccountState.TotalUnrealizedProfit
// 计算盈亏百分比
totalPnLPct := 0.0
if initialBalance > 0 {
totalPnLPct = (totalPnL / initialBalance) * 100
}
history = append(history, EquityPoint{
Timestamp: record.Timestamp.Format("2006-01-02 15:04:05"),
TotalEquity: totalEquity,
AvailableBalance: record.AccountState.AvailableBalance,
TotalPnL: totalPnL,
TotalPnLPct: totalPnLPct,
PositionCount: record.AccountState.PositionCount,
MarginUsedPct: record.AccountState.MarginUsedPct,
CycleNumber: record.CycleNumber,
})
}
c.JSON(http.StatusOK, history)
}
// Start 启动服务器
func (s *Server) Start() error {
addr := fmt.Sprintf(":%d", s.port)
log.Printf("🌐 API服务器启动在 http://localhost%s", addr)
log.Printf("📊 API文档:")
log.Printf(" • GET /api/competition - 竞赛总览(对比所有trader")
log.Printf(" • GET /api/traders - Trader列表")
log.Printf(" • GET /api/status?trader_id=xxx - 指定trader的系统状态")
log.Printf(" • GET /api/account?trader_id=xxx - 指定trader的账户信息")
log.Printf(" • GET /api/positions?trader_id=xxx - 指定trader的持仓列表")
log.Printf(" • GET /api/decisions?trader_id=xxx - 指定trader的决策日志")
log.Printf(" • GET /api/decisions/latest?trader_id=xxx - 指定trader的最新决策")
log.Printf(" • GET /api/statistics?trader_id=xxx - 指定trader的统计信息")
log.Printf(" • GET /api/equity-history?trader_id=xxx - 指定trader的收益率历史数据")
log.Printf(" • GET /health - 健康检查")
log.Println()
return s.router.Run(addr)
}