Color 期期 preview PnL and show RR vs full premium loss.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-19 15:43:01 +08:00
parent 7938628485
commit 38ac258497
6 changed files with 77 additions and 18 deletions
+19
View File
@@ -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
### 修改原因
+2 -1
View File
@@ -149,7 +149,8 @@
### 3.5 情景测算弹窗
测算不再占页面下方大块区域:点 **计算** 弹出结果,底部 **启动计划 / 取消**
取消只关窗;启动按当前参数真实下单(仍受门禁约束)。
取消只关窗;启动按当前参数真实下单(仍受门禁约束)。
期期弹窗:合计盈亏绿/红配色;摘要显示盈亏比(盈利÷全额保费,亏损按权利金全亏计)。
### 3.6 进行中 / 历史 / 统计
+48 -16
View File
@@ -121,6 +121,24 @@
return '<span class="opt-moneyness opt-moneyness-' + m + '">' + label + "</span>";
}
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 '<strong class="' + cls + '">' + fmt(v, digits) + "</strong>";
}
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 =
"止盈合计 <strong>" +
fmt(s.tp_total) +
"</strong> · 止损合计 <strong>" +
fmt(s.sl_total) +
"</strong> · 保费 " +
"止盈合计 " +
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) : "") +
'<span class="muted">(亏=全额保费 ' +
fmt(s.rr_risk_premium != null ? s.rr_risk_premium : s.premium_paid) +
"</span>";
}
summary.innerHTML =
"目标价合计 <strong>" +
fmt(s.at_target_total) +
"</strong> · 到期现价 <strong>" +
fmt(s.expiry_flat_total) +
"</strong> · 保费 " +
"上破 " +
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 =
"<td>" +
(sc.label || sc.id) +
@@ -1035,9 +1067,9 @@
mid +
"</td><td>" +
optCol +
"</td><td><strong>" +
fmt(sc.total) +
"</strong></td><td class=\"muted\">" +
"</td><td>" +
fmtPnlHtml(sc.total) +
"</td><td class=\"muted\">" +
(sc.note || "") +
"</td>";
tbody.appendChild(tr);
+4
View File
@@ -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,
},
}
@@ -301,4 +301,4 @@
</div>
</div>
</div>
<script src="/static/hedge_plan.js?v=24"></script>
<script src="/static/hedge_plan.js?v=25"></script>
+3
View File
@@ -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")