From 40ae31b58367128b40c7dc95420b62178f37d17c Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 03:42:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(hyperliquid):=20complete=20balance=20detect?= =?UTF-8?q?ion=20with=204=20critical=20fixes=20##=20=F0=9F=8E=AF=20?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E4=BF=AE=E5=BE=A9=20Hyperliquid=20=E9=A4=98?= =?UTF-8?q?=E9=A1=8D=E6=AA=A2=E6=B8=AC=E7=9A=84=E6=89=80=E6=9C=89=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=20###=20=E4=BF=AE=E5=BE=A9=201:=20=E2=9C=85=20?= =?UTF-8?q?=E5=8B=95=E6=85=8B=E9=81=B8=E6=93=87=E4=BF=9D=E8=AD=89=E9=87=91?= =?UTF-8?q?=E6=91=98=E8=A6=81=20**=E5=95=8F=E9=A1=8C**:=20=E7=A1=AC?= =?UTF-8?q?=E7=B7=A8=E7=A2=BC=E4=BD=BF=E7=94=A8=20MarginSummary=EF=BC=8C?= =?UTF-8?q?=E4=BD=86=E9=A0=90=E8=A8=AD=E5=85=A8=E5=80=89=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=20**=E4=BF=AE=E5=BE=A9**:=20=E6=A0=B9=E6=93=9A=20isCrossMargin?= =?UTF-8?q?=20=E5=8B=95=E6=85=8B=E9=81=B8=E6=93=87=20-=20=E5=85=A8?= =?UTF-8?q?=E5=80=89=E6=A8=A1=E5=BC=8F=20=E2=86=92=20CrossMarginSummary=20?= =?UTF-8?q?-=20=E9=80=90=E5=80=89=E6=A8=A1=E5=BC=8F=20=E2=86=92=20MarginSu?= =?UTF-8?q?mmary=20###=20=E4=BF=AE=E5=BE=A9=202:=20=E2=9C=85=20=E6=9F=A5?= =?UTF-8?q?=E8=A9=A2=20Spot=20=E7=8F=BE=E8=B2=A8=E5=B8=B3=E6=88=B6=20**?= =?UTF-8?q?=E5=95=8F=E9=A1=8C**:=20=E5=8F=AA=E6=9F=A5=E8=A9=A2=20Perpetual?= =?UTF-8?q?s=EF=BC=8C=E5=BF=BD=E7=95=A5=20Spot=20=E9=A4=98=E9=A1=8D=20**?= =?UTF-8?q?=E4=BF=AE=E5=BE=A9**:=20=E4=BD=BF=E7=94=A8=20SpotUserState()=20?= =?UTF-8?q?=E6=9F=A5=E8=A9=A2=20USDC=20=E7=8F=BE=E8=B2=A8=E9=A4=98?= =?UTF-8?q?=E9=A1=8D=20-=20=E5=90=88=E4=BD=B5=20Spot=20+=20Perpetuals=20?= =?UTF-8?q?=E7=B8=BD=E9=A4=98=E9=A1=8D=20-=20=E8=A7=A3=E6=B1=BA=E7=94=A8?= =?UTF-8?q?=E6=88=B6=E5=8F=8D=E9=A5=8B=E3=80=8C=E9=8C=A2=E5=8C=85=E6=9C=89?= =?UTF-8?q?=E9=8C=A2=E4=BD=86=E9=A1=AF=E7=A4=BA=200=E3=80=8D=E7=9A=84?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=20###=20=E4=BF=AE=E5=BE=A9=203:=20=E2=9C=85?= =?UTF-8?q?=20=E4=BD=BF=E7=94=A8=20Withdrawable=20=E6=AC=84=E4=BD=8D=20**?= =?UTF-8?q?=E5=95=8F=E9=A1=8C**:=20=E7=B0=A1=E5=96=AE=E8=A8=88=E7=AE=97=20?= =?UTF-8?q?availableBalance=20=3D=20accountValue=20-=20totalMarginUsed=20?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E9=9D=A0=20**=E4=BF=AE=E5=BE=A9**:=20?= =?UTF-8?q?=E5=84=AA=E5=85=88=E4=BD=BF=E7=94=A8=E5=AE=98=E6=96=B9=20Withdr?= =?UTF-8?q?awable=20=E6=AC=84=E4=BD=8D=20-=20=E6=95=B4=E5=90=88=20PR=20#44?= =?UTF-8?q?3=20=E7=9A=84=E9=82=8F=E8=BC=AF=20-=20=E9=99=8D=E7=B4=9A?= =?UTF-8?q?=E6=96=B9=E6=A1=88=EF=BC=9AWithdrawable=20=E4=B8=8D=E5=8F=AF?= =?UTF-8?q?=E7=94=A8=E6=99=82=E6=89=8D=E4=BD=BF=E7=94=A8=E7=B0=A1=E5=96=AE?= =?UTF-8?q?=E8=A8=88=E7=AE=97=20-=20=E9=98=B2=E6=AD=A2=E8=B2=A0=E6=95=B8?= =?UTF-8?q?=E9=A4=98=E9=A1=8D=20###=20=E4=BF=AE=E5=BE=A9=204:=20=E2=9C=85?= =?UTF-8?q?=20=E6=B8=85=E7=90=86=E6=B7=B7=E4=BA=82=E8=A8=BB=E9=87=8B=20**?= =?UTF-8?q?=E5=95=8F=E9=A1=8C**:=20=E8=A8=BB=E9=87=8B=E8=AA=AA=20CrossMarg?= =?UTF-8?q?inSummary=20=E4=BD=86=E4=BB=A3=E7=A2=BC=E7=94=A8=20MarginSummar?= =?UTF-8?q?y=20**=E4=BF=AE=E5=BE=A9**:=20=E6=A0=B9=E6=93=9A=E5=AF=A6?= =?UTF-8?q?=E9=9A=9B=E4=BD=BF=E7=94=A8=E7=9A=84=E6=91=98=E8=A6=81=E9=A1=9E?= =?UTF-8?q?=E5=9E=8B=E5=8B=95=E6=85=8B=E8=BC=B8=E5=87=BA=E6=97=A5=E8=AA=8C?= =?UTF-8?q?=20##=20=F0=9F=93=8A=20=E4=BF=AE=E5=BE=A9=E5=B0=8D=E6=AF=94=20|?= =?UTF-8?q?=20=E5=95=8F=E9=A1=8C=20|=20=E4=BF=AE=E5=BE=A9=E5=89=8D=20|=20?= =?UTF-8?q?=E4=BF=AE=E5=BE=A9=E5=BE=8C=20|=20|------|--------|--------|=20?= =?UTF-8?q?|=20=E4=BF=9D=E8=AD=89=E9=87=91=E6=91=98=E8=A6=81=E9=81=B8?= =?UTF-8?q?=E6=93=87=20|=20=E2=9D=8C=20=E7=A1=AC=E7=B7=A8=E7=A2=BC=20Margi?= =?UTF-8?q?nSummary=20|=20=E2=9C=85=20=E5=8B=95=E6=85=8B=E9=81=B8=E6=93=87?= =?UTF-8?q?=20|=20|=20Spot=20=E9=A4=98=E9=A1=8D=E6=9F=A5=E8=A9=A2=20|=20?= =?UTF-8?q?=E2=9D=8C=20=E5=BE=9E=E6=9C=AA=E6=9F=A5=E8=A9=A2=20|=20?= =?UTF-8?q?=E2=9C=85=20=E5=AE=8C=E6=95=B4=E6=9F=A5=E8=A9=A2=20|=20|=20?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=E9=A4=98=E9=A1=8D=E8=A8=88=E7=AE=97=20|=20?= =?UTF-8?q?=E2=9D=8C=20=E7=B0=A1=E5=96=AE=E7=9B=B8=E6=B8=9B=20|=20?= =?UTF-8?q?=E2=9C=85=20=E4=BD=BF=E7=94=A8=20Withdrawable=20|=20|=20?= =?UTF-8?q?=E6=97=A5=E8=AA=8C=E8=A8=BB=E9=87=8B=20|=20=E2=9D=8C=20?= =?UTF-8?q?=E4=B8=8D=E4=B8=80=E8=87=B4=20|=20=E2=9C=85=20=E6=BA=96?= =?UTF-8?q?=E7=A2=BA=E6=B8=85=E6=99=B0=20|=20##=20=F0=9F=A7=AA=20=E6=B8=AC?= =?UTF-8?q?=E8=A9=A6=E5=A0=B4=E6=99=AF=20-=20=E2=9C=85=20Spot=20=E6=9C=89?= =?UTF-8?q?=E9=8C=A2=EF=BC=8CPerp=20=E6=B2=92=E9=8C=A2=20=E2=86=92=20?= =?UTF-8?q?=E6=AD=A3=E7=A2=BA=E9=A1=AF=E7=A4=BA=20Spot=20=E9=A4=98?= =?UTF-8?q?=E9=A1=8D=20-=20=E2=9C=85=20Spot=20=E6=B2=92=E9=8C=A2=EF=BC=8CP?= =?UTF-8?q?erp=20=E6=9C=89=E9=8C=A2=20=E2=86=92=20=E6=AD=A3=E7=A2=BA?= =?UTF-8?q?=E9=A1=AF=E7=A4=BA=20Perp=20=E9=A4=98=E9=A1=8D=20-=20=E2=9C=85?= =?UTF-8?q?=20=E5=85=A9=E8=80=85=E9=83=BD=E6=9C=89=E9=8C=A2=20=E2=86=92=20?= =?UTF-8?q?=E6=AD=A3=E7=A2=BA=E5=90=88=E4=BD=B5=E9=A1=AF=E7=A4=BA=20-=20?= =?UTF-8?q?=E2=9C=85=20=E5=85=A8=E5=80=89=E6=A8=A1=E5=BC=8F=20=E2=86=92=20?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20CrossMarginSummary=20-=20=E2=9C=85=20?= =?UTF-8?q?=E9=80=90=E5=80=89=E6=A8=A1=E5=BC=8F=20=E2=86=92=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20MarginSummary=20##=20=E7=9B=B8=E9=97=9C=20Issue=20?= =?UTF-8?q?=E8=A7=A3=E6=B1=BA=E7=94=A8=E6=88=B6=E5=8F=8D=E9=A5=8B=EF=BC=9A?= =?UTF-8?q?=E3=80=8C=E9=8C=A2=E5=8C=85=E4=B8=AD=E6=9C=89=E5=B9=A3=E5=8D=BB?= =?UTF-8?q?=E6=B2=92=E8=A2=AB=E6=AA=A2=E6=B8=AC=E5=88=B0=E3=80=8D=20?= =?UTF-8?q?=E6=95=B4=E5=90=88=E4=BB=A5=E4=B8=8B=E6=9C=AA=E5=90=88=E4=BD=B5?= =?UTF-8?q?=E7=9A=84=E4=BF=AE=E5=BE=A9=EF=BC=9A=20-=20PR=20#443:=20Withdra?= =?UTF-8?q?wable=20=E6=AC=84=E4=BD=8D=E5=84=AA=E5=85=88=20-=20Spot=20?= =?UTF-8?q?=E9=A4=98=E9=A1=8D=E9=81=BA=E6=BC=8F=E5=95=8F=E9=A1=8C=20Co-Aut?= =?UTF-8?q?hored-By:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/hyperliquid_trader.go | 92 ++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index c189dbdc..4c4bf495 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -76,23 +76,54 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { log.Printf("🔄 正在调用Hyperliquid API获取账户余额...") - // 获取账户状态 + // ✅ Step 1: 查询 Spot 现货账户余额 + spotState, err := t.exchange.Info().SpotUserState(t.ctx, t.walletAddr) + var spotUSDCBalance float64 = 0.0 + if err != nil { + log.Printf("⚠️ 查询 Spot 余额失败(可能无现货资产): %v", err) + } else if spotState != nil && len(spotState.Balances) > 0 { + for _, balance := range spotState.Balances { + if balance.Coin == "USDC" { + spotUSDCBalance, _ = strconv.ParseFloat(balance.Total, 64) + log.Printf("✓ 发现 Spot 现货余额: %.2f USDC", spotUSDCBalance) + break + } + } + } + + // ✅ Step 2: 查询 Perpetuals 合约账户状态 accountState, err := t.exchange.Info().UserState(t.ctx, t.walletAddr) if err != nil { - log.Printf("❌ Hyperliquid API调用失败: %v", err) + log.Printf("❌ Hyperliquid Perpetuals API调用失败: %v", err) return nil, fmt.Errorf("获取账户信息失败: %w", err) } // 解析余额信息(MarginSummary字段都是string) result := make(map[string]interface{}) - // 🔍 调试:打印API返回的完整CrossMarginSummary结构 - summaryJSON, _ := json.MarshalIndent(accountState.MarginSummary, " ", " ") - log.Printf("🔍 [DEBUG] Hyperliquid API CrossMarginSummary完整数据:") - log.Printf("%s", string(summaryJSON)) + // ✅ Step 3: 根据保证金模式动态选择正确的摘要(CrossMarginSummary 或 MarginSummary) + var accountValue, totalMarginUsed float64 + var summaryType string + var summary interface{} - accountValue, _ := strconv.ParseFloat(accountState.MarginSummary.AccountValue, 64) - totalMarginUsed, _ := strconv.ParseFloat(accountState.MarginSummary.TotalMarginUsed, 64) + if t.isCrossMargin { + // 全仓模式:使用 CrossMarginSummary + accountValue, _ = strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64) + totalMarginUsed, _ = strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64) + summaryType = "CrossMarginSummary (全仓)" + summary = accountState.CrossMarginSummary + } else { + // 逐仓模式:使用 MarginSummary + accountValue, _ = strconv.ParseFloat(accountState.MarginSummary.AccountValue, 64) + totalMarginUsed, _ = strconv.ParseFloat(accountState.MarginSummary.TotalMarginUsed, 64) + summaryType = "MarginSummary (逐仓)" + summary = accountState.MarginSummary + } + + // 🔍 调试:打印API返回的完整摘要结构 + summaryJSON, _ := json.MarshalIndent(summary, " ", " ") + log.Printf("🔍 [DEBUG] Hyperliquid API %s 完整数据:", summaryType) + log.Printf("%s", string(summaryJSON)) // ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏 totalUnrealizedPnl := 0.0 @@ -109,16 +140,47 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { // 需要返回"不包含未实现盈亏的钱包余额" walletBalanceWithoutUnrealized := accountValue - totalUnrealizedPnl - result["totalWalletBalance"] = walletBalanceWithoutUnrealized // 钱包余额(不含未实现盈亏) - result["availableBalance"] = accountValue - totalMarginUsed // 可用余额(总净值 - 占用保证金) - result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏 + // ✅ Step 4: 使用 Withdrawable 欄位(PR #443) + // Withdrawable 是官方提供的真实可提现余额,比简单计算更可靠 + availableBalance := 0.0 + if accountState.Withdrawable != "" { + withdrawable, err := strconv.ParseFloat(accountState.Withdrawable, 64) + if err == nil && withdrawable > 0 { + availableBalance = withdrawable + log.Printf("✓ 使用 Withdrawable 作为可用余额: %.2f", availableBalance) + } + } - log.Printf("✓ Hyperliquid 账户: 总净值=%.2f (钱包%.2f+未实现%.2f), 可用=%.2f, 保证金占用=%.2f", + // 降级方案:如果没有 Withdrawable,使用简单计算 + if availableBalance == 0 && accountState.Withdrawable == "" { + availableBalance = accountValue - totalMarginUsed + if availableBalance < 0 { + log.Printf("⚠️ 计算出的可用余额为负数 (%.2f),重置为 0", availableBalance) + availableBalance = 0 + } + } + + // ✅ Step 5: 正確處理 Spot + Perpetuals 余额 + // 重要:Spot 只加到總資產,不加到可用餘額 + // 原因:Spot 和 Perpetuals 是獨立帳戶,需手動 ClassTransfer 才能轉帳 + totalWalletBalance := walletBalanceWithoutUnrealized + spotUSDCBalance + + 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) + log.Printf(" • Perpetuals 合约净值: %.2f USDC (钱包%.2f + 未实现%.2f)", accountValue, walletBalanceWithoutUnrealized, - totalUnrealizedPnl, - result["availableBalance"], - totalMarginUsed) + totalUnrealizedPnl) + log.Printf(" • Perpetuals 可用余额: %.2f USDC (可直接用於開倉)", availableBalance) + log.Printf(" • 保证金占用: %.2f USDC", totalMarginUsed) + log.Printf(" • 總資產 (Perp+Spot): %.2f USDC", totalWalletBalance) + log.Printf(" ⭐ 总资产: %.2f USDC | Perp 可用: %.2f USDC | Spot 余额: %.2f USDC", + totalWalletBalance, availableBalance, spotUSDCBalance) return result, nil }