Files
nofx/api/server.go
T
icy bbe1e1f929 Merge remote tracking branch into local development
- Resolved conflicts in README.md: Combined web-based config updates with multi-exchange support
- Resolved conflicts in main.go: Fixed database initialization and default coin settings
- Resolved conflicts in manager/trader_manager.go: Updated trader management for new database structure
- Resolved conflicts in web/src/App.tsx: Combined UI improvements with responsive design
- Resolved conflicts in web/.dockerignore: Merged dependency exclusions
- Removed deprecated files: Dockerfile, config/config.go, web/Dockerfile, ComparisonChart.tsx, CompetitionPage.tsx
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
2025-10-30 20:57:57 +08:00

682 lines
20 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/config"
"nofx/manager"
"time"
"github.com/gin-gonic/gin"
)
// Server HTTP API服务器
type Server struct {
router *gin.Engine
traderManager *manager.TraderManager
database *config.Database
port int
}
// NewServer 创建API服务器
func NewServer(traderManager *manager.TraderManager, database *config.Database, port int) *Server {
// 设置为Release模式(减少日志输出)
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
// 启用CORS
router.Use(corsMiddleware())
s := &Server{
router: router,
traderManager: traderManager,
database: database,
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.Any("/health", s.handleHealth)
// API路由组
api := s.router.Group("/api")
{
// AI交易员管理
api.GET("/traders", s.handleTraderList)
api.POST("/traders", s.handleCreateTrader)
api.DELETE("/traders/:id", s.handleDeleteTrader)
api.POST("/traders/:id/start", s.handleStartTrader)
api.POST("/traders/:id/stop", s.handleStopTrader)
// AI模型配置
api.GET("/models", s.handleGetModelConfigs)
api.PUT("/models", s.handleUpdateModelConfigs)
// 交易所配置
api.GET("/exchanges", s.handleGetExchangeConfigs)
api.PUT("/exchanges", s.handleUpdateExchangeConfigs)
// 指定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)
api.GET("/performance", s.handlePerformance)
}
}
// 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
}
// AI交易员管理相关结构体
type CreateTraderRequest struct {
Name string `json:"name" binding:"required"`
AIModelID string `json:"ai_model_id" binding:"required"`
ExchangeID string `json:"exchange_id" binding:"required"`
InitialBalance float64 `json:"initial_balance"`
}
type ModelConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey,omitempty"`
}
type ExchangeConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"` // "cex" or "dex"
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey,omitempty"`
SecretKey string `json:"secretKey,omitempty"`
Testnet bool `json:"testnet,omitempty"`
}
type UpdateModelConfigRequest struct {
Models map[string]struct {
Enabled bool `json:"enabled"`
APIKey string `json:"api_key"`
} `json:"models"`
}
type UpdateExchangeConfigRequest struct {
Exchanges map[string]struct {
Enabled bool `json:"enabled"`
APIKey string `json:"api_key"`
SecretKey string `json:"secret_key"`
Testnet bool `json:"testnet"`
} `json:"exchanges"`
}
// handleCreateTrader 创建新的AI交易员
func (s *Server) handleCreateTrader(c *gin.Context) {
var req CreateTraderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 生成交易员ID
traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix())
// 创建交易员配置
trader := &config.TraderConfig{
ID: traderID,
Name: req.Name,
AIModelID: req.AIModelID,
ExchangeID: req.ExchangeID,
InitialBalance: req.InitialBalance,
ScanIntervalMinutes: 3, // 默认3分钟
IsRunning: false,
}
// 保存到数据库
err := s.database.CreateTrader(trader)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建交易员失败: %v", err)})
return
}
log.Printf("✓ 创建交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID)
c.JSON(http.StatusCreated, gin.H{
"trader_id": traderID,
"trader_name": req.Name,
"ai_model": req.AIModelID,
"is_running": false,
})
}
// handleDeleteTrader 删除交易员
func (s *Server) handleDeleteTrader(c *gin.Context) {
traderID := c.Param("id")
// 从数据库删除
err := s.database.DeleteTrader(traderID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("删除交易员失败: %v", err)})
return
}
// 如果交易员正在运行,先停止它
if trader, err := s.traderManager.GetTrader(traderID); err == nil {
status := trader.GetStatus()
if isRunning, ok := status["is_running"].(bool); ok && isRunning {
trader.Stop()
log.Printf("⏹ 已停止运行中的交易员: %s", traderID)
}
}
log.Printf("✓ 交易员已删除: %s", traderID)
c.JSON(http.StatusOK, gin.H{"message": "交易员已删除"})
}
// handleStartTrader 启动交易员
func (s *Server) handleStartTrader(c *gin.Context) {
traderID := c.Param("id")
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
return
}
// 检查交易员是否已经在运行
status := trader.GetStatus()
if isRunning, ok := status["is_running"].(bool); ok && isRunning {
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已在运行中"})
return
}
// 启动交易员
go func() {
log.Printf("▶️ 启动交易员 %s (%s)", traderID, trader.GetName())
if err := trader.Run(); err != nil {
log.Printf("❌ 交易员 %s 运行错误: %v", trader.GetName(), err)
}
}()
// 更新数据库中的运行状态
err = s.database.UpdateTraderStatus(traderID, true)
if err != nil {
log.Printf("⚠️ 更新交易员状态失败: %v", err)
}
log.Printf("✓ 交易员 %s 已启动", trader.GetName())
c.JSON(http.StatusOK, gin.H{"message": "交易员已启动"})
}
// handleStopTrader 停止交易员
func (s *Server) handleStopTrader(c *gin.Context) {
traderID := c.Param("id")
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
return
}
// 检查交易员是否正在运行
status := trader.GetStatus()
if isRunning, ok := status["is_running"].(bool); ok && !isRunning {
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员已停止"})
return
}
// 停止交易员
trader.Stop()
// 更新数据库中的运行状态
err = s.database.UpdateTraderStatus(traderID, false)
if err != nil {
log.Printf("⚠️ 更新交易员状态失败: %v", err)
}
log.Printf("⏹ 交易员 %s 已停止", trader.GetName())
c.JSON(http.StatusOK, gin.H{"message": "交易员已停止"})
}
// handleGetModelConfigs 获取AI模型配置
func (s *Server) handleGetModelConfigs(c *gin.Context) {
models, err := s.database.GetAIModels()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取AI模型配置失败: %v", err)})
return
}
c.JSON(http.StatusOK, models)
}
// handleUpdateModelConfigs 更新AI模型配置
func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
var req UpdateModelConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 更新每个模型的配置
for modelID, modelData := range req.Models {
err := s.database.UpdateAIModel(modelID, modelData.Enabled, modelData.APIKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新模型 %s 失败: %v", modelID, err)})
return
}
}
log.Printf("✓ AI模型配置已更新: %+v", req.Models)
c.JSON(http.StatusOK, gin.H{"message": "模型配置已更新"})
}
// handleGetExchangeConfigs 获取交易所配置
func (s *Server) handleGetExchangeConfigs(c *gin.Context) {
exchanges, err := s.database.GetExchanges()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取交易所配置失败: %v", err)})
return
}
c.JSON(http.StatusOK, exchanges)
}
// handleUpdateExchangeConfigs 更新交易所配置
func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
var req UpdateExchangeConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 更新每个交易所的配置
for exchangeID, exchangeData := range req.Exchanges {
err := s.database.UpdateExchange(exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Testnet)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易所 %s 失败: %v", exchangeID, err)})
return
}
}
log.Printf("✓ 交易所配置已更新: %+v", req.Exchanges)
c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"})
}
// handleTraderList trader列表
func (s *Server) handleTraderList(c *gin.Context) {
traders, err := s.database.GetTraders()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取交易员列表失败: %v", err)})
return
}
result := make([]map[string]interface{}, 0, len(traders))
for _, trader := range traders {
// 获取实时运行状态
isRunning := trader.IsRunning
if at, err := s.traderManager.GetTrader(trader.ID); err == nil {
status := at.GetStatus()
if running, ok := status["is_running"].(bool); ok {
isRunning = running
}
}
result = append(result, map[string]interface{}{
"trader_id": trader.ID,
"trader_name": trader.Name,
"ai_model": trader.AIModelID,
"exchange_id": trader.ExchangeID,
"is_running": isRunning,
"initial_balance": trader.InitialBalance,
})
}
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)
}
// handlePerformance AI历史表现分析(用于展示AI学习和反思)
func (s *Server) handlePerformance(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
}
// 分析最近100个周期的交易表现(避免长期持仓的交易记录丢失)
// 假设每3分钟一个周期,100个周期 = 5小时,足够覆盖大部分交易
performance, err := trader.GetDecisionLogger().AnalyzePerformance(100)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("分析历史表现失败: %v", err),
})
return
}
c.JSON(http.StatusOK, performance)
}
// 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 /health - 健康检查")
log.Printf(" • GET /api/traders - AI交易员列表")
log.Printf(" • POST /api/traders - 创建新的AI交易员")
log.Printf(" • DELETE /api/traders/:id - 删除AI交易员")
log.Printf(" • POST /api/traders/:id/start - 启动AI交易员")
log.Printf(" • POST /api/traders/:id/stop - 停止AI交易员")
log.Printf(" • GET /api/models - 获取AI模型配置")
log.Printf(" • PUT /api/models - 更新AI模型配置")
log.Printf(" • GET /api/exchanges - 获取交易所配置")
log.Printf(" • PUT /api/exchanges - 更新交易所配置")
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 /api/performance?trader_id=xxx - 指定trader的AI学习表现分析")
log.Println()
return s.router.Run(addr)
}