diff --git a/docs/更新文档.md b/docs/更新文档.md index 590733d..1bbefaf 100644 --- a/docs/更新文档.md +++ b/docs/更新文档.md @@ -4,6 +4,25 @@ --- +## 2026-07-19 · 期期情景测算:盈亏配色 + 盈亏比(亏=全额保费) + +### 修改原因 + +情景弹窗合计无红绿区分;需要一眼看盈亏,并按「最大亏损=权利金全亏」给出上破/下破盈亏比。 + +### 修改的地方 + +| 文件 | 改动摘要 | +|------|----------| +| `hedge_plan.js` | 合计/腿盈亏用 `hp-pnl-pos/neg`;摘要显示盈亏比 | +| `hedge_plan_calc_lib.py` | summary 增加 `rr_at_up` / `rr_at_down` / `rr_risk_premium` | + +### 交付之后的验收 + +正数为绿、负数为红;摘要可见「盈亏比 上破 x:1 / 下破 y:1(亏=全额保费)」。 + +--- + ## 2026-07-19 · 修复期期「按张数」拆分:用同张数总张数 2n ### 修改原因 diff --git a/docs/系统说明.md b/docs/系统说明.md index c116bf3..0f933f7 100644 --- a/docs/系统说明.md +++ b/docs/系统说明.md @@ -149,7 +149,8 @@ ### 3.5 情景测算弹窗 测算不再占页面下方大块区域:点 **计算** 弹出结果,底部 **启动计划 / 取消**。 -取消只关窗;启动按当前参数真实下单(仍受门禁约束)。 +取消只关窗;启动按当前参数真实下单(仍受门禁约束)。 +期期弹窗:合计盈亏绿/红配色;摘要显示盈亏比(盈利÷全额保费,亏损按权利金全亏计)。 ### 3.6 进行中 / 历史 / 统计 diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index b7a8f45..9e9fef5 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -121,6 +121,24 @@ return '' + label + ""; } + function pnlClass(v) { + if (v == null || v === "") return ""; + const n = Number(v); + if (Number.isNaN(n)) return ""; + return n >= 0 ? "hp-pnl-pos" : "hp-pnl-neg"; + } + + function fmtPnlHtml(v, digits) { + if (v == null || v === "" || Number.isNaN(Number(v))) return "—"; + const cls = pnlClass(v); + return '' + fmt(v, digits) + ""; + } + + function fmtRr(v) { + if (v == null || Number.isNaN(Number(v))) return "—"; + return fmt(v, 2) + ":1"; + } + function matchesMoneyFilter(c) { const f = state.moneyFilter || "all"; if (f === "all") return true; @@ -997,21 +1015,35 @@ if (summary) { if (d.plan_type === "perp_options") { summary.innerHTML = - "止盈合计 " + - fmt(s.tp_total) + - " · 止损合计 " + - fmt(s.sl_total) + - " · 保费 " + + "止盈合计 " + + fmtPnlHtml(s.tp_total) + + " · 止损合计 " + + fmtPnlHtml(s.sl_total) + + " · 保费 " + fmt(s.premium_paid) + (s.hedge_ratio_at_sl != null ? " · 止损对冲率 " + fmt(s.hedge_ratio_at_sl) + "%" : ""); } else { + const upTot = s.at_target_up_total != null ? s.at_target_up_total : s.at_target_total; + const dnTot = s.at_target_down_total; + let rrLine = ""; + if (s.rr_at_up != null || s.rr_at_down != null) { + rrLine = + " · 盈亏比 上破 " + + fmtRr(s.rr_at_up) + + (dnTot != null ? " / 下破 " + fmtRr(s.rr_at_down) : "") + + '(亏=全额保费 ' + + fmt(s.rr_risk_premium != null ? s.rr_risk_premium : s.premium_paid) + + ")"; + } summary.innerHTML = - "目标价合计 " + - fmt(s.at_target_total) + - " · 到期现价 " + - fmt(s.expiry_flat_total) + - " · 保费 " + + "上破 " + + fmtPnlHtml(upTot) + + (dnTot != null ? " · 下破 " + fmtPnlHtml(dnTot) : "") + + " · 到期现价 " + + fmtPnlHtml(s.expiry_flat_total) + + " · 保费 " + fmt(s.premium_paid) + + rrLine + (s.expiry_is_loss ? " · 到期无盈利(记总亏损)" : ""); } } @@ -1021,11 +1053,11 @@ const tr = document.createElement("tr"); let mid; if (sc.perp_pnl != null) { - mid = "永续 " + fmt(sc.perp_pnl); + mid = "永续 " + fmtPnlHtml(sc.perp_pnl); } else { - mid = "A " + fmt(sc.leg_a_pnl) + " / B " + fmt(sc.leg_b_pnl); + mid = "A " + fmtPnlHtml(sc.leg_a_pnl) + " / B " + fmtPnlHtml(sc.leg_b_pnl); } - const optCol = sc.options_pnl != null ? fmt(sc.options_pnl) : "—"; + const optCol = sc.options_pnl != null ? fmtPnlHtml(sc.options_pnl) : "—"; tr.innerHTML = "" + (sc.label || sc.id) + @@ -1035,9 +1067,9 @@ mid + "" + optCol + - "" + - fmt(sc.total) + - "" + + "" + + fmtPnlHtml(sc.total) + + "" + (sc.note || "") + ""; tbody.appendChild(tr); diff --git a/lib/hedge_plan/hedge_plan_calc_lib.py b/lib/hedge_plan/hedge_plan_calc_lib.py index 2d8d91d..e508c21 100644 --- a/lib/hedge_plan/hedge_plan_calc_lib.py +++ b/lib/hedge_plan/hedge_plan_calc_lib.py @@ -541,6 +541,10 @@ def build_options_options_preview( "expiry_flat_total": round(expiry_loss, 4), "premium_paid": round(prem, 6), "expiry_is_loss": flat_total <= 0, + # 盈亏比:盈利/全亏保费(风险=权利金全损) + "rr_risk_premium": round(prem, 6), + "rr_at_up": round(at_up / prem, 4) if prem > 0 else None, + "rr_at_down": round(at_dn / prem, 4) if prem > 0 else None, }, } diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html index c59694f..0dbfa90 100644 --- a/lib/hedge_plan/templates/hedge_plan_panel.html +++ b/lib/hedge_plan/templates/hedge_plan_panel.html @@ -301,4 +301,4 @@ - + diff --git a/tests/test_hedge_plan_calc.py b/tests/test_hedge_plan_calc.py index 6bf1724..b3a4836 100644 --- a/tests/test_hedge_plan_calc.py +++ b/tests/test_hedge_plan_calc.py @@ -110,6 +110,9 @@ class TestHedgePlanCalc(unittest.TestCase): ) self.assertEqual(p["summary"]["premium_paid"], 10) self.assertTrue(p["summary"]["expiry_is_loss"]) + self.assertEqual(p["summary"]["rr_risk_premium"], 10) + self.assertIsNotNone(p["summary"]["rr_at_up"]) + self.assertAlmostEqual(p["summary"]["rr_at_up"], p["summary"]["at_target_up_total"] / 10, places=4) self.assertEqual(len(p["scenarios"]), 4) self.assertEqual(p["scenarios"][0]["id"], "target_up") self.assertEqual(p["scenarios"][1]["id"], "target_down")