Files
dekun b377956367 Fix false USDC-short error while waiting for a qualified option.
Use risk-sizing preview ask/qty (same as auto-convert) instead of monitor book ask.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 19:28:58 +08:00

249 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OKX:交易账户 USDC 不足时市价 USDT→USDC(目标=期权所需×2)。
仅 OKX SIM/LIVE。资金账户不参与;期权已可开则跳过。
等待选约阶段只用预览名义检测(不落库);落库定仓在 open_pipeline.size_and_gate。
"""
from __future__ import annotations
import logging
import math
import time
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__)
_TARGET_MULTIPLE = 2.0
_MIN_CONVERT_USDT = 1.0
_RETRY_COOLDOWN_SEC = 45.0
_last_attempt_ts: float = 0.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 preview_capacity_for_convert(db: Database | None = None) -> dict[str, Any]:
"""
用预览以损定仓名义评估资金门(不写 settings)。
- 选约杠杆:用定仓卖一(指数/杠杆下限),与预算/k 一致,勿用监控未达标贵卖一
- 实际杠杆:用盘口卖一
手动仓位则直接 assess 当前账本名义。
"""
database = db or get_db()
try:
from .risk_sizing import preview_risk_sizing
prev = preview_risk_sizing(database)
if prev.get("risk_based") and prev.get("ok"):
# option_ask 在 preview 里已是定仓口径(selection=隐含 / actual=盘口)
cap = assess_open_capacity(
database,
option_ask=float(prev["option_ask"])
if prev.get("option_ask") is not None
else None,
option_qty_eth=float(prev["option_qty_eth"])
if prev.get("option_qty_eth") is not None
else None,
perp_qty_eth=float(prev["perp_qty_eth"])
if prev.get("perp_qty_eth") is not None
else None,
call_ask=float(prev["call_ask"])
if prev.get("call_ask") is not None
else None,
put_ask=float(prev["put_ask"])
if prev.get("put_ask") is not None
else None,
)
cap["capacity_basis"] = "risk_preview"
cap["leverage_basis"] = prev.get("leverage_basis")
return cap
except Exception:
logger.exception("preview capacity for convert failed")
cap = assess_open_capacity(database)
cap["capacity_basis"] = "ledger"
return cap
def prepare_okx_trading_usdc(db: Database | None = None) -> dict[str, Any]:
"""等待阶段:预览名义评估 + 兑换,不落库。"""
database = db or get_db()
cap = preview_capacity_for_convert(database)
conv = ensure_okx_trading_usdc(database, cap=cap, force=False)
return {"capacity": cap, "convert": conv}
def ensure_okx_trading_usdc(
db: Database | None = None,
*,
cap: dict[str, Any] | None = None,
force: bool = False,
) -> dict[str, Any]:
"""
- 非 OKX → 跳过
- 期权可开(USDC≥需)→ 跳过
- 否则交易账户市价 USDT→USDC,尽量补到 需×2(预留永续保证金)
- force 不再绕过冷却(防砸单);保留参数仅为兼容调用方
"""
global _last_attempt_ts
_ = force # 明确忽略:冷却始终生效
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
if have_f + 1e-9 >= need_f:
out["detail"] = (
f"交易账户 USDC 已够开仓(有 {have_f:.2f} ≥ 需 {need_f:.2f}),不兑换"
)
out["capacity"] = cap
return out
now = time.time()
if _last_attempt_ts > 0 and now - _last_attempt_ts < _RETRY_COOLDOWN_SEC:
left = _RETRY_COOLDOWN_SEC - (now - _last_attempt_ts)
out["detail"] = f"USDC 不足,自动兑换冷却中({left:.0f}s"
out["capacity"] = cap
out["need_usdc"] = round(need_f, 2)
out["have_usdc"] = round(have_f, 2)
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
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
_last_attempt_ts = now
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
logger.warning("auto_usdc failed: %s", out["detail"])
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