Add OKX trading-account USDC auto-swap and lock exit target while in position.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-30 00:16:47 +08:00
parent f0b0b91f1b
commit 0837982714
16 changed files with 626 additions and 36 deletions
+186
View File
@@ -0,0 +1,186 @@
"""OKX 开仓前:交易账户 USDC 不足时市价 USDT→USDC(目标=期权所需×2)。
仅 OKX SIM/LIVE。资金账户不参与;期权已可开则跳过。
"""
from __future__ import annotations
import logging
import math
from typing import Any
from ..config import get_settings
from ..models.db import Database, get_db
from .open_capacity import assess_open_capacity, invalidate_live_balance_cache
logger = logging.getLogger(__name__)
# 目标持仓 = 期权开仓所需 USDC × 倍数
_TARGET_MULTIPLE = 2.0
# 过小不兑(避免粉尘单)
_MIN_CONVERT_USDT = 1.0
def _is_okx(exchange: str | None = None) -> bool:
from ..exchange.runtime import load_runtime_settings, normalize_exchange_name
ex = exchange
if not ex:
try:
ex = load_runtime_settings().exchange
except Exception:
ex = get_settings().exchange
return normalize_exchange_name(ex) == "okx"
def _round_down(n: float, nd: int = 2) -> float:
if n <= 0:
return 0.0
f = 10**nd
return math.floor(n * f + 1e-12) / f
def ensure_okx_trading_usdc(
db: Database | None = None,
*,
cap: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""
开仓资金门前调用:
- 非 OKX → 跳过
- 期权可开(USDC≥需)→ 跳过
- 否则在**交易账户**市价 USDT→USDC,尽量补到 需×2(并预留永续保证金)
"""
db = db or get_db()
out: dict[str, Any] = {
"ok": True,
"acted": False,
"skipped": True,
"detail": "skip",
}
if not _is_okx():
out["detail"] = "非 OKX,跳过自动兑 USDC"
return out
cap = cap or assess_open_capacity(db)
need = cap.get("option_need_usdc")
have = cap.get("option_have_usdc")
if need is None or have is None:
out["detail"] = "期权所需/持有未知,跳过兑换"
out["capacity"] = cap
return out
need_f = float(need)
have_f = float(have)
if need_f <= 0:
out["detail"] = "期权所需为 0,跳过"
return out
# 可开仓:不兑换(即使低于 2 倍目标)
if have_f + 1e-9 >= need_f:
out["detail"] = (
f"交易账户 USDC 已够开仓(有 {have_f:.2f} ≥ 需 {need_f:.2f}),不兑换"
)
out["capacity"] = cap
return out
target = need_f * _TARGET_MULTIPLE
gap_usdc = target - have_f
if gap_usdc <= 1e-6:
out["detail"] = "无需补足"
return out
from ..live.okx_funds import usdc_usdt_mid_rate
rate = float(usdc_usdt_mid_rate() or 1.0)
if rate <= 0:
rate = 1.0
# usdt_to_usdcamount = 花费的 USDT(与 OkxFundsClient / SIM 一致)
want_usdt = gap_usdc * rate
perp_need = float(cap.get("perp_need_usdt") or 0)
trading_usdt = float(cap.get("perp_have_usdt") or 0)
# 预留永续保证金,避免兑光导致永续不可开
spendable = max(0.0, trading_usdt - max(0.0, perp_need))
spend_usdt = _round_down(min(want_usdt, spendable), 2)
out.update(
{
"need_usdc": round(need_f, 2),
"have_usdc": round(have_f, 2),
"target_usdc": round(target, 2),
"want_usdt": round(want_usdt, 2),
"spend_usdt": spend_usdt,
"rate": rate,
"spendable_usdt": round(spendable, 2),
}
)
if spend_usdt < _MIN_CONVERT_USDT:
out["ok"] = False
out["skipped"] = True
out["detail"] = (
f"交易账户可兑 USDT 不足(可兑 {spendable:.2f}"
f"目标补约 {want_usdt:.2f},门槛 {_MIN_CONVERT_USDT}"
)
out["capacity"] = cap
return out
s = get_settings()
try:
if s.is_sim:
from ..sim.funds_wallets import SimFundsWallets
r = SimFundsWallets(db).convert(
direction="usdt_to_usdc",
amount=spend_usdt,
rate=rate,
account="trading",
)
else:
from ..live.okx_funds import OkxFundsClient
client = OkxFundsClient()
try:
r = client.spot_swap_usdt_usdc(
direction="usdt_to_usdc",
amount=spend_usdt,
)
finally:
client.close()
invalidate_live_balance_cache()
except Exception as e:
logger.exception("auto USDC convert failed")
out["ok"] = False
out["skipped"] = False
out["acted"] = False
out["detail"] = f"自动兑换异常:{e}"
return out
if not r.get("ok"):
out["ok"] = False
out["skipped"] = False
out["acted"] = False
out["detail"] = f"自动兑换失败:{r.get('detail') or r}"
out["raw"] = r
return out
# 兑换后重评
invalidate_live_balance_cache()
cap2 = assess_open_capacity(db)
out.update(
{
"ok": True,
"acted": True,
"skipped": False,
"detail": (
f"交易账户市价兑 USDC:花 {spend_usdt:.2f} USDT"
f"(目标持仓≈{target:.2f}=需{need_f:.2f}×{_TARGET_MULTIPLE:g}"
),
"capacity_before": cap,
"capacity_after": cap2,
"raw": r,
}
)
logger.info("auto_usdc: %s", out["detail"])
return out