fix:完善aster账户净值和盈亏计算|Improve the calculation of the net value and profit/loss of the aster account (#695)

Co-authored-by: LindenWang <linden@Lindens-MacBookPro-2.local>
This commit is contained in:
Linden
2025-11-07 13:38:39 +08:00
committed by GitHub
parent eb16882282
commit f1f24ad1fa
8 changed files with 88 additions and 71 deletions

View File

@@ -1483,7 +1483,6 @@ func (s *Server) authMiddleware() gin.HandlerFunc {
return return
} }
tokenString := tokenParts[1] tokenString := tokenParts[1]
// 黑名单检查 // 黑名单检查

View File

@@ -82,8 +82,8 @@ type Context struct {
// Decision AI的交易决策 // Decision AI的交易决策
type Decision struct { type Decision struct {
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "update_stop_loss", "update_take_profit", "partial_close", "hold", "wait" Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "update_stop_loss", "update_take_profit", "partial_close", "hold", "wait"
// 开仓参数 // 开仓参数
Leverage int `json:"leverage,omitempty"` Leverage int `json:"leverage,omitempty"`
@@ -92,14 +92,14 @@ type Decision struct {
TakeProfit float64 `json:"take_profit,omitempty"` TakeProfit float64 `json:"take_profit,omitempty"`
// 调整参数(新增) // 调整参数(新增)
NewStopLoss float64 `json:"new_stop_loss,omitempty"` // 用于 update_stop_loss NewStopLoss float64 `json:"new_stop_loss,omitempty"` // 用于 update_stop_loss
NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 update_take_profit NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 update_take_profit
ClosePercentage float64 `json:"close_percentage,omitempty"` // 用于 partial_close (0-100) ClosePercentage float64 `json:"close_percentage,omitempty"` // 用于 partial_close (0-100)
// 通用参数 // 通用参数
Confidence int `json:"confidence,omitempty"` // 信心度 (0-100) Confidence int `json:"confidence,omitempty"` // 信心度 (0-100)
RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险 RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险
Reasoning string `json:"reasoning"` Reasoning string `json:"reasoning"`
} }
// FullDecision AI的完整决策包含思维链 // FullDecision AI的完整决策包含思维链
@@ -691,8 +691,8 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi
// ✅ 验证最小开仓金额(防止数量格式化为 0 的错误) // ✅ 验证最小开仓金额(防止数量格式化为 0 的错误)
// Binance 最小名义价值 10 USDT + 安全边际 // Binance 最小名义价值 10 USDT + 安全边际
const minPositionSizeGeneral = 12.0 // 10 + 20% 安全边际 const minPositionSizeGeneral = 12.0 // 10 + 20% 安全边际
const minPositionSizeBTCETH = 60.0 // BTC/ETH 因价格高和精度限制需要更大金额(更灵活) const minPositionSizeBTCETH = 60.0 // BTC/ETH 因价格高和精度限制需要更大金额(更灵活)
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
if d.PositionSizeUSD < minPositionSizeBTCETH { if d.PositionSizeUSD < minPositionSizeBTCETH {

View File

@@ -33,9 +33,9 @@ func NewTelegramSender(botToken string, chatID int64) (*TelegramSender, error) {
sender := &TelegramSender{ sender := &TelegramSender{
bot: bot, bot: bot,
chatID: chatID, chatID: chatID,
msgChan: make(chan string, 20), // 固定缓冲区大小: 20 msgChan: make(chan string, 20), // 固定缓冲区大小: 20
retryCount: 3, // 固定重试次数: 3 retryCount: 3, // 固定重试次数: 3
retryInterval: 3 * time.Second, // 固定重试间隔: 3秒 retryInterval: 3 * time.Second, // 固定重试间隔: 3秒
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }

View File

@@ -96,7 +96,7 @@ func (client *Client) SetQwenAPIKey(apiKey string, customURL string, customModel
client.Model = customModel client.Model = customModel
log.Printf("🔧 [MCP] Qwen 使用自定义 Model: %s", customModel) log.Printf("🔧 [MCP] Qwen 使用自定义 Model: %s", customModel)
} else { } else {
client.Model = "qwen3-max" client.Model = "qwen3-max"
log.Printf("🔧 [MCP] Qwen 使用默认 Model: %s", client.Model) log.Printf("🔧 [MCP] Qwen 使用默认 Model: %s", client.Model)
} }
// 打印 API Key 的前后各4位用于验证 // 打印 API Key 的前后各4位用于验证

View File

@@ -438,55 +438,78 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) {
return nil, err return nil, err
} }
// 🔍 调试打印原始API响应
log.Printf("🔍 Aster API原始响应: %s", string(body))
// 查找USDT余额 // 查找USDT余额
totalBalance := 0.0
availableBalance := 0.0 availableBalance := 0.0
crossUnPnl := 0.0 crossUnPnl := 0.0
crossWalletBalance := 0.0
foundUSDT := false
for _, bal := range balances { for _, bal := range balances {
// 🔍 调试:打印每条余额记录
log.Printf("🔍 余额记录: %+v", bal)
if asset, ok := bal["asset"].(string); ok && asset == "USDT" { if asset, ok := bal["asset"].(string); ok && asset == "USDT" {
// 🔍 调试打印USDT余额详情 foundUSDT = true
log.Printf("🔍 USDT余额详情: balance=%v, availableBalance=%v, crossUnPnl=%v",
bal["balance"], bal["availableBalance"], bal["crossUnPnl"])
if wb, ok := bal["balance"].(string); ok { // 解析Aster字段参考: https://github.com/asterdex/api-docs
totalBalance, _ = strconv.ParseFloat(wb, 64)
}
if avail, ok := bal["availableBalance"].(string); ok { if avail, ok := bal["availableBalance"].(string); ok {
availableBalance, _ = strconv.ParseFloat(avail, 64) availableBalance, _ = strconv.ParseFloat(avail, 64)
} }
if unpnl, ok := bal["crossUnPnl"].(string); ok { if unpnl, ok := bal["crossUnPnl"].(string); ok {
crossUnPnl, _ = strconv.ParseFloat(unpnl, 64) crossUnPnl, _ = strconv.ParseFloat(unpnl, 64)
} }
if cwb, ok := bal["crossWalletBalance"].(string); ok {
crossWalletBalance, _ = strconv.ParseFloat(cwb, 64)
}
break break
} }
} }
// ✅ Aster API完全兼容Binance API格式 if !foundUSDT {
// balance字段 = wallet balance不包含未实现盈亏 log.Printf("⚠️ 未找到USDT资产记录")
// crossUnPnl = unrealized profit未实现盈亏 }
// crossWalletBalance = balance + crossUnPnl全仓钱包余额包含盈亏
//
// 参考Binance官方文档
// - Account Information V2: marginBalance = walletBalance + unrealizedProfit
// - Balance V3: crossWalletBalance = balance + crossUnPnl
log.Printf("✓ Aster API返回: 钱包余额=%.2f, 未实现盈亏=%.2f, 可用余额=%.2f", // 获取持仓计算保证金占用和真实未实现盈亏
totalBalance, positions, err := t.GetPositions()
crossUnPnl, if err != nil {
availableBalance) log.Printf("⚠️ 获取持仓信息失败: %v", err)
// fallback: 无法获取持仓时使用简单计算
return map[string]interface{}{
"totalWalletBalance": crossWalletBalance,
"availableBalance": availableBalance,
"totalUnrealizedProfit": crossUnPnl,
}, nil
}
// ⚠️ 关键修复:从持仓中累加真正的未实现盈亏
// Aster 的 crossUnPnl 字段不准确,需要从持仓数据中重新计算
totalMarginUsed := 0.0
realUnrealizedPnl := 0.0
for _, pos := range positions {
markPrice := pos["markPrice"].(float64)
quantity := pos["positionAmt"].(float64)
if quantity < 0 {
quantity = -quantity
}
unrealizedPnl := pos["unRealizedProfit"].(float64)
realUnrealizedPnl += unrealizedPnl
leverage := 10
if lev, ok := pos["leverage"].(float64); ok {
leverage = int(lev)
}
marginUsed := (quantity * markPrice) / float64(leverage)
totalMarginUsed += marginUsed
}
// ✅ Aster 正确计算方式:
// 总净值 = 可用余额 + 保证金占用
// 钱包余额 = 总净值 - 未实现盈亏
// 未实现盈亏 = 从持仓累加计算不使用API的crossUnPnl
totalEquity := availableBalance + totalMarginUsed
totalWalletBalance := totalEquity - realUnrealizedPnl
// 返回与Binance相同的字段名确保AutoTrader能正确解析
return map[string]interface{}{ return map[string]interface{}{
"totalWalletBalance": totalBalance, // 钱包余额(不含未实现盈亏) "totalWalletBalance": totalWalletBalance, // 钱包余额(不含未实现盈亏)
"availableBalance": availableBalance, "availableBalance": availableBalance, // 可用余额
"totalUnrealizedProfit": crossUnPnl, // 未实现盈亏 "totalUnrealizedProfit": realUnrealizedPnl, // 未实现盈亏(从持仓累加)
}, nil }, nil
} }
@@ -1010,8 +1033,6 @@ func (t *AsterTrader) SetTakeProfit(symbol string, positionSide string, quantity
return err return err
} }
// CancelStopLossOrders 仅取消止损单(不影响止盈单) // CancelStopLossOrders 仅取消止损单(不影响止盈单)
func (t *AsterTrader) CancelStopLossOrders(symbol string) error { func (t *AsterTrader) CancelStopLossOrders(symbol string) error {
// 获取该币种的所有未完成订单 // 获取该币种的所有未完成订单

View File

@@ -97,16 +97,16 @@ type AutoTrader struct {
lastResetTime time.Time lastResetTime time.Time
stopUntil time.Time stopUntil time.Time
isRunning bool isRunning bool
startTime time.Time // 系统启动时间 startTime time.Time // 系统启动时间
callCount int // AI调用次数 callCount int // AI调用次数
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒)
stopMonitorCh chan struct{} // 用于停止监控goroutine stopMonitorCh chan struct{} // 用于停止监控goroutine
monitorWg sync.WaitGroup // 用于等待监控goroutine结束 monitorWg sync.WaitGroup // 用于等待监控goroutine结束
peakPnLCache map[string]float64 // 最高收益缓存 (symbol -> 峰值盈亏百分比) peakPnLCache map[string]float64 // 最高收益缓存 (symbol -> 峰值盈亏百分比)
peakPnLCacheMutex sync.RWMutex // 缓存读写锁 peakPnLCacheMutex sync.RWMutex // 缓存读写锁
lastBalanceSyncTime time.Time // 上次余额同步时间 lastBalanceSyncTime time.Time // 上次余额同步时间
database interface{} // 数据库引用(用于自动更新余额) database interface{} // 数据库引用(用于自动更新余额)
userID string // 用户ID userID string // 用户ID
} }
// NewAutoTrader 创建自动交易器 // NewAutoTrader 创建自动交易器
@@ -436,7 +436,7 @@ func (at *AutoTrader) runCycle() error {
}) })
} }
log.Print(strings.Repeat("=", 70)) log.Print(strings.Repeat("=", 70))
for _, coin := range ctx.CandidateCoins { for _, coin := range ctx.CandidateCoins {
record.CandidateCoins = append(record.CandidateCoins, coin.Symbol) record.CandidateCoins = append(record.CandidateCoins, coin.Symbol)
} }
@@ -465,11 +465,11 @@ func (at *AutoTrader) runCycle() error {
// 打印系统提示词和AI思维链即使有错误也要输出以便调试 // 打印系统提示词和AI思维链即使有错误也要输出以便调试
if decision != nil { if decision != nil {
log.Print("\n" + strings.Repeat("=", 70) + "\n") log.Print("\n" + strings.Repeat("=", 70) + "\n")
log.Printf("📋 系统提示词 [模板: %s] (错误情况)", at.systemPromptTemplate) log.Printf("📋 系统提示词 [模板: %s] (错误情况)", at.systemPromptTemplate)
log.Println(strings.Repeat("=", 70)) log.Println(strings.Repeat("=", 70))
log.Println(decision.SystemPrompt) log.Println(decision.SystemPrompt)
log.Println(strings.Repeat("=", 70)) log.Println(strings.Repeat("=", 70))
if decision.CoTTrace != "" { if decision.CoTTrace != "" {
log.Print("\n" + strings.Repeat("-", 70) + "\n") log.Print("\n" + strings.Repeat("-", 70) + "\n")
@@ -508,9 +508,9 @@ func (at *AutoTrader) runCycle() error {
// } // }
// } // }
log.Println() log.Println()
log.Print(strings.Repeat("-", 70)) log.Print(strings.Repeat("-", 70))
// 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限) // 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限)
log.Print(strings.Repeat("-", 70)) log.Print(strings.Repeat("-", 70))
// 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限) // 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限)
sortedDecisions := sortDecisionsByPriority(decision.Decisions) sortedDecisions := sortDecisionsByPriority(decision.Decisions)

View File

@@ -491,8 +491,6 @@ func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string]
return result, nil return result, nil
} }
// CancelStopLossOrders 仅取消止损单(不影响止盈单) // CancelStopLossOrders 仅取消止损单(不影响止盈单)
func (t *FuturesTrader) CancelStopLossOrders(symbol string) error { func (t *FuturesTrader) CancelStopLossOrders(symbol string) error {
// 获取该币种的所有未完成订单 // 获取该币种的所有未完成订单

View File

@@ -175,10 +175,10 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
// 原因Spot 和 Perpetuals 是独立帐户,需手动 ClassTransfer 才能转账 // 原因Spot 和 Perpetuals 是独立帐户,需手动 ClassTransfer 才能转账
totalWalletBalance := walletBalanceWithoutUnrealized + spotUSDCBalance totalWalletBalance := walletBalanceWithoutUnrealized + spotUSDCBalance
result["totalWalletBalance"] = totalWalletBalance // 总资产Perp + Spot result["totalWalletBalance"] = totalWalletBalance // 总资产Perp + Spot
result["availableBalance"] = availableBalance // 可用余额(仅 Perpetuals不含 Spot result["availableBalance"] = availableBalance // 可用余额(仅 Perpetuals不含 Spot
result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏(仅来自 Perpetuals result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏(仅来自 Perpetuals
result["spotBalance"] = spotUSDCBalance // Spot 现货余额(单独返回) result["spotBalance"] = spotUSDCBalance // Spot 现货余额(单独返回)
log.Printf("✓ Hyperliquid 完整账户:") log.Printf("✓ Hyperliquid 完整账户:")
log.Printf(" • Spot 现货余额: %.2f USDC (需手动转账到 Perpetuals 才能开仓)", spotUSDCBalance) log.Printf(" • Spot 现货余额: %.2f USDC (需手动转账到 Perpetuals 才能开仓)", spotUSDCBalance)
@@ -551,7 +551,6 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str
// CancelStopOrders 取消该币种的止盈/止 // CancelStopOrders 取消该币种的止盈/止
// CancelStopLossOrders 仅取消止损单Hyperliquid 暂无法区分止损和止盈,取消所有) // CancelStopLossOrders 仅取消止损单Hyperliquid 暂无法区分止损和止盈,取消所有)
func (t *HyperliquidTrader) CancelStopLossOrders(symbol string) error { func (t *HyperliquidTrader) CancelStopLossOrders(symbol string) error {
// Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段 // Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段