fix(api): add balance sync endpoint with smart detection

## Summary
- Add POST /traders/:id/sync-balance endpoint (Option B)
- Add smart detection showing balance change percentage (Option C)
- Fix balance display bug caused by commit 2b9c4d2
## Changes
### api/server.go
- Add handleSyncBalance() handler
- Query actual exchange balance via trader.GetBalance()
- Calculate change percentage for smart detection
- Update initial_balance in database
- Reload trader into memory after update
### config/database.go
- Add UpdateTraderInitialBalance() method
- Update traders.initial_balance field
## Root Cause
Commit 2b9c4d2 auto-queries exchange balance at trader creation time,
but never updates after user deposits more funds, causing:
- Wrong initial_balance (400 USDT vs actual 3000 USDT)
- Wrong P&L calculations (-2598.55 USDT instead of actual)
## Solution
Provides manual sync API + smart detection to update initial_balance
when user deposits funds after trader creation.
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
ZhouYongyou
2025-11-04 19:55:16 +08:00
parent 4f7d21c581
commit c8f72bcc78
2 changed files with 115 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ import (
"nofx/config"
"nofx/decision"
"nofx/manager"
"nofx/trader"
"strconv"
"strings"
"time"
@@ -109,6 +110,7 @@ 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)
@@ -641,6 +643,113 @@ 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)
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.LoadUserTraders(s.database, userID)
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")