账户余额查询与稳定币兑换机制
账户余额查询与稳定币兑换机制
1. 概述
本系统运行在 Hyperliquid 交易所上,支持两种账户模式:统一账户(Unified Account) 和 普通账户(Standard Account)。两种模式下余额查询的 API 和计算逻辑完全不同。
核心文件:
src/trading/executor.py— 余额查询、兑换执行src/trading/config.py— 兑换配置src/trading/risk_manager.py— 风控中的余额校验src/trading/orchestrator.py— 编排层调用入口
2. 账户模式检测
2.1 检测逻辑(_detect_unified_account)
通过 Spot API 推断账户类型:
spot_user_state(address)
├─ USDC total > 0 且 hold > 0 → 统一账户(hold 被 perp 占用)
├─ USDC total > 0 且 perp accountValue == 0 → 统一账户(资金全在 spot 层)
└─ 其他 → 普通账户
2.2 统一账户 vs 普通账户资金结构
| 维度 | 统一账户 | 普通账户 |
|---|---|---|
| 资金位置 | 全部在 Spot 层 | Perp 层独立 |
| Perp 保证金 | Spot USDC 中 hold 部分 | Perp accountValue |
| 可用余额 | Spot 所有代币 sum(total - hold) | Perp withdrawable |
| Perp withdrawable | 恒为 0(不代表可用) | 真实可提取金额 |
3. Hyperliquid API 调用
3.1 调用的 API 列表
| API 方法 | 封装方法 | 用途 | 返回关键字段 |
|---|---|---|---|
info.spot_user_state(addr) |
_spot_user_state_with_retry |
Spot 层余额 | balances[].{coin, total, hold} |
info.user_state(addr) |
_user_state_with_retry |
Perp 账户状态 | marginSummary.accountValue, withdrawable |
info.user_state(addr, dex=) |
— | HIP-3 DEX 账户状态 | marginSummary.accountValue |
3.2 spot_user_state 返回结构
{
"balances": [
{"coin": "USDC", "token": 0, "total": "85.68", "hold": "82.16"},
{"coin": "USDH", "token": 360, "total": "10.97", "hold": "0.0"},
{"coin": "USDE", "token": 235, "total": "10.24", "hold": "0.0"}
]
}
total:该代币的总余额hold:被占用部分(统一账户下 = perp 保证金占用)available = total - hold
3.3 user_state 返回结构(Perp)
{
"marginSummary": {
"accountValue": "82.16",
"totalMarginUsed": "82.16",
"totalNtlPos": "243.61",
"totalRawUsd": "-161.45"
},
"withdrawable": "0.0",
"crossMarginSummary": { ... }
}
- 统一账户下
accountValue= spot USDC hold(perp 占用的那部分) - 统一账户下
withdrawable恒为 0
3.4 重试策略
| 场景 | 最大重试次数 | 最大退避时间 |
|---|---|---|
初始化阶段(is_critical=True) |
5 次 | 20 秒 |
运行时(is_critical=False) |
3 次 | 10 秒 |
4. 余额计算逻辑
4.1 账户总价值(get_account_value)
统一账户:
Spot total_value(所有代币 total 之和)
+ HIP-3 DEX accountValue(按 DEX 分别查询)
= 总价值
普通账户:
Perp marginSummary.accountValue(WS 缓存优先)
+ HIP-3 DEX accountValue
= 总价值
4.2 可用余额(get_available_balance)
统一账户:
→ _get_spot_available_value()
→ 遍历 spot balances,累加 max(total - hold, 0)
→ 所有代币可用余额之和(USDC + USDH + USDE + ...)
普通账户:
→ WS 缓存优先(BalanceChangedEvent 驱动更新,TTL 5秒)
→ 缓存未命中 → HTTP 降级:user_state().withdrawable
4.3 缓存机制
普通账户下,余额通过 WebSocket 事件驱动缓存:
Trading WS → BalanceChangedEvent → _on_balance_changed()
→ _cached_available_balance = event.available_balance
→ _balance_cache_ts = now
Trading WS → PositionUpdatedEvent → _on_position_updated()
→ _cached_account_value = event.account_value
→ _position_cache_ts = now
- 缓存 TTL:5 秒
- WS 断连时跳过缓存,直接 HTTP 查询
- WS 重连时清空所有缓存
统一账户不使用 WS 缓存,每次都通过 HTTP 查询 spot_user_state。
5. 稳定币兑换机制
5.1 配置项
| 配置 | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
quote_swap_enabled |
TRADING_QUOTE_SWAP_ENABLED |
false |
总开关 |
quote_swap_buffer_pct |
TRADING_QUOTE_SWAP_BUFFER_PCT |
0.05 |
兑换额外缓冲 5% |
quote_swap_slippage |
TRADING_QUOTE_SWAP_SLIPPAGE |
0.005 |
Spot 兑换滑点 0.5% |
quote_swap_min_amount |
TRADING_QUOTE_SWAP_MIN_AMOUNT |
10.0 |
最低兑换金额(HL 要求 ≥10 USD) |
5.2 HIP-3 报价货币识别
HIP-3 DEX 可能使用非 USDC 的稳定币作为保证金(如 USDH)。识别流程:
info.meta(dex=dex_name)
→ universe[].collateralToken(整数索引)
→ info.spot_meta().tokens[index].name
→ 报价货币名称
示例:
collateralToken=0 → token_index_map[0] → "USDC"
collateralToken=360 → token_index_map[360] → "USDH"
结果存储在 _hip3_dex_quote_map: dict[str, str]({dex_name → quote_currency})。
5.3 兑换入口(_ensure_quote_currency)
开仓前由 open_position 调用,判断是否需要兑换:
_ensure_quote_currency(coin, required_usd)
│
├─ quote_swap_enabled == False → 直接放行
│
├─ _get_required_quote(coin)
│ ├─ 标准 perp(coin 不含 ':')→ None
│ ├─ HIP-3 USDC 计价 DEX → None
│ └─ HIP-3 非 USDC 计价 DEX → "USDH" 等
│
├─ quote is None(标准 perp / USDC 计价):
│ ├─ _hip3_dex_quote_map 为空 → 放行
│ ├─ _get_spot_balance("USDC") → usdc_available
│ ├─ usdc_available >= required → 放行
│ └─ 不足 → _sell_stables_for_usdc(deficit × 1.05)
│
└─ quote is not None(HIP-3 非 USDC 计价):
├─ _get_spot_balance(quote) → available
├─ available >= required → 放行
├─ 不足 → 检查 USDC 余额
│ ├─ USDC 够 → _execute_quote_swap(quote, amount)
│ └─ USDC 不够 → _sell_stables_for_usdc() 先补 USDC
└─ 再执行 _execute_quote_swap(quote, amount)
5.4 正向兑换:USDC → 目标稳定币(_execute_quote_swap)
场景:HIP-3 DEX 需要 USDH 保证金,用 USDC 买入 USDH。
# Spot 市价买入
exchange.market_open(
f"{target_token}/USDC", # 如 "USDH/USDC"
True, # is_buy = True
amount, # 兑换数量
slippage=quote_swap_slippage,
)
金额计算:
deficit = required - available
swap_amount = deficit × (1 + buffer_pct)
swap_amount = max(swap_amount, min_amount) # 强制最低值
swap_amount = math.floor(swap_amount × 100) / 100 # 按 szDecimals=2 截断
5.5 反向兑换:其他稳定币 → USDC(_sell_stables_for_usdc)
场景:标准 perp 开仓时 Spot USDC 可用余额不足,卖出闲置的 USDH/USDE 等换取 USDC。
# 从 _hip3_dex_quote_map 收集所有非 USDC 稳定币
candidates = {token for token in _hip3_dex_quote_map.values()
if token != "USDC" and token != exclude_token}
# 逐个尝试卖出
for token in candidates:
available = _get_spot_balance(token) # (total - hold)
sell_amount = min(available, max(required_usdc, min_amount))
sell_amount = math.floor(sell_amount × 100) / 100
if sell_amount < min_amount:
continue # 余额不够最低兑换金额,跳过
exchange.market_open(f"{token}/USDC", False, sell_amount, slippage=...)
5.6 兑换失败处理
- 每次兑换失败都通过
_alert_quote_swap_failure发送飞书告警 - 错误信息返回给调用方,由
open_position记录到OrderResult.error_message - 信号状态记录为
rejected,写入 DB
6. 风控中的余额校验
6.1 开仓前风控(RiskManager.pre_trade_check)
编排层在执行开仓前获取余额并传入风控:
account_value = executor.get_account_value()
available_balance = executor.get_available_balance()
passed, reason = risk_manager.pre_trade_check(
signal, open_positions, account_value, available_balance
)
6.2 可用余额校验公式
position_usd = base_position_usd × strength_scale
legs = 2 (pair 模式) 或 1 (single 模式)
min_required = position_usd × legs / leverage × min_balance_factor
if available_balance < min_required:
→ 拒绝开仓
6.3 开仓后余额复查
Leg A 限价单成交后、Leg B 下单前,强制 HTTP 刷新余额:
recheck_balance = executor.get_available_balance(force_refresh=True)
7. 数据流全景图
┌─────────────────────────────────────────────────────────────────┐
│ 开仓信号触发 │
└──────────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ orchestrator._handle_entry_signal() │
│ ├─ get_account_value() ─┐ │
│ ├─ get_available_balance() ── → risk_manager.pre_trade_check()│
│ └─ 风控通过 → open_position() │
└──────────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ executor.open_position() │
│ ├─ margin_needed = size × price / leverage │
│ ├─ _ensure_quote_currency(coin, margin_needed) │
│ │ ├─ 标准 perp: 检查 Spot USDC,不足则卖其他稳定币 │
│ │ └─ HIP-3: 检查对应 quote token,不足则 USDC 兑换 │
│ ├─ _place_limit_order(coin, ..., l2_snapshot) ← Leg A │
│ ├─ _track_limit_order(...) ← 追踪成交 │
│ ├─ get_available_balance(force_refresh=True) ← 复查余额 │
│ └─ _place_market_order(base_coin, ...) ← Leg B │
└─────────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Hyperliquid API │
│ ├─ spot_user_state(addr) → Spot 余额(统一账户的资金来源) │
│ ├─ user_state(addr) → Perp 状态(普通账户的资金来源) │
│ ├─ user_state(addr, dex=) → HIP-3 DEX 状态 │
│ ├─ exchange.market_open() → Spot 兑换(稳定币互换) │
│ ├─ exchange.order() → Perp 限价单 │
│ └─ exchange.market_open() → Perp 市价单 │
└─────────────────────────────────────────────────────────────────┘
8. 已知问题与注意事项
-
统一账户下 Perp withdrawable 恒为 0:不能用
user_state().withdrawable判断统一账户的可用余额,必须用spot_user_state()计算。 -
Available to Trade 只看 USDC:Hyperliquid 标准 perp 的 "Available to Trade" 仅等于 Spot USDC available(total - hold),不会自动使用 USDH/USDE 等其他稳定币。需要通过 Spot 市价单手动兑换。
-
quote_swap_min_amount需对齐交易所要求:Hyperliquid Spot 交易最低名义价值为 10 USD,该配置不应高于此值,否则会导致可卖稳定币被错误跳过。 -
统一账户不使用 WS 余额缓存:每次查询都走 HTTP
spot_user_state,在高频查询场景下可能产生 API 限速风险。