mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-07 11:17:56 +08:00
bbe1e1f929
- 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>
682 lines
20 KiB
Go
682 lines
20 KiB
Go
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)
|
||
}
|