中控可下发买一全平:客户端新增 /api/hub/options/close,由本地执行并拒绝无效买一。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
"""中控只读 Bridge 路由 + SSO 登录入口."""
|
"""中控 Bridge:只读快照 + 受限写(买一全平) + SSO 登录入口."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
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({"ok": False, "msg": str(e), "version": 1, "enabled": True}), 500
|
||||||
return jsonify(_enrich_hub_protocol(snap, app_module=app_module))
|
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")
|
@app.route("/hub_sso")
|
||||||
def hub_sso_login():
|
def hub_sso_login():
|
||||||
token = (request.args.get("token") or "").strip()
|
token = (request.args.get("token") or "").strip()
|
||||||
|
|||||||
@@ -76,8 +76,13 @@ def _register_options_hub_bridge(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
def snapshot_fn():
|
def snapshot_fn():
|
||||||
return build_options_hub_snapshot(cfg)
|
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 = dict(app.config.get("HUB_CTX") or {})
|
||||||
hub_ctx["options_snapshot_fn"] = snapshot_fn
|
hub_ctx["options_snapshot_fn"] = snapshot_fn
|
||||||
|
hub_ctx["options_close_fn"] = close_fn
|
||||||
app.config["HUB_CTX"] = hub_ctx
|
app.config["HUB_CTX"] = hub_ctx
|
||||||
|
|
||||||
|
|
||||||
@@ -169,6 +174,78 @@ def _require_options_ex(cfg: dict[str, Any]):
|
|||||||
return ex, ""
|
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]:
|
def _budget_full_usdc(cfg: dict[str, Any], ex: Any) -> tuple[float | None, str]:
|
||||||
"""打满可用额度 = min(交易户可用 USDC, 单笔预算);calc_order_size 再乘 budget_buffer."""
|
"""打满可用额度 = min(交易户可用 USDC, 单笔预算);calc_order_size 再乘 budget_buffer."""
|
||||||
from lib.exchange.okx_options_lib import fetch_options_trading_usdc
|
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"])
|
@app.route("/api/options/close", methods=["POST"])
|
||||||
@lr
|
@lr
|
||||||
def api_options_close():
|
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 {}
|
data = request.get_json(silent=True) or {}
|
||||||
inst_id = (data.get("inst_id") or "").strip()
|
inst_id = (data.get("inst_id") or "").strip()
|
||||||
if not inst_id:
|
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
|
sheets_i = int(sheets) if sheets is not None and str(sheets).strip() != "" else None
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return jsonify({"ok": False, "msg": "张数无效"})
|
return jsonify({"ok": False, "msg": "张数无效"})
|
||||||
from lib.options.options_close_exec_lib import close_option_by_bid1
|
|
||||||
|
|
||||||
# 手动买一平仓:只验有效流动性;2×门控仅用于自动/目标位平仓
|
# 手动买一平仓:只验有效流动性;2×门控仅用于自动/目标位平仓
|
||||||
result = close_option_by_bid1(
|
return jsonify(_run_manual_bid_close(cfg, inst_id, sheets=sheets_i, reason="手动平仓"))
|
||||||
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)
|
|
||||||
|
|
||||||
@app.route("/api/options/spot-bridge/retry-sell", methods=["POST"])
|
@app.route("/api/options/spot-bridge/retry-sell", methods=["POST"])
|
||||||
@lr
|
@lr
|
||||||
|
|||||||
Reference in New Issue
Block a user