mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-03 11:00:58 +08:00
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
This commit is contained in:
394
api/server.go
Normal file
394
api/server.go
Normal file
@@ -0,0 +1,394 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user