diff --git a/lib/common/static/options_settings.js b/lib/common/static/options_settings.js index 2ba297f..71bb615 100644 --- a/lib/common/static/options_settings.js +++ b/lib/common/static/options_settings.js @@ -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); diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index 93ed7e2..7ff93bf 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -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") diff --git a/tests/test_okx_funding_balances.py b/tests/test_okx_funding_balances.py new file mode 100644 index 0000000..b148e19 --- /dev/null +++ b/tests/test_okx_funding_balances.py @@ -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()