cc0f0ffd0d
Co-authored-by: Cursor <cursoragent@cursor.com>
246 lines
7.6 KiB
Python
246 lines
7.6 KiB
Python
"""OKX 开仓前:交易账户 USDC 不足时市价 USDT→USDC(目标=期权所需×2)。
|
||
|
||
仅 OKX SIM/LIVE。资金账户不参与;期权已可开则跳过。
|
||
空仓等待开仓时也会按盘口刷新以损定仓名义后再检测,不依赖已选中合格期权。
|
||
"""
|
||
|
||
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__)
|
||
|
||
# 目标持仓 = 期权开仓所需 USDC × 倍数
|
||
_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 refresh_risk_sizing_from_market(db: Database | None = None) -> dict[str, Any]:
|
||
"""空仓时用当前指数/卖一刷新以损定仓名义,便于资金门与自动兑对齐展示。"""
|
||
database = db or get_db()
|
||
out: dict[str, Any] = {"ok": True, "detail": "skip"}
|
||
try:
|
||
from ..sim.ledger import Ledger
|
||
from .open_capacity import _index_and_option_ask
|
||
from .risk_sizing import apply_risk_sizing_to_ledger, is_risk_based
|
||
|
||
if not is_risk_based(Ledger(database)):
|
||
out["detail"] = "manual_sizing"
|
||
return out
|
||
idx, ask = _index_and_option_ask()
|
||
if idx is None or ask is None or float(idx) <= 0 or float(ask) <= 0:
|
||
out["ok"] = False
|
||
out["detail"] = "暂无指数或期权卖一"
|
||
return out
|
||
r = apply_risk_sizing_to_ledger(
|
||
index_px=float(idx), option_ask=float(ask), db=database
|
||
)
|
||
out["ok"] = bool(r.ok)
|
||
out["detail"] = r.detail
|
||
out["k"] = r.k
|
||
out["option_qty_eth"] = r.option_qty_eth
|
||
return out
|
||
except Exception as e:
|
||
logger.exception("refresh risk sizing for auto_usdc failed")
|
||
return {"ok": False, "detail": str(e)}
|
||
|
||
|
||
def prepare_okx_trading_usdc(db: Database | None = None) -> dict[str, Any]:
|
||
"""选约前也可调用:先刷新名义,再按资金门自动兑 USDC。"""
|
||
database = db or get_db()
|
||
sized = refresh_risk_sizing_from_market(database)
|
||
top = ensure_okx_trading_usdc(database)
|
||
return {"sizing": sized, "convert": top}
|
||
|
||
|
||
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(并预留永续保证金)
|
||
"""
|
||
global _last_attempt_ts
|
||
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
|
||
|
||
now = time.time()
|
||
if (
|
||
not force
|
||
and _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
|
||
# usdt_to_usdc:amount = 花费的 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
|
||
|
||
_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
|