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:
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user