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]);
|
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) {
|
async function resolveMaxAmount(account, ccy, scope) {
|
||||||
const bal = await loadBalances(true, scope || "main");
|
const bal = await loadBalances(true, scope || "main");
|
||||||
return pickBalance(bal, account, ccy);
|
return pickBalance(bal, account, ccy);
|
||||||
@@ -148,17 +164,33 @@
|
|||||||
swapAllBtn.addEventListener("click", async function () {
|
swapAllBtn.addEventListener("click", async function () {
|
||||||
try {
|
try {
|
||||||
const dir = document.getElementById("opt-set-swap-dir").value;
|
const dir = document.getElementById("opt-set-swap-dir").value;
|
||||||
const ccy = dir === "usdc_to_usdt" ? "USDC" : "USDT";
|
const { amount, bal, ccy, source } = await resolveSwapMaxAmount(dir);
|
||||||
const amount = await resolveMaxAmount("funding", ccy, "main");
|
|
||||||
if (!amount) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
const srcLabel = source === "trading" ? "交易账户" : "资金账户";
|
||||||
const msg =
|
const msg =
|
||||||
"确认全部兑换?\n\n" +
|
"确认全部兑换?\n\n" +
|
||||||
"方向:" + swapDirLabel(dir) + "\n" +
|
"方向:" + swapDirLabel(dir) + "\n" +
|
||||||
"金额:" + fmtAmt(amount, ccy) + "\n\n" +
|
"金额:" + fmtAmt(amount, ccy) + "\n" +
|
||||||
"将兑换资金账户全部可用余额。市价成交可能有延时,请确认。";
|
"来源:" + srcLabel + "\n\n" +
|
||||||
|
"将按该账户可用余额发起市价兑换(可能有延时)。请确认。";
|
||||||
if (!confirmOk(msg)) return;
|
if (!confirmOk(msg)) return;
|
||||||
document.getElementById("opt-set-swap-amount").value = String(amount);
|
document.getElementById("opt-set-swap-amount").value = String(amount);
|
||||||
await submitSwap(amount);
|
await submitSwap(amount);
|
||||||
|
|||||||
@@ -439,6 +439,40 @@ def fetch_account_balances_by_type(
|
|||||||
return out, avail
|
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]:
|
def fetch_subaccount_asset_balances(ex: ccxt.okx, sub_acct: str) -> dict[str, float | None]:
|
||||||
"""子账户各币种可用余额(主/子划转「全部」用)."""
|
"""子账户各币种可用余额(主/子划转「全部」用)."""
|
||||||
sub = (sub_acct or "").strip()
|
sub = (sub_acct or "").strip()
|
||||||
@@ -489,6 +523,9 @@ def fetch_options_balances(
|
|||||||
return dict(cached)
|
return dict(cached)
|
||||||
|
|
||||||
funding, funding_avail = fetch_account_balances_by_type(ex, "funding")
|
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")
|
trading, trading_avail = fetch_account_balances_by_type(ex, "trading")
|
||||||
if trading.get("USDC") is None:
|
if trading.get("USDC") is None:
|
||||||
swap_bal, swap_avail = fetch_account_balances_by_type(ex, "swap")
|
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