From fcef3b3d37db143340234098a8697ac08176190b Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 2 Oct 2026 16:03:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=AD=E6=8E=A7=E5=8F=AF=E4=B8=8B=E5=8F=91?= =?UTF-8?q?=E4=B9=B0=E4=B8=80=E5=85=A8=E5=B9=B3=EF=BC=9A=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E6=96=B0=E5=A2=9E=20/api/hub/options/close=EF=BC=8C?= =?UTF-8?q?=E7=94=B1=E6=9C=AC=E5=9C=B0=E6=89=A7=E8=A1=8C=E5=B9=B6=E6=8B=92?= =?UTF-8?q?=E7=BB=9D=E6=97=A0=E6=95=88=E4=B9=B0=E4=B8=80=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- lib/hub/hub_bridge_register.py | 30 ++++++- lib/options/options_register.py | 136 ++++++++++++++++++-------------- 2 files changed, 107 insertions(+), 59 deletions(-) diff --git a/lib/hub/hub_bridge_register.py b/lib/hub/hub_bridge_register.py index ba5a159..3f0457e 100644 --- a/lib/hub/hub_bridge_register.py +++ b/lib/hub/hub_bridge_register.py @@ -1,4 +1,4 @@ -"""中控只读 Bridge 路由 + SSO 登录入口.""" +"""中控 Bridge:只读快照 + 受限写(买一全平) + SSO 登录入口.""" from __future__ import annotations from typing import Any @@ -151,6 +151,34 @@ def register_hub_bridge_routes(app: Flask, app_module: Any) -> None: return jsonify({"ok": False, "msg": str(e), "version": 1, "enabled": True}), 500 return jsonify(_enrich_hub_protocol(snap, app_module=app_module)) + @app.route("/api/hub/options/close", methods=["POST"]) + def api_hub_options_close(): + """中控下发全平指令:客户端本地买一限价执行;无效买一拒绝.""" + if not hub_bridge_token_ok(request.headers): + return jsonify({"ok": False, "msg": "未授权", "version": 1}), 401 + data = request.get_json(silent=True) or {} + if data.get("market"): + return jsonify({"ok": False, "msg": "已禁用市价平仓,仅支持买一限价", "version": 1}) + # 总览约定只全平,忽略/拒绝自定义张数 + if data.get("sheets") is not None and str(data.get("sheets")).strip() != "": + return jsonify({"ok": False, "msg": "中控平仓仅支持全平,勿传 sheets", "version": 1}) + inst_id = (data.get("inst_id") or "").strip() + if not inst_id: + return jsonify({"ok": False, "msg": "缺少 inst_id", "version": 1}) + hub_ctx = app.config.get("HUB_CTX") or {} + fn = hub_ctx.get("options_close_fn") + if not callable(fn): + return jsonify({"ok": False, "msg": "平仓接口未注册", "version": 1}), 503 + try: + result = fn(inst_id) or {} + except Exception as e: + return jsonify({"ok": False, "msg": str(e) or "平仓失败", "version": 1}), 500 + if not isinstance(result, dict): + return jsonify({"ok": False, "msg": "平仓返回异常", "version": 1}), 500 + out = dict(result) + out["version"] = 1 + return jsonify(out) + @app.route("/hub_sso") def hub_sso_login(): token = (request.args.get("token") or "").strip() diff --git a/lib/options/options_register.py b/lib/options/options_register.py index fb05abd..e92b419 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -76,8 +76,13 @@ def _register_options_hub_bridge(app: Flask, cfg: dict[str, Any]) -> None: def snapshot_fn(): return build_options_hub_snapshot(cfg) + def close_fn(inst_id: str) -> dict[str, Any]: + # 中控总览只全平(不改张数);无效买一在执行层拒绝 + return _run_manual_bid_close(cfg, inst_id, sheets=None, reason="中控平仓") + hub_ctx = dict(app.config.get("HUB_CTX") or {}) hub_ctx["options_snapshot_fn"] = snapshot_fn + hub_ctx["options_close_fn"] = close_fn app.config["HUB_CTX"] = hub_ctx @@ -169,6 +174,78 @@ def _require_options_ex(cfg: dict[str, Any]): return ex, "" +def _run_manual_bid_close( + cfg: dict[str, Any], + inst_id: str, + *, + sheets: int | None = None, + reason: str = "手动平仓", +) -> dict[str, Any]: + """手动买一限价平仓(全平或指定张数);残档/无效买一由 close_option_by_bid1 拒绝.""" + ex, err = _require_options_ex(cfg) + if ex is None: + return {"ok": False, "msg": err} + inst_id = (inst_id or "").strip() + if not inst_id: + return {"ok": False, "msg": "缺少 inst_id"} + from lib.options.options_close_exec_lib import close_option_by_bid1 + + result = close_option_by_bid1( + cfg, + ex, + inst_id, + sheets=sheets, + require_recycle_gate=False, + ) + if not result.get("ok"): + return result + from lib.exchange.okx_options_lib import invalidate_option_positions_cache + + invalidate_option_positions_cache() + _sync_options_trades(cfg, force=True) + try: + from lib.options.options_notify_lib import notify_options_close + from lib.options.options_target_lib import cancel_target_monitor + + conn2 = cfg["get_db"]() + try: + if result.get("fully_closed"): + cancel_target_monitor(conn2, inst_id=inst_id) + conn2.commit() + note = reason if result.get("fully_closed") else f"{reason}(部分)" + notify_options_close( + cfg, + conn2, + inst_id=inst_id, + reason=note, + sheets=result.get("submitted_sheets"), + premium_received=result.get("premium_received"), + close_quote=result.get("locked_bid_px") or result.get("bid"), + ) + finally: + conn2.close() + except Exception: + pass + if result.get("fully_closed"): + try: + from lib.options.options_coin_open_lib import maybe_sell_spot_after_close + + spot_sell = maybe_sell_spot_after_close(cfg, ex, inst_id=inst_id, close_result=result) + if spot_sell is not None: + result = dict(result) + result["spot_sell"] = spot_sell + if spot_sell.get("bridge_status") == "pending_sell_spot": + result["msg"] = ( + str(result.get("msg") or "平仓成功") + + ";但卖回 USDT 失败,请点「重试卖回」" + ) + except Exception as e: + result = dict(result) + result["spot_sell"] = {"ok": False, "msg": str(e)} + _mark_balances_stale(cfg) + return result + + def _budget_full_usdc(cfg: dict[str, Any], ex: Any) -> tuple[float | None, str]: """打满可用额度 = min(交易户可用 USDC, 单笔预算);calc_order_size 再乘 budget_buffer.""" from lib.exchange.okx_options_lib import fetch_options_trading_usdc @@ -1538,9 +1615,6 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: @app.route("/api/options/close", methods=["POST"]) @lr def api_options_close(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) data = request.get_json(silent=True) or {} inst_id = (data.get("inst_id") or "").strip() if not inst_id: @@ -1552,62 +1626,8 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: sheets_i = int(sheets) if sheets is not None and str(sheets).strip() != "" else None except (TypeError, ValueError): return jsonify({"ok": False, "msg": "张数无效"}) - from lib.options.options_close_exec_lib import close_option_by_bid1 - # 手动买一平仓:只验有效流动性;2×门控仅用于自动/目标位平仓 - result = close_option_by_bid1( - cfg, - ex, - inst_id, - sheets=sheets_i, - require_recycle_gate=False, - ) - if result.get("ok"): - from lib.exchange.okx_options_lib import invalidate_option_positions_cache - - invalidate_option_positions_cache() - _sync_options_trades(cfg, force=True) - try: - from lib.options.options_target_lib import cancel_target_monitor - from lib.options.options_notify_lib import notify_options_close - - conn2 = cfg["get_db"]() - try: - if result.get("fully_closed"): - cancel_target_monitor(conn2, inst_id=inst_id) - conn2.commit() - reason = "手动平仓" if result.get("fully_closed") else "手动平仓(部分)" - notify_options_close( - cfg, - conn2, - inst_id=inst_id, - reason=reason, - sheets=result.get("submitted_sheets"), - premium_received=result.get("premium_received"), - close_quote=result.get("locked_bid_px") or result.get("bid"), - ) - finally: - conn2.close() - except Exception: - pass - if result.get("fully_closed"): - try: - from lib.options.options_coin_open_lib import maybe_sell_spot_after_close - - spot_sell = maybe_sell_spot_after_close(cfg, ex, inst_id=inst_id, close_result=result) - if spot_sell is not None: - result = dict(result) - result["spot_sell"] = spot_sell - if spot_sell.get("bridge_status") == "pending_sell_spot": - result["msg"] = ( - str(result.get("msg") or "平仓成功") - + ";但卖回 USDT 失败,请点「重试卖回」" - ) - except Exception as e: - result = dict(result) - result["spot_sell"] = {"ok": False, "msg": str(e)} - _mark_balances_stale(cfg) - return jsonify(result) + return jsonify(_run_manual_bid_close(cfg, inst_id, sheets=sheets_i, reason="手动平仓")) @app.route("/api/options/spot-bridge/retry-sell", methods=["POST"]) @lr