From a83b6794d868b91ad58a586e45001ee06a049610 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 14 Oct 2026 16:51:33 +0800 Subject: [PATCH] =?UTF-8?q?Keep=20unflat=20legs=20on=20=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E6=8C=81=E4=BB=93=20after=20plan=20end.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closed plans with open legs stay visible and closable so residual positions are not orphaned from the UI. Co-authored-by: Cursor --- lib/common/static/hedge_plan.js | 11 ++++- lib/common/static/instance_theme.css | 12 +++++ lib/hedge_plan/hedge_plan_db.py | 28 +++++++++++ .../hedge_plan_live_positions_lib.py | 46 +++++++++++++++---- lib/hedge_plan/hedge_plan_orders_lib.py | 7 ++- .../templates/hedge_plan_panel.html | 2 +- 6 files changed, 92 insertions(+), 14 deletions(-) diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index 2738e9f..15906ac 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -937,12 +937,15 @@ const perpSide = String(perp.side || "").toLowerCase() === "short" ? "空" : "多"; const comboCls = pnlClsNum(combo.net_usdt); const liqWarn = opt.liq_ok === false; + const unflat = !!card.unflat || String(card.status || "") === "closed"; const bidTxt = opt.bid != null ? fmt(opt.bid, 4) + (opt.bid_sz != null ? "/" + fmt(opt.bid_sz, 0) : "") : "—"; return ( - '
' + '
' + @@ -950,7 +953,11 @@ (card.plan_id || "—") + " · " + (card.underlying || "") + - "
" + + "" + + (unflat + ? '未平仓' + : "") + + "" + '
' + "开仓 " + (card.opened_at || "—") + diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index ef3c783..dbef1a8 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -4175,6 +4175,18 @@ html[data-theme="light"] .opt-be-dist-down { margin-bottom: 8px; font-size: 0.88rem; } +.hedge-plan-page-wrap .hp-live-card--unflat { + border-color: rgba(245, 197, 66, 0.35); +} +.hedge-plan-page-wrap .hp-live-unflat-tag { + font-size: 0.72rem; + font-weight: 650; + color: #1a1408; + background: #f5c542; + padding: 2px 8px; + border-radius: 4px; + white-space: nowrap; +} .hedge-plan-page-wrap .hp-live-time-row { display: flex; flex-wrap: wrap; diff --git a/lib/hedge_plan/hedge_plan_db.py b/lib/hedge_plan/hedge_plan_db.py index 729d116..53f7dee 100644 --- a/lib/hedge_plan/hedge_plan_db.py +++ b/lib/hedge_plan/hedge_plan_db.py @@ -192,6 +192,34 @@ def list_plans( return [dict(r) for r in rows] +def list_plans_for_live_positions( + conn: sqlite3.Connection, + *, + plan_type: str = "perp_options", + limit: int = 40, +) -> list[dict[str, Any]]: + """当前持仓用:进行中计划 + 已结束但仍有 open 腿(人工结束未平仓残留).""" + rows = conn.execute( + """ + SELECT DISTINCT p.* + FROM hedge_plans p + LEFT JOIN hedge_plan_legs l ON l.plan_id = p.id + WHERE p.plan_type = ? + AND ( + p.status IN ('opening', 'active', 'partial') + OR ( + p.status = 'closed' + AND lower(coalesce(l.status, '')) = 'open' + ) + ) + ORDER BY p.id DESC + LIMIT ? + """, + (str(plan_type), int(limit)), + ).fetchall() + return [dict(r) for r in rows] + + def get_plan(conn: sqlite3.Connection, plan_id: int) -> Optional[dict[str, Any]]: row = conn.execute("SELECT * FROM hedge_plans WHERE id=?", (plan_id,)).fetchone() return dict(row) if row else None diff --git a/lib/hedge_plan/hedge_plan_live_positions_lib.py b/lib/hedge_plan/hedge_plan_live_positions_lib.py index 8455c2c..5a56b0d 100644 --- a/lib/hedge_plan/hedge_plan_live_positions_lib.py +++ b/lib/hedge_plan/hedge_plan_live_positions_lib.py @@ -85,15 +85,28 @@ def _parse_opened_ms(opened_at: Any) -> Optional[int]: return None -def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any]]: - """进行中的永期计划 → 期权/永续腿实时卡 + 扣费组合净利.""" - from lib.hedge_plan.hedge_plan_db import attach_legs_to_plans, list_plans +def _pick_leg( + legs: list[dict[str, Any]], roles: tuple[str, ...], *, prefer_open: bool = True +) -> Optional[dict[str, Any]]: + matched = [x for x in legs if str(x.get("leg_role") or "") in roles] + if not matched: + return None + if prefer_open: + open_ones = [x for x in matched if str(x.get("status") or "").lower() == "open"] + if open_ones: + return open_ones[0] + return matched[0] - rows: list[dict[str, Any]] = [] - for status in ("opening", "active", "partial"): - rows.extend(list_plans(conn, status=status, limit=40)) - rows = [r for r in rows if str(r.get("plan_type") or "") == "perp_options"] - rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True) + +def _has_open_leg(legs: list[dict[str, Any]]) -> bool: + return any(str(x.get("status") or "").lower() == "open" for x in legs) + + +def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any]]: + """当前持仓:未平完的永期仓(含计划已结束但腿仍 open).""" + from lib.hedge_plan.hedge_plan_db import attach_legs_to_plans, list_plans_for_live_positions + + rows = list_plans_for_live_positions(conn, plan_type="perp_options", limit=40) if not rows: return [] plans = attach_legs_to_plans(conn, rows) @@ -121,10 +134,22 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any for plan in plans: legs = plan.get("legs") or [] - opt_leg = next((x for x in legs if str(x.get("leg_role") or "") in ("option_hedge", "option_a")), None) - perp_leg = next((x for x in legs if str(x.get("leg_role") or "") == "perp"), None) + plan_st = str(plan.get("status") or "").lower() + # 已结束计划:仅当仍有未平腿才进当前持仓 + if plan_st == "closed" and not _has_open_leg(legs): + continue + opt_leg = _pick_leg(legs, ("option_hedge", "option_a")) + perp_leg = _pick_leg(legs, ("perp",)) if not opt_leg and not perp_leg: continue + # 已结束:只展示仍 open 的腿;若该腿已平则置空 + if plan_st == "closed": + if opt_leg and str(opt_leg.get("status") or "").lower() != "open": + opt_leg = None + if perp_leg and str(perp_leg.get("status") or "").lower() != "open": + perp_leg = None + if not opt_leg and not perp_leg: + continue view = str(plan.get("direction") or "long").lower() uly = str(plan.get("underlying") or "ETH").upper() @@ -218,6 +243,7 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any { "plan_id": plan.get("id"), "status": plan.get("status"), + "unflat": plan_st == "closed", "underlying": uly, "direction": view, "opened_at": opened_at, diff --git a/lib/hedge_plan/hedge_plan_orders_lib.py b/lib/hedge_plan/hedge_plan_orders_lib.py index 7ef36d7..0ebcbf5 100644 --- a/lib/hedge_plan/hedge_plan_orders_lib.py +++ b/lib/hedge_plan/hedge_plan_orders_lib.py @@ -1464,7 +1464,12 @@ def execute_manual_close_plan(cfg: dict[str, Any], conn: Any, plan_id: int) -> d if not plan: return {"ok": False, "msg": "计划不存在"} st = str(plan.get("status") or "") - if st not in ("opening", "active", "partial", "watching"): + legs0 = get_plan_legs(conn, int(plan_id)) + has_open_leg = any(str(x.get("status") or "").lower() == "open" for x in legs0) + # 进行中可平;已结束但腿仍 open(人工结束未平仓残留)也可补平 + if st not in ("opening", "active", "partial", "watching") and not ( + st == "closed" and has_open_leg + ): return {"ok": False, "msg": f"当前状态 {st or '—'} 不可平仓"} # 盯盘无持仓:直接结束 diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html index ffb7037..fb95535 100644 --- a/lib/hedge_plan/templates/hedge_plan_panel.html +++ b/lib/hedge_plan/templates/hedge_plan_panel.html @@ -431,4 +431,4 @@
- +