mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-04 03:21:04 +08:00
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user