From 4db1e0c470bd83ee5f14a89af76ab735f4912f80 Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 8 Aug 2026 14:35:10 +0800 Subject: [PATCH] Estimate semi forward PnL from strike intrinsic on move points. Replace misleading exit-unit label with option/perp point-move preview vs selected K. Co-authored-by: Cursor --- docs/半自动说明.md | 3 +- docs/更新说明.md | 9 ++ frontend/src/pages/Plan.tsx | 187 +++++++++++++++++++++++++++++++++--- 3 files changed, 186 insertions(+), 13 deletions(-) diff --git a/docs/半自动说明.md b/docs/半自动说明.md index 71f3bec..b83394d 100644 --- a/docs/半自动说明.md +++ b/docs/半自动说明.md @@ -64,7 +64,8 @@ - 流动性不足:进入 `liquidity_wait`;回落未达标则取消挂起。 - 半自动平仓**禁止**远虚「只平永续、期权归档」;顺序仍为**先期权后永续**。 -首页预览「指数 → 到点」用**当前指数**示意;真实触发达标用**开仓指数**。 +首页预览「指数 → 到点」用**当前指数**示意;真实触发达标用**开仓指数**。 +「波动点」旁的**顺向预估**按选定行权价的内在价值变化 + 永续点位粗算(不含 IV/时间价值);与「净利基数×k」逆向兑现目标分开显示。 --- diff --git a/docs/更新说明.md b/docs/更新说明.md index 31544bc..9eb1120 100644 --- a/docs/更新说明.md +++ b/docs/更新说明.md @@ -5,6 +5,15 @@ --- +## 2026-08-08 — 半自动波动点顺向预估盈利 + +### 变更 + +1. 「波动点」下方按标的顺向 N 点 + 行权价内在价值变化粗估组合盈利(期权腿/永续腿分列)。 +2. 与「净利基数×k」逆向兑现目标分开显示,不再把基数目标误标为顺向净利。 + +--- + ## 2026-08-08 — 半自动以损定仓预览开仓量 ### 变更 diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index 1e5e264..387aff5 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -225,6 +225,68 @@ function estimateSemiRiskQty(args: { return { ok: false, detail: "预算内无合规 k" }; } +/** + * 标的顺向波动 move 点后的组合盈亏粗估(期权用内在价值变化,永续按点位)。 + * 多=Call+永续空;空=Put+永续多。不含权利金时间价值/IV。 + */ +function estimateSemiMoveProfit(args: { + view: "long" | "short"; + indexPx: number; + strike: number; + movePoints: number; + optionQty: number; + perpQty: number; + feeRate?: number; +}): { + ok: boolean; + detail: string; + targetPx?: number; + optionPnl?: number; + perpPnl?: number; + feesEst?: number; + netEst?: number; +} { + const { + view, + indexPx, + strike, + movePoints, + optionQty, + perpQty, + feeRate = 0, + } = args; + const S = Number(indexPx); + const K = Number(strike); + const move = Number(movePoints); + const oq = Number(optionQty); + const pq = Number(perpQty); + if (!(S > 0) || !(K > 0) || !(move > 0) || !(oq > 0) || !(pq > 0)) { + return { ok: false, detail: "缺指数/行权价/数量" }; + } + const tgt = view === "long" ? S + move : S - move; + const callIntr = (px: number) => Math.max(0, px - K); + const putIntr = (px: number) => Math.max(0, K - px); + const dIntr = + view === "long" + ? callIntr(tgt) - callIntr(S) + : putIntr(tgt) - putIntr(S); + // 多:空永续随涨亏;空:多永续随跌亏 → 均为 -move×qty + const optionPnl = dIntr * oq; + const perpPnl = -move * pq; + const fees = + feeRate > 0 ? S * feeRate * 3 * Math.max(oq, pq) /* 粗估开平 */ : 0; + const net = optionPnl + perpPnl - fees; + return { + ok: true, + detail: "ok", + targetPx: tgt, + optionPnl: Number(optionPnl.toFixed(2)), + perpPnl: Number(perpPnl.toFixed(2)), + feesEst: Number(fees.toFixed(2)), + netEst: Number(net.toFixed(2)), + }; +} + export default function PlanPage() { const [snap, setSnap] = useState(null); const [plan, setPlan] = useState(null); @@ -817,7 +879,8 @@ export default function PlanPage() {
  • 人工定方向、行权类型、永续:期权配比与出场 →「授权开下一单」后机器盯选约/开/平。
  • 平仓后停在等待授权,不自动连开;与全自动循环无关。
  • 虚值:偏离 ≤ 设定点数;杠杆门 ≥180(默认 200)。实值/平值用本单最低杠杆。
  • -
  • 顺向:开仓指数 ± 波动点数,且组合净利 > 0 → 双腿全平(先期权后永续)。
  • +
  • 波动点:标的相对当前价顺向波动 N 点(多涨/空跌);下方按行权价内在价值变化 + 永续点位粗估盈利。
  • +
  • 顺向出场:开仓指数 ± 波动点,且组合净利 > 0 → 双腿全平(先期权后永续)。
  • 逆向兑现:组合净利 ≥ 净利基数 × k → 全平。
  • 以损定仓时配比为单位再乘 k;手动仓按配比直接开。
  • @@ -910,7 +973,7 @@ export default function PlanPage() { />
    - +
    {(() => { - const idx = snap?.index_px ?? ladder?.index_px; - if (idx == null || !Number.isFinite(Number(idx))) { - return `净利目标≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U`; + const shown = filterSemiLadderRows( + ladder?.rows || [], + semiMny, + Number(semiOtmOff) || 25, + semiView, + ); + const pick = pickSizingAskRow(shown); + const idx = + ladder?.index_px != null + ? Number(ladder.index_px) + : snap?.index_px != null + ? Number(snap.index_px) + : null; + const strike = + pick?.strike != null + ? Number(pick.strike) + : ladder?.atm_strike != null + ? Number(ladder.atm_strike) + : null; + const ask = pick?.ask != null ? Number(pick.ask) : null; + const fee = Number(plan.fee_rate ?? 0.0005); + const feeOk = Number.isFinite(fee) && fee >= 0 ? fee : 0.0005; + + let optQty = Number(semiOptU) || 4; + let perpQty = Number(semiPerpU) || 1; + if (riskLocked) { + optQty = Number( + plan.option_qty_eth ?? pos?.option_qty_eth ?? optQty, + ); + perpQty = Number( + plan.perp_qty_eth ?? pos?.perp_qty_eth ?? perpQty, + ); + } else if (riskBased) { + const budget = Number(plan.risk_sizing_preview?.budget); + if ( + ask != null && + idx != null && + Number.isFinite(budget) && + budget > 0 + ) { + const est = estimateSemiRiskQty({ + budget, + indexPx: idx, + optionAsk: ask, + feeRate: feeOk, + perpUnit: Number(semiPerpU) || 1, + optionUnit: Number(semiOptU) || 4, + exitUnit: Number(semiExitU) || 5, + }); + if (est.ok && est.optionQty != null && est.perpQty != null) { + optQty = est.optionQty; + perpQty = est.perpQty; + } + } else { + const prev = plan.risk_sizing_preview; + if (prev?.ok && prev.option_qty_eth != null) { + optQty = Number(prev.option_qty_eth); + perpQty = Number(prev.perp_qty_eth ?? perpQty); + } + } } - const n = Number(idx); - const tgt = - semiView === "long" - ? n + Number(semiMove) - : n - Number(semiMove); - return `${fmtExPx("index", n)}→${fmtExPx("index", tgt)} · 净利≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U`; + + const reverseTgt = Number( + plan.semi_net_exit_target ?? + (riskBased + ? Number(semiExitU) * + Number( + plan.risk_last_k ?? + plan.risk_sizing_preview?.k ?? + 1, + ) + : semiExitU), + ); + + if (idx == null || !Number.isFinite(idx)) { + return `逆向兑现≥${fmt(reverseTgt, 2)}U`; + } + if (strike == null || !Number.isFinite(strike)) { + const tgt = + semiView === "long" + ? idx + Number(semiMove) + : idx - Number(semiMove); + return `${fmtExPx("index", idx)}→${fmtExPx("index", tgt)} · 待行权价估盈利 · 逆向≥${fmt(reverseTgt, 2)}U`; + } + + const prof = estimateSemiMoveProfit({ + view: semiView, + indexPx: idx, + strike, + movePoints: Number(semiMove) || 50, + optionQty: optQty, + perpQty: perpQty, + feeRate: 0, // 预估展示用内在+永续点位;手续费另计 + }); + if (!prof.ok) { + return prof.detail; + } + return ( + <> + {fmtExPx("index", idx)}→{fmtExPx("index", prof.targetPx!)} · + K{Math.round(strike)} · 顺向预估≈{fmt(prof.netEst, 2)}U + + {" "} + (期权{fmt(prof.optionPnl, 1)}/永续 + {fmt(prof.perpPnl, 1)} · 内在粗估) + + {" · "} + 逆向≥{fmt(reverseTgt, 2)}U + + ); })()}
    @@ -1080,7 +1243,7 @@ export default function PlanPage() { ? ` · K${Math.round(pick.strike)}` : ""} {est.netTarget != null - ? ` · 净利目标≈${fmt(est.netTarget, 2)}U` + ? ` · 逆向目标≈${fmt(est.netTarget, 2)}U` : ""} );