"""OKX 资金:余额 / USDT↔USDC 现货兑换 / 账户划转(对齐 crypto_monitor)。""" from __future__ import annotations import logging from typing import Any from .okx_trade import OkxTradeClient logger = logging.getLogger(__name__) # OKX acct: 6=资金, 18=交易 _ACCT_CODE = { "funding": "6", "trading": "18", "spot": "18", "options_funding": "6", "options_trading": "18", } def _f(v: Any) -> float | None: try: if v is None or v == "": return None return float(v) except (TypeError, ValueError): return None class OkxFundsClient: def __init__(self, trade: OkxTradeClient | None = None) -> None: self.trade = trade or OkxTradeClient() def close(self) -> None: self.trade.close() def fetch_balances(self) -> dict[str, float | None]: """ 拉取资金账户 + 交易账户 USDT/USDC。 资金:GET /api/v5/asset/balances 交易:GET /api/v5/account/balance """ out: dict[str, float | None] = { "funding_usdt": None, "funding_usdc": None, "trading_usdt": None, "trading_usdc": None, "options_funding_usdc": None, "options_trading_usdc": None, "options_funding_usdt": None, "options_trading_usdt": None, } try: rows = self.trade._request("GET", "/api/v5/asset/balances") for row in rows: ccy = str(row.get("ccy") or "").upper() bal = _f(row.get("bal")) or _f(row.get("availBal")) if ccy == "USDT": out["funding_usdt"] = bal out["options_funding_usdt"] = bal elif ccy == "USDC": out["funding_usdc"] = bal out["options_funding_usdc"] = bal except Exception as e: logger.warning("OKX asset balances failed: %s", e) try: rows = self.trade._request("GET", "/api/v5/account/balance") for block in rows: details = block.get("details") or [] if not isinstance(details, list): continue for row in details: if not isinstance(row, dict): continue ccy = str(row.get("ccy") or "").upper() eq = _f(row.get("eq")) or _f(row.get("cashBal")) or _f(row.get("availBal")) if ccy == "USDT": out["trading_usdt"] = eq out["options_trading_usdt"] = eq elif ccy == "USDC": out["trading_usdc"] = eq out["options_trading_usdc"] = eq except Exception as e: logger.warning("OKX account balance failed: %s", e) return out def spot_swap_usdt_usdc(self, *, direction: str, amount: float) -> dict[str, Any]: """现货市价兑换 USDC-USDT(与 crypto_monitor spot_market_swap_usdt_usdc 同口径)。""" amt = float(amount) if amt <= 0: return {"ok": False, "detail": "数量须大于 0"} d = (direction or "").strip().lower() inst_id = "USDC-USDT" if d == "usdt_to_usdc": body = { "instId": inst_id, "tdMode": "cash", "side": "buy", "ordType": "market", "sz": str(amt), "tgtCcy": "quote_ccy", } elif d == "usdc_to_usdt": body = { "instId": inst_id, "tdMode": "cash", "side": "sell", "ordType": "market", "sz": str(amt), "tgtCcy": "base_ccy", } else: return {"ok": False, "detail": "direction 须为 usdt_to_usdc 或 usdc_to_usdt"} try: rows = self.trade._request("POST", "/api/v5/trade/order", body) if not rows: return {"ok": False, "detail": "兑换下单无返回"} row = rows[0] if str(row.get("sCode") or "0") not in ("0", ""): return { "ok": False, "detail": str(row.get("sMsg") or row.get("sCode") or "兑换失败"), "raw": row, } return {"ok": True, "detail": "converted", "data": row} except Exception as e: return {"ok": False, "detail": str(e)} def transfer( self, *, ccy: str, amount: float, from_account: str, to_account: str, ) -> dict[str, Any]: """同一 API Key 下资金↔交易划转。""" amt = float(amount) if amt <= 0: return {"ok": False, "detail": "划转金额须大于 0"} fa = (from_account or "").strip().lower() ta = (to_account or "").strip().lower() if fa == ta: return {"ok": False, "detail": "来源与目标账户不能相同"} from_code = _ACCT_CODE.get(fa) to_code = _ACCT_CODE.get(ta) if not from_code or not to_code: return {"ok": False, "detail": "账户须为 funding/trading(或 options_* 别名)"} body = { "ccy": str(ccy).upper(), "amt": str(amt), "from": from_code, "to": to_code, "type": "0", } try: rows = self.trade._request("POST", "/api/v5/asset/transfer", body) if not rows: return {"ok": False, "detail": "划转无返回"} return {"ok": True, "detail": "transferred", "data": rows[0]} except Exception as e: return {"ok": False, "detail": str(e)} def usdc_usdt_mid_rate() -> float: """公共盘口中间价:1 USDC ≈ ? USDT;失败则 1.0。""" try: import httpx from ..config import get_settings s = get_settings() proxy = (s.okx_http_proxy or "").strip() or None with httpx.Client(base_url=s.okx_rest_base.rstrip("/"), timeout=8.0, proxy=proxy) as c: r = c.get("/api/v5/market/ticker", params={"instId": "USDC-USDT"}) r.raise_for_status() rows = (r.json() or {}).get("data") or [] if not rows: return 1.0 bid = _f(rows[0].get("bidPx")) ask = _f(rows[0].get("askPx")) last = _f(rows[0].get("last")) if bid and ask and bid > 0 and ask > 0: return (bid + ask) / 2.0 if last and last > 0: return last except Exception as e: logger.warning("USDC-USDT mid failed: %s", e) return 1.0