Fix swap-all balance by reading OKX funding via asset/balances API.

Swap-all targets funding account but ccxt free was often empty; fall back to trading USDT for unified accounts and show balances in errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-13 09:01:37 +08:00
parent c4bac23c48
commit c4753ffb89
3 changed files with 113 additions and 5 deletions
+37 -5
View File
@@ -99,6 +99,22 @@
return roundAvail(bal[availKey] != null ? bal[availKey] : bal[totalKey]);
}
async function resolveSwapMaxAmount(dir) {
const bal = await loadBalances(true, "main");
const ccy = dir === "usdc_to_usdt" ? "USDC" : "USDT";
// 币种兑换走资金账户现货;统一账户下 USDT 有时在交易户,市价单仍可能成交
let amount = pickBalance(bal, "funding", ccy);
let source = "funding";
if (!amount && ccy === "USDT") {
const tradingAmt = pickBalance(bal, "trading", ccy);
if (tradingAmt) {
amount = tradingAmt;
source = "trading";
}
}
return { amount, bal, ccy, source };
}
async function resolveMaxAmount(account, ccy, scope) {
const bal = await loadBalances(true, scope || "main");
return pickBalance(bal, account, ccy);
@@ -148,17 +164,33 @@
swapAllBtn.addEventListener("click", async function () {
try {
const dir = document.getElementById("opt-set-swap-dir").value;
const ccy = dir === "usdc_to_usdt" ? "USDC" : "USDT";
const amount = await resolveMaxAmount("funding", ccy, "main");
const { amount, bal, ccy, source } = await resolveSwapMaxAmount(dir);
if (!amount) {
setMsg("opt-set-swap-msg", "资金账户可用余额不足", true);
const fu = bal.funding_usdt_avail != null ? bal.funding_usdt_avail : bal.funding_usdt;
const tu = bal.trading_usdt_avail != null ? bal.trading_usdt_avail : bal.trading_usdt;
const fc = bal.funding_usdc_avail != null ? bal.funding_usdc_avail : bal.funding_usdc;
setMsg(
"opt-set-swap-msg",
"资金账户可用 " +
ccy +
" 不足(资金户 USDT:" +
(fu != null ? fu : "—") +
" USDC:" +
(fc != null ? fc : "—") +
"; 交易户 USDT:" +
(tu != null ? tu : "—") +
")",
true
);
return;
}
const srcLabel = source === "trading" ? "交易账户" : "资金账户";
const msg =
"确认全部兑换?\n\n" +
"方向:" + swapDirLabel(dir) + "\n" +
"金额:" + fmtAmt(amount, ccy) + "\n\n" +
"将兑换资金账户全部可用余额。市价成交可能有延时,请确认。";
"金额:" + fmtAmt(amount, ccy) + "\n" +
"来源:" + srcLabel + "\n\n" +
"将按该账户可用余额发起市价兑换(可能有延时)。请确认。";
if (!confirmOk(msg)) return;
document.getElementById("opt-set-swap-amount").value = String(amount);
await submitSwap(amount);
+37
View File
@@ -439,6 +439,40 @@ def fetch_account_balances_by_type(
return out, avail
def fetch_funding_balances_via_asset_api(
ex: ccxt.okx,
) -> tuple[dict[str, float | None], dict[str, float | None]]:
"""OKX 资金账户余额(GET /api/v5/asset/balances),比 ccxt fetch_balance 更准确."""
out: dict[str, float | None] = {"USDT": None, "USDC": None, "USDG": None}
avail: dict[str, float | None] = {"USDT": None, "USDC": None, "USDG": None}
try:
resp = ex.private_get_asset_balances({})
for row in (resp or {}).get("data") or []:
if not isinstance(row, dict):
continue
ccy = str(row.get("ccy") or "").upper()
if ccy not in out:
continue
a = _safe_float(row.get("availBal"))
b = _safe_float(row.get("bal")) or _safe_float(row.get("eq"))
avail[ccy] = a
out[ccy] = b if b is not None else a
except Exception:
pass
return out, avail
def _merge_balance_maps(
primary: dict[str, float | None],
secondary: dict[str, float | None],
) -> dict[str, float | None]:
merged = dict(primary)
for ccy, val in secondary.items():
if merged.get(ccy) is None and val is not None:
merged[ccy] = val
return merged
def fetch_subaccount_asset_balances(ex: ccxt.okx, sub_acct: str) -> dict[str, float | None]:
"""子账户各币种可用余额(主/子划转「全部」用)."""
sub = (sub_acct or "").strip()
@@ -489,6 +523,9 @@ def fetch_options_balances(
return dict(cached)
funding, funding_avail = fetch_account_balances_by_type(ex, "funding")
asset_funding, asset_funding_avail = fetch_funding_balances_via_asset_api(ex)
funding = _merge_balance_maps(funding, asset_funding)
funding_avail = _merge_balance_maps(funding_avail, asset_funding_avail)
trading, trading_avail = fetch_account_balances_by_type(ex, "trading")
if trading.get("USDC") is None:
swap_bal, swap_avail = fetch_account_balances_by_type(ex, "swap")
+39
View File
@@ -0,0 +1,39 @@
"""OKX 资金账户余额(asset/balances)."""
from __future__ import annotations
import unittest
from unittest.mock import MagicMock
from lib.exchange.okx_options_lib import (
fetch_funding_balances_via_asset_api,
fetch_options_balances,
)
class TestOkxFundingBalances(unittest.TestCase):
def test_asset_balances_parsed(self):
ex = MagicMock()
ex.private_get_asset_balances.return_value = {
"data": [
{"ccy": "USDT", "availBal": "25.5", "bal": "30"},
{"ccy": "USDC", "availBal": "10", "bal": "10"},
]
}
total, avail = fetch_funding_balances_via_asset_api(ex)
self.assertEqual(avail["USDT"], 25.5)
self.assertEqual(total["USDT"], 30.0)
self.assertEqual(avail["USDC"], 10.0)
def test_fetch_options_balances_merges_asset_api(self):
ex = MagicMock()
ex.fetch_balance.return_value = {"free": {}, "total": {}}
ex.private_get_asset_balances.return_value = {
"data": [{"ccy": "USDT", "availBal": "18.2", "bal": "18.2"}]
}
bal = fetch_options_balances(ex, force=True)
self.assertEqual(bal["funding_usdt_avail"], 18.2)
self.assertEqual(bal["funding_usdt"], 18.2)
if __name__ == "__main__":
unittest.main()