fix(hyperliquid): query both Spot and Perpetuals balance to resolve "0 balance" false reports

## 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 <noreply@anthropic.com>
This commit is contained in:
ZhouYongyou
2025-11-05 03:08:35 +08:00
parent d2cf05c4e1
commit fe8ba6ac34

View File

@@ -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
}