"""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