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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user