From f1f24ad1fa5e732b13f4f76445addfccdd57d897 Mon Sep 17 00:00:00 2001 From: Linden <30583749+LindenWang01@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:38:39 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E5=AE=8C=E5=96=84aster=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E5=87=80=E5=80=BC=E5=92=8C=E7=9B=88=E4=BA=8F=E8=AE=A1=E7=AE=97?= =?UTF-8?q?|Improve=20the=20calculation=20of=20the=20net=20value=20and=20p?= =?UTF-8?q?rofit/loss=20of=20the=20aster=20account=20(#695)=20Co-authored-?= =?UTF-8?q?by:=20LindenWang=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server.go | 1 - decision/engine.go | 20 ++++----- logger/telegram_sender.go | 6 +-- mcp/client.go | 2 +- trader/aster_trader.go | 83 ++++++++++++++++++++++-------------- trader/auto_trader.go | 36 ++++++++-------- trader/binance_futures.go | 2 - trader/hyperliquid_trader.go | 9 ++-- 8 files changed, 88 insertions(+), 71 deletions(-) diff --git a/api/server.go b/api/server.go index fb350c4e..1ae567ec 100644 --- a/api/server.go +++ b/api/server.go @@ -1483,7 +1483,6 @@ func (s *Server) authMiddleware() gin.HandlerFunc { return } - tokenString := tokenParts[1] // 黑名单检查 diff --git a/decision/engine.go b/decision/engine.go index 98a56f73..a84dbb5c 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -82,8 +82,8 @@ type Context struct { // Decision AI的交易决策 type Decision struct { - 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" + 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" // 开仓参数 Leverage int `json:"leverage,omitempty"` @@ -92,14 +92,14 @@ type Decision struct { TakeProfit float64 `json:"take_profit,omitempty"` // 调整参数(新增) - NewStopLoss float64 `json:"new_stop_loss,omitempty"` // 用于 update_stop_loss - NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 update_take_profit - ClosePercentage float64 `json:"close_percentage,omitempty"` // 用于 partial_close (0-100) + NewStopLoss float64 `json:"new_stop_loss,omitempty"` // 用于 update_stop_loss + NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 update_take_profit + ClosePercentage float64 `json:"close_percentage,omitempty"` // 用于 partial_close (0-100) // 通用参数 - Confidence int `json:"confidence,omitempty"` // 信心度 (0-100) - RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险 - Reasoning string `json:"reasoning"` + Confidence int `json:"confidence,omitempty"` // 信心度 (0-100) + RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险 + Reasoning string `json:"reasoning"` } // FullDecision AI的完整决策(包含思维链) @@ -691,8 +691,8 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi // ✅ 验证最小开仓金额(防止数量格式化为 0 的错误) // Binance 最小名义价值 10 USDT + 安全边际 - const minPositionSizeGeneral = 12.0 // 10 + 20% 安全边际 - const minPositionSizeBTCETH = 60.0 // BTC/ETH 因价格高和精度限制需要更大金额(更灵活) + const minPositionSizeGeneral = 12.0 // 10 + 20% 安全边际 + const minPositionSizeBTCETH = 60.0 // BTC/ETH 因价格高和精度限制需要更大金额(更灵活) if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { if d.PositionSizeUSD < minPositionSizeBTCETH { diff --git a/logger/telegram_sender.go b/logger/telegram_sender.go index 8013dc18..6658d9f2 100644 --- a/logger/telegram_sender.go +++ b/logger/telegram_sender.go @@ -33,9 +33,9 @@ func NewTelegramSender(botToken string, chatID int64) (*TelegramSender, error) { sender := &TelegramSender{ bot: bot, chatID: chatID, - msgChan: make(chan string, 20), // 固定缓冲区大小: 20 - retryCount: 3, // 固定重试次数: 3 - retryInterval: 3 * time.Second, // 固定重试间隔: 3秒 + msgChan: make(chan string, 20), // 固定缓冲区大小: 20 + retryCount: 3, // 固定重试次数: 3 + retryInterval: 3 * time.Second, // 固定重试间隔: 3秒 stopChan: make(chan struct{}), } diff --git a/mcp/client.go b/mcp/client.go index 14f49eae..0f785534 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -96,7 +96,7 @@ func (client *Client) SetQwenAPIKey(apiKey string, customURL string, customModel client.Model = customModel log.Printf("🔧 [MCP] Qwen 使用自定义 Model: %s", customModel) } else { - client.Model = "qwen3-max" + client.Model = "qwen3-max" log.Printf("🔧 [MCP] Qwen 使用默认 Model: %s", client.Model) } // 打印 API Key 的前后各4位用于验证 diff --git a/trader/aster_trader.go b/trader/aster_trader.go index 49f42530..362bb894 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -438,55 +438,78 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) { return nil, err } - // 🔍 调试:打印原始API响应 - log.Printf("🔍 Aster API原始响应: %s", string(body)) - // 查找USDT余额 - totalBalance := 0.0 availableBalance := 0.0 crossUnPnl := 0.0 + crossWalletBalance := 0.0 + foundUSDT := false for _, bal := range balances { - // 🔍 调试:打印每条余额记录 - log.Printf("🔍 余额记录: %+v", bal) - if asset, ok := bal["asset"].(string); ok && asset == "USDT" { - // 🔍 调试:打印USDT余额详情 - log.Printf("🔍 USDT余额详情: balance=%v, availableBalance=%v, crossUnPnl=%v", - bal["balance"], bal["availableBalance"], bal["crossUnPnl"]) + foundUSDT = true - if wb, ok := bal["balance"].(string); ok { - totalBalance, _ = strconv.ParseFloat(wb, 64) - } + // 解析Aster字段(参考: https://github.com/asterdex/api-docs) if avail, ok := bal["availableBalance"].(string); ok { availableBalance, _ = strconv.ParseFloat(avail, 64) } if unpnl, ok := bal["crossUnPnl"].(string); ok { crossUnPnl, _ = strconv.ParseFloat(unpnl, 64) } + if cwb, ok := bal["crossWalletBalance"].(string); ok { + crossWalletBalance, _ = strconv.ParseFloat(cwb, 64) + } break } } - // ✅ Aster API完全兼容Binance API格式 - // balance字段 = wallet balance(不包含未实现盈亏) - // crossUnPnl = unrealized profit(未实现盈亏) - // crossWalletBalance = balance + crossUnPnl(全仓钱包余额,包含盈亏) - // - // 参考Binance官方文档: - // - Account Information V2: marginBalance = walletBalance + unrealizedProfit - // - Balance V3: crossWalletBalance = balance + crossUnPnl + if !foundUSDT { + log.Printf("⚠️ 未找到USDT资产记录!") + } - log.Printf("✓ Aster API返回: 钱包余额=%.2f, 未实现盈亏=%.2f, 可用余额=%.2f", - totalBalance, - crossUnPnl, - availableBalance) + // 获取持仓计算保证金占用和真实未实现盈亏 + positions, err := t.GetPositions() + if err != nil { + 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{}{ - "totalWalletBalance": totalBalance, // 钱包余额(不含未实现盈亏) - "availableBalance": availableBalance, - "totalUnrealizedProfit": crossUnPnl, // 未实现盈亏 + "totalWalletBalance": totalWalletBalance, // 钱包余额(不含未实现盈亏) + "availableBalance": availableBalance, // 可用余额 + "totalUnrealizedProfit": realUnrealizedPnl, // 未实现盈亏(从持仓累加) }, nil } @@ -1010,8 +1033,6 @@ func (t *AsterTrader) SetTakeProfit(symbol string, positionSide string, quantity return err } - - // CancelStopLossOrders 仅取消止损单(不影响止盈单) func (t *AsterTrader) CancelStopLossOrders(symbol string) error { // 获取该币种的所有未完成订单 diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 54c1b1b7..059313b5 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -97,16 +97,16 @@ type AutoTrader struct { lastResetTime time.Time stopUntil time.Time isRunning bool - startTime time.Time // 系统启动时间 - callCount int // AI调用次数 - positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) - stopMonitorCh chan struct{} // 用于停止监控goroutine - monitorWg sync.WaitGroup // 用于等待监控goroutine结束 - peakPnLCache map[string]float64 // 最高收益缓存 (symbol -> 峰值盈亏百分比) - peakPnLCacheMutex sync.RWMutex // 缓存读写锁 - lastBalanceSyncTime time.Time // 上次余额同步时间 - database interface{} // 数据库引用(用于自动更新余额) - userID string // 用户ID + startTime time.Time // 系统启动时间 + callCount int // AI调用次数 + positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) + stopMonitorCh chan struct{} // 用于停止监控goroutine + monitorWg sync.WaitGroup // 用于等待监控goroutine结束 + peakPnLCache map[string]float64 // 最高收益缓存 (symbol -> 峰值盈亏百分比) + peakPnLCacheMutex sync.RWMutex // 缓存读写锁 + lastBalanceSyncTime time.Time // 上次余额同步时间 + database interface{} // 数据库引用(用于自动更新余额) + userID string // 用户ID } // 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 { record.CandidateCoins = append(record.CandidateCoins, coin.Symbol) } @@ -465,11 +465,11 @@ func (at *AutoTrader) runCycle() error { // 打印系统提示词和AI思维链(即使有错误,也要输出以便调试) if decision != nil { - log.Print("\n" + strings.Repeat("=", 70) + "\n") - log.Printf("📋 系统提示词 [模板: %s] (错误情况)", at.systemPromptTemplate) - log.Println(strings.Repeat("=", 70)) - log.Println(decision.SystemPrompt) - log.Println(strings.Repeat("=", 70)) + log.Print("\n" + strings.Repeat("=", 70) + "\n") + log.Printf("📋 系统提示词 [模板: %s] (错误情况)", at.systemPromptTemplate) + log.Println(strings.Repeat("=", 70)) + log.Println(decision.SystemPrompt) + log.Println(strings.Repeat("=", 70)) if decision.CoTTrace != "" { log.Print("\n" + strings.Repeat("-", 70) + "\n") @@ -508,9 +508,9 @@ func (at *AutoTrader) runCycle() error { // } // } log.Println() - log.Print(strings.Repeat("-", 70)) + log.Print(strings.Repeat("-", 70)) // 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限) - log.Print(strings.Repeat("-", 70)) + log.Print(strings.Repeat("-", 70)) // 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限) sortedDecisions := sortDecisionsByPriority(decision.Decisions) diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 9ba1acd6..3fc9713f 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -491,8 +491,6 @@ func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string] return result, nil } - - // CancelStopLossOrders 仅取消止损单(不影响止盈单) func (t *FuturesTrader) CancelStopLossOrders(symbol string) error { // 获取该币种的所有未完成订单 diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index 812581f2..1c4ad954 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -175,10 +175,10 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { // 原因:Spot 和 Perpetuals 是独立帐户,需手动 ClassTransfer 才能转账 totalWalletBalance := walletBalanceWithoutUnrealized + spotUSDCBalance - result["totalWalletBalance"] = totalWalletBalance // 总资产(Perp + Spot) - result["availableBalance"] = availableBalance // 可用余额(仅 Perpetuals,不含 Spot) - result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏(仅来自 Perpetuals) - result["spotBalance"] = spotUSDCBalance // Spot 现货余额(单独返回) + result["totalWalletBalance"] = totalWalletBalance // 总资产(Perp + Spot) + result["availableBalance"] = availableBalance // 可用余额(仅 Perpetuals,不含 Spot) + result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏(仅来自 Perpetuals) + result["spotBalance"] = spotUSDCBalance // Spot 现货余额(单独返回) log.Printf("✓ Hyperliquid 完整账户:") log.Printf(" • Spot 现货余额: %.2f USDC (需手动转账到 Perpetuals 才能开仓)", spotUSDCBalance) @@ -551,7 +551,6 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str // CancelStopOrders 取消该币种的止盈/止 - // CancelStopLossOrders 仅取消止损单(Hyperliquid 暂无法区分止损和止盈,取消所有) func (t *HyperliquidTrader) CancelStopLossOrders(symbol string) error { // Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段