fix(stats): fixed the PNL calculation (#963)

This commit is contained in:
Diego
2025-11-13 01:27:13 -05:00
committed by tangmengqiu
parent 970cfaadf3
commit fc8a4d3d63
11 changed files with 549 additions and 304 deletions

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log"
"math"
"net"
"net/http"
"nofx/auth"
@@ -132,7 +133,6 @@ func (s *Server) setupRoutes() {
protected.POST("/traders/:id/start", s.handleStartTrader)
protected.POST("/traders/:id/stop", s.handleStopTrader)
protected.PUT("/traders/:id/prompt", s.handleUpdateTraderPrompt)
protected.POST("/traders/:id/sync-balance", s.handleSyncBalance)
// AI模型配置
protected.GET("/models", s.handleGetModelConfigs)
@@ -589,16 +589,36 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
if balanceErr != nil {
log.Printf("⚠️ 查询交易所余额失败,使用用户输入的初始资金: %v", balanceErr)
} else {
// 提取可用余额
if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 {
actualBalance = availableBalance
log.Printf("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance)
} else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 {
// 有些交易所可能只返回 balance 字段
actualBalance = totalBalance
log.Printf("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance)
// 🔧 计算Total Equity = Wallet Balance + Unrealized Profit
// 这是账户的真实净值用作Initial Balance的基准
var totalWalletBalance float64
var totalUnrealizedProfit float64
// 提取钱包余额
if wb, ok := balanceInfo["totalWalletBalance"].(float64); ok {
totalWalletBalance = wb
} else if wb, ok := balanceInfo["wallet_balance"].(float64); ok {
totalWalletBalance = wb
} else if wb, ok := balanceInfo["balance"].(float64); ok {
totalWalletBalance = wb
}
// 提取未实现盈亏
if up, ok := balanceInfo["totalUnrealizedProfit"].(float64); ok {
totalUnrealizedProfit = up
} else if up, ok := balanceInfo["unrealized_profit"].(float64); ok {
totalUnrealizedProfit = up
}
// 计算总净值
totalEquity := totalWalletBalance + totalUnrealizedProfit
if totalEquity > 0 {
actualBalance = totalEquity
log.Printf("✅ 查询到交易所实际净值: %.2f USDT (钱包: %.2f + 未实现: %.2f, 用户输入: %.2f)",
actualBalance, totalWalletBalance, totalUnrealizedProfit, req.InitialBalance)
} else {
log.Printf("⚠️ 无法从余额信息中提取可用余额,使用用户输入的初始资金")
log.Printf("⚠️ 无法从余额信息中计算净值,使用用户输入的初始资金")
}
}
}
@@ -752,6 +772,21 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
return
}
// 如果请求中包含initial_balance且与现有值不同单独更新它
// UpdateTrader不会更新initial_balance需要使用专门的方法
if req.InitialBalance > 0 && math.Abs(req.InitialBalance-existingTrader.InitialBalance) > 0.1 {
err = s.database.UpdateTraderInitialBalance(userID, traderID, req.InitialBalance)
if err != nil {
log.Printf("⚠️ 更新初始余额失败: %v", err)
// 不返回错误,因为主要配置已更新成功
} else {
log.Printf("✓ 初始余额已更新: %.2f -> %.2f", existingTrader.InitialBalance, req.InitialBalance)
}
}
// 🔄 从内存中移除旧的trader实例以便重新加载最新配置
s.traderManager.RemoveTrader(traderID)
// 重新加载交易员到内存
err = s.traderManager.LoadTraderByID(s.database, userID, traderID)
if err != nil {
@@ -913,113 +948,6 @@ func (s *Server) handleUpdateTraderPrompt(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "自定义prompt已更新"})
}
// handleSyncBalance 同步交易所余额到initial_balance选项B手动同步 + 选项C智能检测
func (s *Server) handleSyncBalance(c *gin.Context) {
userID := c.GetString("user_id")
traderID := c.Param("id")
log.Printf("🔄 用户 %s 请求同步交易员 %s 的余额", userID, traderID)
// 从数据库获取交易员配置(包含交易所信息)
traderConfig, _, exchangeCfg, err := s.database.GetTraderConfig(userID, traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
return
}
if exchangeCfg == nil || !exchangeCfg.Enabled {
c.JSON(http.StatusBadRequest, gin.H{"error": "交易所未配置或未启用"})
return
}
// 创建临时 trader 查询余额
var tempTrader trader.Trader
var createErr error
switch traderConfig.ExchangeID {
case "binance":
tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey, userID)
case "hyperliquid":
tempTrader, createErr = trader.NewHyperliquidTrader(
exchangeCfg.APIKey,
exchangeCfg.HyperliquidWalletAddr,
exchangeCfg.Testnet,
)
case "aster":
tempTrader, createErr = trader.NewAsterTrader(
exchangeCfg.AsterUser,
exchangeCfg.AsterSigner,
exchangeCfg.AsterPrivateKey,
)
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的交易所类型"})
return
}
if createErr != nil {
log.Printf("⚠️ 创建临时 trader 失败: %v", createErr)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("连接交易所失败: %v", createErr)})
return
}
// 查询实际余额
balanceInfo, balanceErr := tempTrader.GetBalance()
if balanceErr != nil {
log.Printf("⚠️ 查询交易所余额失败: %v", balanceErr)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("查询余额失败: %v", balanceErr)})
return
}
// 提取可用余额
var actualBalance float64
if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 {
actualBalance = availableBalance
} else if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 {
actualBalance = availableBalance
} else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 {
actualBalance = totalBalance
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取可用余额"})
return
}
oldBalance := traderConfig.InitialBalance
// ✅ 选项C智能检测余额变化
changePercent := ((actualBalance - oldBalance) / oldBalance) * 100
changeType := "增加"
if changePercent < 0 {
changeType = "减少"
}
log.Printf("✓ 查询到交易所实际余额: %.2f USDT (当前配置: %.2f USDT, 变化: %.2f%%)",
actualBalance, oldBalance, changePercent)
// 更新数据库中的 initial_balance
err = s.database.UpdateTraderInitialBalance(userID, traderID, actualBalance)
if err != nil {
log.Printf("❌ 更新initial_balance失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新余额失败"})
return
}
// 重新加载交易员到内存
err = s.traderManager.LoadTraderByID(s.database, userID, traderID)
if err != nil {
log.Printf("⚠️ 重新加载交易员到内存失败: %v", err)
}
log.Printf("✅ 已同步余额: %.2f → %.2f USDT (%s %.2f%%)", oldBalance, actualBalance, changeType, changePercent)
c.JSON(http.StatusOK, gin.H{
"message": "余额同步成功",
"old_balance": oldBalance,
"new_balance": actualBalance,
"change_percent": changePercent,
"change_type": changeType,
})
}
// handleGetModelConfigs 获取AI模型配置
func (s *Server) handleGetModelConfigs(c *gin.Context) {
userID := c.GetString("user_id")
@@ -1563,22 +1491,16 @@ func (s *Server) handleEquityHistory(c *gin.Context) {
CycleNumber int `json:"cycle_number"`
}
// 从AutoTrader获取初始余额于计算盈亏百分比
initialBalance := 0.0
// 从AutoTrader获取当前初始余额(用作旧数据的fallback
base := 0.0
if status := trader.GetStatus(); status != nil {
if ib, ok := status["initial_balance"].(float64); ok && ib > 0 {
initialBalance = ib
base = ib
}
}
// 如果无法从status获取且有历史记录则从第一条记录获取
if initialBalance == 0 && len(records) > 0 {
// 第一条记录的equity作为初始余额
initialBalance = records[0].AccountState.TotalBalance
}
// 如果还是无法获取,返回错误
if initialBalance == 0 {
if base == 0 {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "无法获取初始余额",
})
@@ -1588,14 +1510,24 @@ func (s *Server) handleEquityHistory(c *gin.Context) {
var history []EquityPoint
for _, record := range records {
// TotalBalance字段实际存储的是TotalEquity
totalEquity := record.AccountState.TotalBalance
// totalEquity := record.AccountState.TotalBalance
// TotalUnrealizedProfit字段实际存储的是TotalPnL相对初始余额
totalPnL := record.AccountState.TotalUnrealizedProfit
// totalPnL := record.AccountState.TotalUnrealizedProfit
walletBalance := record.AccountState.TotalBalance
unrealizedPnL := record.AccountState.TotalUnrealizedProfit
totalEquity := walletBalance + unrealizedPnL
// 🔄 使用历史记录中保存的initial_balance如果有
// 这样可以保持历史PNL%的准确性即使用户后来更新了initial_balance
if record.AccountState.InitialBalance > 0 {
base = record.AccountState.InitialBalance
}
totalPnL := totalEquity - base
// 计算盈亏百分比
totalPnLPct := 0.0
if initialBalance > 0 {
totalPnLPct = (totalPnL / initialBalance) * 100
if base > 0 {
totalPnLPct = (totalPnL / base) * 100
}
history = append(history, EquityPoint{