中控可下发买一全平:客户端新增 /api/hub/options/close,由本地执行并拒绝无效买一。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-02 16:03:10 +08:00
parent 34322d1731
commit fcef3b3d37
2 changed files with 107 additions and 59 deletions
+29 -1
View File
@@ -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()
+78 -58
View File
@@ -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