Add OKX-style funds bar with USDT/USDC convert and transfer.
SIM uses multi-wallet balances; LIVE hits OKX asset/account APIs and spot USDC-USDT swap. Funds strip hides sim labeling. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user