fix(api): query actual exchange balance when creating trader

Problem:
- Users could input arbitrary initial balance when creating traders
- This didn't reflect the actual available balance in exchange account
- Could lead to incorrect position sizing and risk calculations

Solution:
- Before creating trader, query exchange API for actual balance
- Use GetBalance() from respective trader implementation:
  * Binance: NewFuturesTrader + GetBalance()
  * Hyperliquid: NewHyperliquidTrader + GetBalance()
  * Aster: NewAsterTrader + GetBalance()
- Extract 'available_balance' or 'balance' from response
- Override user input with actual balance
- Fallback to user input if query fails

Changes:
- Added 'nofx/trader' import
- Query GetExchanges() to find matching exchange config
- Create temporary trader instance based on exchange type
- Call GetBalance() to fetch actual available balance
- Use actualBalance instead of req.InitialBalance
- Comprehensive error handling with fallback logic

Benefits:
-  Ensures accurate initial balance matches exchange account
-  Prevents user errors in balance input
-  Improves position sizing accuracy
-  Maintains data integrity between system and exchange

Example logs:
✓ 查询到交易所实际余额: 150.00 USDT (用户输入: 100.00 USDT)
⚠️ 查询交易所余额失败,使用用户输入的初始资金: connection timeout

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ZhouYongyou
2025-11-03 20:30:00 +08:00
parent 5649cb7496
commit 2b9c4d2d6b

View File

@@ -9,6 +9,7 @@ import (
"nofx/config"
"nofx/decision"
"nofx/manager"
"nofx/trader"
"strconv"
"strings"
"time"
@@ -347,6 +348,73 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
scanIntervalMinutes = 3 // 默认3分钟
}
// ✨ 查询交易所实际余额,覆盖用户输入
actualBalance := req.InitialBalance // 默认使用用户输入
exchanges, err := s.database.GetExchanges(userID)
if err != nil {
log.Printf("⚠️ 获取交易所配置失败,使用用户输入的初始资金: %v", err)
}
// 查找匹配的交易所配置
var exchangeCfg *config.ExchangeConfig
for _, ex := range exchanges {
if ex.ID == req.ExchangeID {
exchangeCfg = ex
break
}
}
if exchangeCfg == nil {
log.Printf("⚠️ 未找到交易所 %s 的配置,使用用户输入的初始资金", req.ExchangeID)
} else if !exchangeCfg.Enabled {
log.Printf("⚠️ 交易所 %s 未启用,使用用户输入的初始资金", req.ExchangeID)
} else {
// 根据交易所类型创建临时 trader 查询余额
var tempTrader trader.Trader
var createErr error
switch req.ExchangeID {
case "binance":
tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey)
case "hyperliquid":
tempTrader, createErr = trader.NewHyperliquidTrader(
exchangeCfg.APIKey, // private key
exchangeCfg.HyperliquidWalletAddr,
exchangeCfg.Testnet,
)
case "aster":
tempTrader, createErr = trader.NewAsterTrader(
exchangeCfg.AsterUser,
exchangeCfg.AsterSigner,
exchangeCfg.AsterPrivateKey,
)
default:
log.Printf("⚠️ 不支持的交易所类型: %s使用用户输入的初始资金", req.ExchangeID)
}
if createErr != nil {
log.Printf("⚠️ 创建临时 trader 失败,使用用户输入的初始资金: %v", createErr)
} else if tempTrader != nil {
// 查询实际余额
balanceInfo, balanceErr := tempTrader.GetBalance()
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)
} else {
log.Printf("⚠️ 无法从余额信息中提取可用余额,使用用户输入的初始资金")
}
}
}
}
// 创建交易员配置(数据库实体)
trader := &config.TraderRecord{
ID: traderID,
@@ -354,7 +422,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
Name: req.Name,
AIModelID: req.AIModelID,
ExchangeID: req.ExchangeID,
InitialBalance: req.InitialBalance,
InitialBalance: actualBalance, // 使用实际查询的余额
BTCETHLeverage: btcEthLeverage,
AltcoinLeverage: altcoinLeverage,
TradingSymbols: req.TradingSymbols,
@@ -369,7 +437,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
}
// 保存到数据库
err := s.database.CreateTrader(trader)
err = s.database.CreateTrader(traderRecord)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建交易员失败: %v", err)})
return