diff --git a/backend/app/strategy/auto_usdc.py b/backend/app/strategy/auto_usdc.py index 608ce44..2c18140 100644 --- a/backend/app/strategy/auto_usdc.py +++ b/backend/app/strategy/auto_usdc.py @@ -1,12 +1,14 @@ """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 @@ -19,6 +21,9 @@ 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: @@ -40,10 +45,49 @@ def _round_down(n: float, nd: int = 2) -> float: 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]: """ 开仓资金门前调用: @@ -51,6 +95,7 @@ def ensure_okx_trading_usdc( - 期权可开(USDC≥需)→ 跳过 - 否则在**交易账户**市价 USDT→USDC,尽量补到 需×2(并预留永续保证金) """ + global _last_attempt_ts db = db or get_db() out: dict[str, Any] = { "ok": True, @@ -84,6 +129,19 @@ def ensure_okx_trading_usdc( 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: @@ -126,6 +184,7 @@ def ensure_okx_trading_usdc( out["capacity"] = cap return out + _last_attempt_ts = now s = get_settings() try: if s.is_sim: @@ -163,9 +222,9 @@ def ensure_okx_trading_usdc( 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( diff --git a/backend/app/strategy/engine.py b/backend/app/strategy/engine.py index be31e4b..4afced8 100644 --- a/backend/app/strategy/engine.py +++ b/backend/app/strategy/engine.py @@ -807,9 +807,44 @@ class StrategyEngine: self._set_state(phase="open", last_error="有未平仓,禁止开下一组") return + # OKX:先按盘口刷新名义并检测 USDC;不够则交易账户市价兑换(不依赖已选中合格期权) + try: + from .auto_usdc import prepare_okx_trading_usdc + + prep = prepare_okx_trading_usdc(self.db) + conv = prep.get("convert") or {} + if conv.get("acted"): + self._set_state( + last_error=None, + phase="wait_signal", + ) + logger.info("auto_usdc prepared: %s", conv.get("detail")) + elif conv.get("ok") is False and "不足" in str(conv.get("detail") or ""): + # 保留资金提示,但仍继续尝试选约(可能只是冷却/短暂失败) + logger.warning("auto_usdc: %s", conv.get("detail")) + except Exception: + logger.exception("prepare OKX USDC failed") + self._set_state(phase="wait_signal") pick = await get_session().pick_for_open_async() if pick is None: + # 若期权仍不可开且刚才未兑成功,把资金状态写进错误,便于排查「为何没自动兑」 + try: + from .open_capacity import assess_open_capacity + + cap = assess_open_capacity(self.db) + if cap.get("option_can_open") is False: + self._set_state( + phase="wait_funds", + last_error=( + "无合格期权;且交易账户 USDC 不够开仓" + f"(需≈{cap.get('option_need_usdc')}U / 有{cap.get('option_have_usdc')}U)。" + "系统会在冷却后自动市价兑 USDC。" + ), + ) + return + except Exception: + pass self._set_state( last_error="无合格期权:需剩余时长、杠杆(及已开启的ATM偏差)同时满足" ) @@ -842,11 +877,11 @@ class StrategyEngine: self._set_state(phase="idle", last_error="以损定仓计算异常,暂不开仓") return - # OKX:交易账户 USDC 不够开期权时,市价 USDT→USDC(目标=所需×2);够则跳过 + # 选约后名义可能变化,再检一次 USDC(force 跳过冷却,避免刚选完仍差一截) try: from .auto_usdc import ensure_okx_trading_usdc - ensure_okx_trading_usdc(self.db) + ensure_okx_trading_usdc(self.db, force=True) except Exception: logger.exception("auto USDC top-up failed") diff --git a/docs/更新说明.md b/docs/更新说明.md index 1c92215..39d538c 100644 --- a/docs/更新说明.md +++ b/docs/更新说明.md @@ -5,6 +5,15 @@ --- +## 2026-07-30 — 自动兑 USDC:不等待选约,USDC 不够即兑 + +### 变更 + +1. 策略空仓循环在选约**之前**先刷新以损定仓名义并检测交易账户 USDC;不够则市价兑换(不再因「无合格期权」跳过)。 +2. 兑换失败/不足时错误提示会带上需/有 USDC;成功兑换有 45s 冷却防砸单。 + +--- + ## 2026-07-30 — 手动兑换改走交易账户 ### 变更