From fe8ba6ac340595123cd358a4a13d98b66e5865fb Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 03:08:35 +0800 Subject: [PATCH] fix(hyperliquid): query both Spot and Perpetuals balance to resolve "0 balance" false reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Analysis PR #443 fixed Withdrawable field priority, but users still reported "wallet has funds but shows 0". **Root Cause**: Hyperliquid has TWO separate account systems: 1. **Spot Account** (現貨帳戶) - holds USDC/tokens 2. **Perpetuals Account** (合約帳戶) - for futures trading Previous implementation ONLY queried Perpetuals (`UserState`), completely missing Spot balance. ## Real-World Scenario User's actual account state: - Spot Account: 100 USDC ✅ (not detected before) - Perpetuals: 0 USDC - **Old display**: 0.00 USDC ❌ - **New display**: 100.00 USDC ✅ ## Solution Implemented ### 1. Query Both Accounts ```go // Step 1: Query Spot balance (SpotUserState) spotState := exchange.Info().SpotUserState(ctx, walletAddr) spotUSDCBalance := spotState.Balances[USDC].Total // Step 2: Query Perpetuals balance (UserState) accountState := exchange.Info().UserState(ctx, walletAddr) perpetualsValue := accountState.MarginSummary.AccountValue // Step 3: Combine both totalBalance = spotUSDCBalance + perpetualsValue ``` ### 2. Enhanced Logging New log format shows separate breakdowns: ``` ✓ Hyperliquid 账户总览: • Spot 现货余额: 100.00 USDC • Perpetuals 合约净值: 0.00 USDC • Perpetuals 可用余额: 0.00 USDC • 保证金占用: 0.00 USDC ⭐ 总净值: 100.00 USDC | 总可用: 100.00 USDC ``` ### 3. Backward Compatibility - If SpotUserState fails (API error), continues with Perpetuals only - Logs warning instead of failing completely - Maintains same return structure for auto_trader.go ## Technical Details **API Endpoints Used**: - `Info.SpotUserState(ctx, address)` → returns `SpotUserState{Balances[]}` - `Info.UserState(ctx, address)` → returns perpetuals state **Balance Fields**: - `SpotBalance.Total` - total USDC in spot (includes held + free) - `SpotBalance.Hold` - amount locked in spot orders - Combined with existing Perpetuals logic ## Impact **Before**: Users with Spot-only funds saw 0 balance → couldn't trade **After**: Correctly shows Spot + Perpetuals combined balance Closes false "insufficient balance" reports when funds exist in Spot account. ## References - Hyperliquid API Docs: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot - Related: PR #443 (Withdrawable field priority) - SDK: github.com/sonirico/go-hyperliquid v0.17.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- trader/hyperliquid_trader.go | 47 +++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index 6acd8205..8dd30812 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -72,14 +72,30 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) }, nil } -// GetBalance 获取账户余额 +// GetBalance 获取账户余额(同时查询 Spot 和 Perpetuals) func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { log.Printf("🔄 正在调用Hyperliquid API获取账户余额...") - // 获取账户状态 + // ✅ 第一步:查询 Spot 现货余额(用户的 USDC/USDT 可能在这里) + 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 { + // 查找 USDC 余额(Hyperliquid 现货主要使用 USDC) + for _, balance := range spotState.Balances { + if balance.Coin == "USDC" { + spotUSDCBalance, _ = strconv.ParseFloat(balance.Total, 64) + log.Printf("✓ 发现 Spot 现货余额: %.2f USDC", spotUSDCBalance) + break + } + } + } + + // ✅ 第二步:查询 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) } @@ -88,12 +104,15 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { // 🔍 调试:打印API返回的完整CrossMarginSummary结构 summaryJSON, _ := json.MarshalIndent(accountState.MarginSummary, " ", " ") - log.Printf("🔍 [DEBUG] Hyperliquid API CrossMarginSummary完整数据:") + log.Printf("🔍 [DEBUG] Hyperliquid Perpetuals CrossMarginSummary完整数据:") log.Printf("%s", string(summaryJSON)) accountValue, _ := strconv.ParseFloat(accountState.MarginSummary.AccountValue, 64) totalMarginUsed, _ := strconv.ParseFloat(accountState.MarginSummary.TotalMarginUsed, 64) + // ⚠️ 关键修复:将 Spot 现货余额加入总余额 + accountValue += spotUSDCBalance + // ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏 totalUnrealizedPnl := 0.0 for _, assetPos := range accountState.AssetPositions { @@ -132,16 +151,22 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { } } + // ✅ 可用余额 = Spot 现货余额 + Perpetuals 可用余额 + totalAvailableBalance := spotUSDCBalance + availableBalance + result["totalWalletBalance"] = walletBalanceWithoutUnrealized // 钱包余额(不含未实现盈亏) - result["availableBalance"] = availableBalance // 可用余额(优先使用Withdrawable,最小为0) + result["availableBalance"] = totalAvailableBalance // 总可用余额(Spot + Perpetuals) result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏 - log.Printf("✓ Hyperliquid 账户: 总净值=%.2f (钱包%.2f+未实现%.2f), 可用=%.2f, 保证金占用=%.2f", - accountValue, - walletBalanceWithoutUnrealized, - totalUnrealizedPnl, - availableBalance, - totalMarginUsed) + log.Printf("✓ Hyperliquid 账户总览:") + log.Printf(" • Spot 现货余额: %.2f USDC", spotUSDCBalance) + log.Printf(" • Perpetuals 合约净值: %.2f USDC (钱包%.2f + 未实现%.2f)", + accountValue-spotUSDCBalance, + walletBalanceWithoutUnrealized-spotUSDCBalance, + totalUnrealizedPnl) + log.Printf(" • Perpetuals 可用余额: %.2f USDC", availableBalance) + log.Printf(" • 保证金占用: %.2f USDC", totalMarginUsed) + log.Printf(" ⭐ 总净值: %.2f USDC | 总可用: %.2f USDC", accountValue, totalAvailableBalance) return result, nil }