From dffcd778370132bf07e422310e21c913508fa64b Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 31 Jul 2026 15:44:52 +0800 Subject: [PATCH] Add trade move points; label option UPL price as bid not mark. Co-authored-by: Cursor --- backend/app/api/trades.py | 43 +++++++++++++++++++++++++++++++++++ backend/app/live/executor.py | 5 ++-- backend/app/sim/matcher.py | 4 ++-- frontend/src/pages/Plan.tsx | 2 +- frontend/src/pages/Trades.tsx | 23 +++++++++++++++++++ 5 files changed, 72 insertions(+), 5 deletions(-) diff --git a/backend/app/api/trades.py b/backend/app/api/trades.py index bb858ba..a25a6f5 100644 --- a/backend/app/api/trades.py +++ b/backend/app/api/trades.py @@ -61,6 +61,46 @@ def _expiry_settle_info(g: dict, fills: list) -> dict | None: } +def _close_index_px(g: dict, fills: list) -> float | None: + """平仓时标的指数:优先 settle_index_px,否则用永续平仓价近似。""" + raw = g.get("settle_index_px") + if raw is not None: + try: + v = float(raw) + if v > 0: + return v + except (TypeError, ValueError): + pass + for row in fills: + f = dict(row) if not isinstance(row, dict) else row + if str(f.get("leg") or "") == "perp" and str(f.get("action") or "") == "close": + try: + v = float(f.get("fill_px") or 0) + if v > 0: + return v + except (TypeError, ValueError): + pass + break + return None + + +def _move_points(g: dict, fills: list) -> float | None: + """开仓指数 → 平仓指数的点数(带符号:上涨为正)。持仓中无平仓价则空。""" + entry = g.get("entry_index_px") + if entry is None: + return None + try: + e = float(entry) + except (TypeError, ValueError): + return None + if e <= 0: + return None + close_px = _close_index_px(g, fills) + if close_px is None: + return None + return round(float(close_px) - e, 2) + + def _enrich_group(g: dict, fills: list) -> dict: summary = summarize_fills_pnl(fills) # LIVE:优先 groups.realized_pnl(已按交易所回写,含资金费) @@ -81,6 +121,9 @@ def _enrich_group(g: dict, fills: list) -> dict: g["expiry_settle"] = info if g.get("settle_index_px") is None and info.get("settle_index_px") is not None: g["settle_index_px"] = info["settle_index_px"] + mp = _move_points(g, fills) + g["move_points"] = mp + g["close_index_px"] = _close_index_px(g, fills) return g diff --git a/backend/app/live/executor.py b/backend/app/live/executor.py index 4994f82..b652302 100644 --- a/backend/app/live/executor.py +++ b/backend/app/live/executor.py @@ -1108,10 +1108,11 @@ class OkxLiveExecutor(Matcher): net = summary.get("net_pnl") if net is None: net = opt_pnl + perp_pnl - of_fee - pf_fee + close_index = float(spot) if spot is not None else None self.db._conn.execute( """UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?, - fees=?, slip_cost=? WHERE group_id=?""", - ("closed", now, reason, float(net), fees, slip, group_id), + fees=?, slip_cost=?, settle_index_px=COALESCE(?, settle_index_px) WHERE group_id=?""", + ("closed", now, reason, float(net), fees, slip, close_index, group_id), ) self.db._conn.execute( """UPDATE positions SET diff --git a/backend/app/sim/matcher.py b/backend/app/sim/matcher.py index c6e01cc..eaeb858 100644 --- a/backend/app/sim/matcher.py +++ b/backend/app/sim/matcher.py @@ -583,7 +583,7 @@ class Matcher: ).fetchone() fees = float(g["fees"] or 0) + pf.fee + of.fee slip = float(g["slip_cost"] or 0) + pf.slip + of.slip - settle_index = float(spot) if is_expiry and spot is not None else None + settle_index = float(spot) if spot is not None else None self.db._conn.execute( """UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?, fees=?, slip_cost=?, note=NULL, settle_index_px=? WHERE group_id=?""", @@ -1045,7 +1045,7 @@ class Matcher: ) est_opt_close_fee = of.fee opt_mark = bid - # 浮盈亏:买一×数量 − 初始权利金 + # 浮盈亏:买一×数量 − 初始权利金(对齐可市价卖出) option_upl = bid * opt_qty - initial_premium elif oq: opt_mark = oq.bid or oq.mark_px diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index 219ba83..8966c46 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -645,7 +645,7 @@ export default function PlanPage() { {fmt(pos?.option_entry_px)}
- 标记/买一 + 买一 {fmt(pos?.option_mark_px)}
diff --git a/frontend/src/pages/Trades.tsx b/frontend/src/pages/Trades.tsx index cd75328..20a6d9f 100644 --- a/frontend/src/pages/Trades.tsx +++ b/frontend/src/pages/Trades.tsx @@ -44,6 +44,9 @@ type Group = { hold_basis?: string | null; strike?: number | null; settle_index_px?: number | null; + entry_index_px?: number | null; + close_index_px?: number | null; + move_points?: number | null; exec_mode?: string | null; expiry_settle?: ExpirySettle | null; pnl_summary?: PnlSummary; @@ -102,6 +105,13 @@ function groupPnl(g: Group) { return g.net_pnl ?? g.pnl_summary?.net_pnl ?? g.realized_pnl; } +/** 开仓→平仓指数点数(带符号) */ +function fmtMovePoints(n: number | null | undefined) { + if (n == null || Number.isNaN(n)) return "—"; + const s = n > 0 ? `+${n.toFixed(1)}` : n.toFixed(1); + return s; +} + export default function TradesPage() { const titleId = useId(); const [groups, setGroups] = useState([]); @@ -196,6 +206,7 @@ export default function TradesPage() { 开 {fmtTime(openMs)} · 平 {fmtTime(closeMs)} · 周期{" "} {fmtHold(g.hold_ms)} + {g.move_points != null ? ` · 波动 ${fmtMovePoints(g.move_points)}` : ""}
@@ -244,6 +255,7 @@ export default function TradesPage() { 开仓时间 平仓时间 持仓时长 + 波动点数 盈亏金额 平仓方式 @@ -269,6 +281,7 @@ export default function TradesPage() { {fmtTime(openMs)} {fmtTime(closeMs)} {fmtHold(g.hold_ms)} + {fmtMovePoints(g.move_points)} {fmt(listPnl)} @@ -389,6 +402,16 @@ export default function TradesPage() { {fmtHold(selectedGroup.hold_ms)}
+
+ 波动点数 + + {fmtMovePoints(selectedGroup.move_points)} + {selectedGroup.entry_index_px != null && + selectedGroup.close_index_px != null + ? `(${fmt(selectedGroup.entry_index_px, 1)}→${fmt(selectedGroup.close_index_px, 1)})` + : ""} + +