Add end-plan action; never show unfilled option legs as open.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -201,7 +201,12 @@ def legs_contract_summary(legs: list[dict[str, Any]]) -> str:
|
||||
for leg in legs:
|
||||
role = str(leg.get("leg_role") or "")
|
||||
st = str(leg.get("status") or "").strip().lower()
|
||||
suffix = "(待补)" if st == "pending" else ""
|
||||
if st == "pending":
|
||||
suffix = "(待补)"
|
||||
elif st in ("cancelled", "canceled"):
|
||||
suffix = "(未成交)"
|
||||
else:
|
||||
suffix = ""
|
||||
if role == "perp":
|
||||
name = str(leg.get("symbol") or "永续")
|
||||
parts.append(f"永续 {name}{suffix}")
|
||||
|
||||
@@ -647,3 +647,142 @@ def dump_preview(preview: Any) -> str:
|
||||
return json.dumps(preview, ensure_ascii=False)[:8000]
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _live_option_pos_sheets(ex: Any, inst_id: str) -> float:
|
||||
from lib.exchange.okx_options_lib import fetch_option_positions
|
||||
|
||||
inst_id = (inst_id or "").strip()
|
||||
if not inst_id or ex is None:
|
||||
return 0.0
|
||||
rows = fetch_option_positions(ex)
|
||||
if rows is None:
|
||||
return -1.0 # API 失败:未知
|
||||
for r in rows:
|
||||
if str(r.get("instId") or "").strip() != inst_id:
|
||||
continue
|
||||
try:
|
||||
return abs(float(r.get("pos") or 0))
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
return 0.0
|
||||
|
||||
|
||||
def _sync_plan_status_after_leg_fix(conn: Any, plan_id: int) -> None:
|
||||
"""腿状态校正后:有 open + pending → partial."""
|
||||
from lib.hedge_plan.hedge_plan_db import get_plan, get_plan_legs, update_plan
|
||||
|
||||
plan = get_plan(conn, int(plan_id))
|
||||
if not plan:
|
||||
return
|
||||
pst = str(plan.get("status") or "")
|
||||
if pst not in ("opening", "active", "partial"):
|
||||
return
|
||||
legs = get_plan_legs(conn, int(plan_id))
|
||||
statuses = [str(l.get("status") or "").lower() for l in legs]
|
||||
n_open = sum(1 for s in statuses if s == "open")
|
||||
n_pending = sum(1 for s in statuses if s == "pending")
|
||||
if n_pending and n_open:
|
||||
update_plan(conn, int(plan_id), status="partial", close_reason="partial_fail")
|
||||
|
||||
|
||||
def reconcile_unfilled_option_legs(cfg: dict[str, Any], conn: Any, plan_id: int) -> list[str]:
|
||||
"""未成交却标 open 的期权腿 → pending(可补开);不显示成持仓."""
|
||||
from lib.hedge_plan.hedge_plan_db import get_plan_legs, update_leg
|
||||
from lib.exchange.okx_options_lib import fetch_option_order
|
||||
|
||||
ex = cfg.get("exchange_options")
|
||||
notes: list[str] = []
|
||||
legs = get_plan_legs(conn, int(plan_id))
|
||||
for leg in legs:
|
||||
role = str(leg.get("leg_role") or "")
|
||||
if not role.startswith("option"):
|
||||
continue
|
||||
st = str(leg.get("status") or "").lower()
|
||||
if st != "open":
|
||||
continue
|
||||
inst = str(leg.get("inst_id") or "").strip()
|
||||
oid = str(leg.get("exchange_ord_id") or "").strip()
|
||||
leg_id = int(leg["id"])
|
||||
sheets = _live_option_pos_sheets(ex, inst)
|
||||
if sheets < 0:
|
||||
continue # 查仓失败不改
|
||||
if sheets >= 1:
|
||||
continue
|
||||
# 无实仓:再看订单是否已成交(仍挂单只改 pending,不撤单)
|
||||
if ex is not None and inst and oid:
|
||||
od = fetch_option_order(ex, inst_id=inst, ord_id=oid)
|
||||
if od.get("ok"):
|
||||
acc = float(od.get("acc_fill_sz") or 0)
|
||||
ostate = str(od.get("state") or "")
|
||||
if acc >= 1 or ostate == "filled":
|
||||
continue # 有成交但仓位暂未同步,暂不改
|
||||
update_leg(
|
||||
conn,
|
||||
leg_id,
|
||||
status="pending",
|
||||
close_reason=None,
|
||||
closed_at=None,
|
||||
avg_open=None,
|
||||
premium=0,
|
||||
)
|
||||
notes.append(f"{inst} 无成交却标open→pending")
|
||||
if notes:
|
||||
_sync_plan_status_after_leg_fix(conn, int(plan_id))
|
||||
return notes
|
||||
|
||||
|
||||
def execute_manual_end_plan(cfg: dict[str, Any], conn: Any, plan_id: int) -> dict[str, Any]:
|
||||
"""人工结束进行中计划:不自动平仓;未成交腿标 cancelled."""
|
||||
from lib.hedge_plan.hedge_plan_db import get_plan, get_plan_legs, update_leg, update_plan
|
||||
from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_end
|
||||
from lib.exchange.okx_options_lib import cancel_option_order
|
||||
|
||||
plan = get_plan(conn, int(plan_id))
|
||||
if not plan:
|
||||
return {"ok": False, "msg": "计划不存在"}
|
||||
st = str(plan.get("status") or "")
|
||||
if st not in ("opening", "active", "partial"):
|
||||
return {"ok": False, "msg": f"当前状态 {st or '—'} 不可结束"}
|
||||
|
||||
notes = reconcile_unfilled_option_legs(cfg, conn, int(plan_id))
|
||||
ex = cfg.get("exchange_options")
|
||||
legs = get_plan_legs(conn, int(plan_id))
|
||||
for leg in legs:
|
||||
lst = str(leg.get("status") or "").lower()
|
||||
inst = str(leg.get("inst_id") or "").strip()
|
||||
oid = str(leg.get("exchange_ord_id") or "").strip()
|
||||
if lst == "pending":
|
||||
if ex is not None and inst and oid:
|
||||
cancel_option_order(ex, inst_id=inst, ord_id=oid)
|
||||
update_leg(
|
||||
conn,
|
||||
int(leg["id"]),
|
||||
status="cancelled",
|
||||
close_reason="manual_end",
|
||||
closed_at=_now(),
|
||||
avg_open=None,
|
||||
premium=0,
|
||||
)
|
||||
notes.append(f"{inst or leg.get('leg_role')} 待补→cancelled")
|
||||
|
||||
update_plan(
|
||||
conn,
|
||||
int(plan_id),
|
||||
status="closed",
|
||||
close_reason="manual",
|
||||
closed_at=_now(),
|
||||
note=((plan.get("note") or "") + " · 人工结束(不平仓)").strip(" ·")[:500],
|
||||
)
|
||||
plan2 = get_plan(conn, int(plan_id))
|
||||
if plan2:
|
||||
try:
|
||||
notify_plan_end(cfg, conn, plan2)
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"ok": True,
|
||||
"plan_id": int(plan_id),
|
||||
"msg": "计划已结束(未自动平仓;有持仓请自行平掉)",
|
||||
"notes": notes,
|
||||
}
|
||||
|
||||
@@ -588,6 +588,24 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
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
|
||||
def api_hedge_end_plan(plan_id: int):
|
||||
"""人工结束进行中计划:不自动平仓;未成交腿改为 cancelled."""
|
||||
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables
|
||||
from lib.hedge_plan.hedge_plan_orders_lib import execute_manual_end_plan
|
||||
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
out = execute_manual_end_plan(cfg, conn, plan_id)
|
||||
if not out.get("ok"):
|
||||
return jsonify(out), 400
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
return jsonify(out)
|
||||
|
||||
@app.route("/api/hedge-plan/<int:plan_id>/complete-leg", methods=["POST"])
|
||||
@lr
|
||||
def api_hedge_complete_leg(plan_id: int):
|
||||
@@ -742,6 +760,7 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
init_hedge_plan_tables,
|
||||
list_plans,
|
||||
)
|
||||
from lib.hedge_plan.hedge_plan_orders_lib import reconcile_unfilled_option_legs
|
||||
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
@@ -750,6 +769,16 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
for status in ("opening", "active", "partial"):
|
||||
rows.extend(list_plans(conn, status=status, limit=80))
|
||||
rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
|
||||
for row in rows:
|
||||
try:
|
||||
reconcile_unfilled_option_legs(cfg, conn, int(row["id"]))
|
||||
except Exception:
|
||||
pass
|
||||
# 校正后可能 status 变化,重新拉一遍
|
||||
rows = []
|
||||
for status in ("opening", "active", "partial"):
|
||||
rows.extend(list_plans(conn, status=status, limit=80))
|
||||
rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
|
||||
plans = attach_legs_to_plans(conn, rows)
|
||||
conn.commit()
|
||||
finally:
|
||||
@@ -779,6 +808,7 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
init_hedge_plan_tables,
|
||||
legs_contract_summary,
|
||||
)
|
||||
from lib.hedge_plan.hedge_plan_orders_lib import reconcile_unfilled_option_legs
|
||||
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
@@ -786,6 +816,10 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
plan = get_plan(conn, plan_id)
|
||||
if not plan:
|
||||
return jsonify({"ok": False, "msg": "计划不存在"}), 404
|
||||
# 打开细节时校正:无成交却标 open → cancelled
|
||||
if str(plan.get("status") or "") in ("opening", "active", "partial"):
|
||||
reconcile_unfilled_option_legs(cfg, conn, plan_id)
|
||||
plan = get_plan(conn, plan_id) or plan
|
||||
legs = get_plan_legs(conn, plan_id)
|
||||
conn.commit()
|
||||
finally:
|
||||
|
||||
@@ -301,4 +301,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/hedge_plan.js?v=27"></script>
|
||||
<script src="/static/hedge_plan.js?v=28"></script>
|
||||
|
||||
Reference in New Issue
Block a user