Fix strategy-logic P0s from audit: monitor false-flat, fill-confirmed open/close, mode gates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-30 09:56:18 +08:00
parent fa7ff739a0
commit 4b4dca9e3c
15 changed files with 820 additions and 167 deletions
+77 -54
View File
@@ -186,13 +186,14 @@ def _gates_dict(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
raw = fetch_option_positions(ex) if ex is not None else []
has_standalone = has_standalone_option_position(conn, raw or [])
except Exception:
has_standalone = False
has_standalone = True # fail-closed
conn.commit()
finally:
conn.close()
except Exception:
active = 0
has_standalone = False
# fail-closed:探测失败视为不可开仓
active = 10**9
has_standalone = True
return gate_status(
hedge_enabled=_hedge_enabled(),
sizing_mode=load_position_sizing_mode(),
@@ -220,31 +221,45 @@ def _gates_public(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
def _maybe_start_monitor(cfg: dict[str, Any]) -> None:
if not _hedge_enabled():
return
try:
secs = float(os.getenv("HEDGE_PLAN_MONITOR_POLL_SECONDS") or "15")
except ValueError:
secs = 15.0
secs = max(5.0, secs)
# 始终启动监控线程:单独期权模式下仍需收口遗留 active/partial 计划
with _hedge_start_lock():
if cfg.get("hedge_monitor_thread") is not None:
return
try:
secs = float(os.getenv("HEDGE_PLAN_MONITOR_POLL_SECONDS") or "15")
except ValueError:
secs = 15.0
secs = max(5.0, secs)
def _loop() -> None:
import time
def _loop() -> None:
import time
from lib.hedge_plan.hedge_plan_monitor_lib import tick_active_plans
from lib.hedge_plan.hedge_plan_monitor_lib import tick_active_plans
while True:
try:
tick_active_plans(cfg)
except Exception:
pass
time.sleep(secs)
while True:
try:
tick_active_plans(cfg)
except Exception:
pass
time.sleep(secs)
import threading
import threading
t = threading.Thread(target=_loop, name="hedge-plan-monitor", daemon=True)
t.start()
cfg["hedge_monitor_thread"] = t
t = threading.Thread(target=_loop, name="hedge-plan-monitor", daemon=True)
t.start()
cfg["hedge_monitor_thread"] = t
_start_lock = None
def _hedge_start_lock():
global _start_lock
if _start_lock is None:
import threading
_start_lock = threading.Lock()
return _start_lock
def _start_body_json(body: dict[str, Any], missing_leg: Optional[str] = None) -> str:
@@ -566,37 +581,38 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
body = request.get_json(silent=True) or {}
plan_type = (body.get("plan_type") or "perp_options").strip().lower()
dry_run = bool(body.get("dry_run")) or _env_bool("HEDGE_PLAN_DRY_RUN", False)
gates = _gates_dict(cfg, plan_type)
if not dry_run and not gates.get("can_start"):
return jsonify(
{"ok": False, "msg": "; ".join(gates.get("reasons") or ["不可开仓"]), "gates": gates}
), 400
err = validate_start_body(plan_type, body)
if err:
return jsonify({"ok": False, "msg": err, "gates": gates}), 400
# 补齐永续杠杆
if plan_type == "perp_options" and not body.get("leverage"):
base = str(body.get("underlying") or "ETH").upper()
body["leverage"] = cfg.get("btc_leverage") if base == "BTC" else (cfg.get("btc_leverage") or 10)
# ETH 也用 BTC 档 10x 按方案;ALT 为 alt_leverage 仅非 BTC/ETH
if base in ("BTC", "ETH"):
body["leverage"] = int(cfg.get("btc_leverage") or 10)
if plan_type == "options_options":
out = execute_options_options_start(
cfg,
body,
dry_run=dry_run,
persist=(None if dry_run else (lambda r, b: _persist_oo(cfg, r, b))),
)
else:
out = execute_perp_options_start(
cfg,
body,
dry_run=dry_run,
persist=(None if dry_run else (lambda r, b: _persist_po(cfg, r, b))),
)
out["gates"] = gates
return jsonify(out), (200 if out.get("ok") else 400)
with _hedge_start_lock():
gates = _gates_dict(cfg, plan_type)
if not dry_run and not gates.get("can_start"):
return jsonify(
{"ok": False, "msg": "; ".join(gates.get("reasons") or ["不可开仓"]), "gates": gates}
), 400
err = validate_start_body(plan_type, body)
if err:
return jsonify({"ok": False, "msg": err, "gates": gates}), 400
# 补齐永续杠杆
if plan_type == "perp_options" and not body.get("leverage"):
base = str(body.get("underlying") or "ETH").upper()
body["leverage"] = cfg.get("btc_leverage") if base == "BTC" else (cfg.get("btc_leverage") or 10)
# ETH 也用 BTC 档 10x 按方案;ALT 为 alt_leverage 仅非 BTC/ETH
if base in ("BTC", "ETH"):
body["leverage"] = int(cfg.get("btc_leverage") or 10)
if plan_type == "options_options":
out = execute_options_options_start(
cfg,
body,
dry_run=dry_run,
persist=(None if dry_run else (lambda r, b: _persist_oo(cfg, r, b))),
)
else:
out = execute_perp_options_start(
cfg,
body,
dry_run=dry_run,
persist=(None if dry_run else (lambda r, b: _persist_po(cfg, r, b))),
)
out["gates"] = gates
return jsonify(out), (200 if out.get("ok") else 400)
@app.route("/api/hedge-plan/<int:plan_id>/end", methods=["POST"])
@lr
@@ -634,12 +650,19 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
body = request.get_json(silent=True) or {}
dry_run = bool(body.get("dry_run")) or _env_bool("HEDGE_PLAN_DRY_RUN", False)
if not dry_run and not _hedge_enabled():
return jsonify({"ok": False, "msg": "当前交易模式为单独期权,不可补开对冲腿"}), 400
conn = cfg["get_db"]()
try:
init_hedge_plan_tables(conn)
plan = get_plan(conn, plan_id)
if not plan:
return jsonify({"ok": False, "msg": "计划不存在"}), 404
pt = str(plan.get("plan_type") or "")
if pt == "perp_options" and not _show_perp_options():
return jsonify({"ok": False, "msg": "当前模式非永期对冲,不可补开"}), 400
if pt == "options_options" and not _show_options_options():
return jsonify({"ok": False, "msg": "当前模式非期期对冲,不可补开"}), 400
if str(plan.get("status") or "") != "partial":
return jsonify({"ok": False, "msg": "仅半腿待补(partial)计划可补开"}), 400
legs = get_plan_legs(conn, plan_id)