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 <cursoragent@cursor.com>
This commit is contained in:
+2
-1
@@ -64,7 +64,8 @@
|
||||
- 流动性不足:进入 `liquidity_wait`;回落未达标则取消挂起。
|
||||
- 半自动平仓**禁止**远虚「只平永续、期权归档」;顺序仍为**先期权后永续**。
|
||||
|
||||
首页预览「指数 → 到点」用**当前指数**示意;真实触发达标用**开仓指数**。
|
||||
首页预览「指数 → 到点」用**当前指数**示意;真实触发达标用**开仓指数**。
|
||||
「波动点」旁的**顺向预估**按选定行权价的内在价值变化 + 永续点位粗算(不含 IV/时间价值);与「净利基数×k」逆向兑现目标分开显示。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,6 +5,15 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-08 — 半自动波动点顺向预估盈利
|
||||
|
||||
### 变更
|
||||
|
||||
1. 「波动点」下方按标的顺向 N 点 + 行权价内在价值变化粗估组合盈利(期权腿/永续腿分列)。
|
||||
2. 与「净利基数×k」逆向兑现目标分开显示,不再把基数目标误标为顺向净利。
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-08 — 半自动以损定仓预览开仓量
|
||||
|
||||
### 变更
|
||||
|
||||
+175
-12
@@ -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<MarketSnapshot | null>(null);
|
||||
const [plan, setPlan] = useState<PlanState | null>(null);
|
||||
@@ -817,7 +879,8 @@ export default function PlanPage() {
|
||||
<li>人工定方向、行权类型、永续:期权配比与出场 →「授权开下一单」后机器盯选约/开/平。</li>
|
||||
<li>平仓后停在等待授权,不自动连开;与全自动循环无关。</li>
|
||||
<li>虚值:偏离 ≤ 设定点数;杠杆门 ≥180(默认 200)。实值/平值用本单最低杠杆。</li>
|
||||
<li>顺向:开仓指数 ± 波动点数,且组合净利 > 0 → 双腿全平(先期权后永续)。</li>
|
||||
<li>波动点:标的相对当前价顺向波动 N 点(多涨/空跌);下方按行权价内在价值变化 + 永续点位粗估盈利。</li>
|
||||
<li>顺向出场:开仓指数 ± 波动点,且组合净利 > 0 → 双腿全平(先期权后永续)。</li>
|
||||
<li>逆向兑现:组合净利 ≥ 净利基数 × k → 全平。</li>
|
||||
<li>以损定仓时配比为单位再乘 k;手动仓按配比直接开。</li>
|
||||
</ul>
|
||||
@@ -910,7 +973,7 @@ export default function PlanPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="semiMove">波动点</label>
|
||||
<label htmlFor="semiMove">波动点(预估)</label>
|
||||
<input
|
||||
id="semiMove"
|
||||
className="mono"
|
||||
@@ -976,16 +1039,116 @@ export default function PlanPage() {
|
||||
</div>
|
||||
<div className="mono meta plan-semi-summary">
|
||||
{(() => {
|
||||
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
|
||||
<span className="meta">
|
||||
{" "}
|
||||
(期权{fmt(prof.optionPnl, 1)}/永续
|
||||
{fmt(prof.perpPnl, 1)} · 内在粗估)
|
||||
</span>
|
||||
{" · "}
|
||||
逆向≥{fmt(reverseTgt, 2)}U
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="mono meta plan-semi-sizing">
|
||||
@@ -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`
|
||||
: ""}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user