Keep unflat legs on 当前持仓 after plan end.

Closed plans with open legs stay visible and closable so residual positions are not orphaned from the UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-14 16:51:33 +08:00
parent 989accdc50
commit a83b6794d8
6 changed files with 92 additions and 14 deletions
+9 -2
View File
@@ -937,12 +937,15 @@
const perpSide = String(perp.side || "").toLowerCase() === "short" ? "空" : "多"; const perpSide = String(perp.side || "").toLowerCase() === "short" ? "空" : "多";
const comboCls = pnlClsNum(combo.net_usdt); const comboCls = pnlClsNum(combo.net_usdt);
const liqWarn = opt.liq_ok === false; const liqWarn = opt.liq_ok === false;
const unflat = !!card.unflat || String(card.status || "") === "closed";
const bidTxt = const bidTxt =
opt.bid != null opt.bid != null
? fmt(opt.bid, 4) + (opt.bid_sz != null ? "/" + fmt(opt.bid_sz, 0) : "") ? fmt(opt.bid, 4) + (opt.bid_sz != null ? "/" + fmt(opt.bid_sz, 0) : "")
: "—"; : "—";
return ( return (
'<article class="hp-live-card" data-plan-id="' + '<article class="hp-live-card' +
(unflat ? " hp-live-card--unflat" : "") +
'" data-plan-id="' +
(card.plan_id || "") + (card.plan_id || "") +
'">' + '">' +
'<div class="hp-live-card-head">' + '<div class="hp-live-card-head">' +
@@ -950,7 +953,11 @@
(card.plan_id || "—") + (card.plan_id || "—") +
" · " + " · " +
(card.underlying || "") + (card.underlying || "") +
"</strong></div>" + "</strong>" +
(unflat
? '<span class="hp-live-unflat-tag" title="计划已结束但腿仍持仓中">未平仓</span>'
: "") +
"</div>" +
'<div class="hp-live-time-row">' + '<div class="hp-live-time-row">' +
"<span>开仓 <strong>" + "<span>开仓 <strong>" +
(card.opened_at || "—") + (card.opened_at || "—") +
+12
View File
@@ -4175,6 +4175,18 @@ html[data-theme="light"] .opt-be-dist-down {
margin-bottom: 8px; margin-bottom: 8px;
font-size: 0.88rem; 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 { .hedge-plan-page-wrap .hp-live-time-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
+28
View File
@@ -192,6 +192,34 @@ def list_plans(
return [dict(r) for r in rows] 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]]: 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() row = conn.execute("SELECT * FROM hedge_plans WHERE id=?", (plan_id,)).fetchone()
return dict(row) if row else None return dict(row) if row else None
+36 -10
View File
@@ -85,15 +85,28 @@ def _parse_opened_ms(opened_at: Any) -> Optional[int]:
return None return None
def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any]]: def _pick_leg(
"""进行中的永期计划 → 期权/永续腿实时卡 + 扣费组合净利.""" legs: list[dict[str, Any]], roles: tuple[str, ...], *, prefer_open: bool = True
from lib.hedge_plan.hedge_plan_db import attach_legs_to_plans, list_plans ) -> 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"): def _has_open_leg(legs: list[dict[str, Any]]) -> bool:
rows.extend(list_plans(conn, status=status, limit=40)) return any(str(x.get("status") or "").lower() == "open" for x in legs)
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 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: if not rows:
return [] return []
plans = attach_legs_to_plans(conn, rows) 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: for plan in plans:
legs = plan.get("legs") or [] 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) plan_st = str(plan.get("status") or "").lower()
perp_leg = next((x for x in legs if str(x.get("leg_role") or "") == "perp"), None) # 已结束计划:仅当仍有未平腿才进当前持仓
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: if not opt_leg and not perp_leg:
continue 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() view = str(plan.get("direction") or "long").lower()
uly = str(plan.get("underlying") or "ETH").upper() 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"), "plan_id": plan.get("id"),
"status": plan.get("status"), "status": plan.get("status"),
"unflat": plan_st == "closed",
"underlying": uly, "underlying": uly,
"direction": view, "direction": view,
"opened_at": opened_at, "opened_at": opened_at,
+6 -1
View File
@@ -1464,7 +1464,12 @@ def execute_manual_close_plan(cfg: dict[str, Any], conn: Any, plan_id: int) -> d
if not plan: if not plan:
return {"ok": False, "msg": "计划不存在"} return {"ok": False, "msg": "计划不存在"}
st = str(plan.get("status") or "") 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 '—'} 不可平仓"} return {"ok": False, "msg": f"当前状态 {st or '—'} 不可平仓"}
# 盯盘无持仓:直接结束 # 盯盘无持仓:直接结束
@@ -431,4 +431,4 @@
</div> </div>
</div> </div>
</div> </div>
<script src="/static/hedge_plan.js?v=56"></script> <script src="/static/hedge_plan.js?v=57"></script>