Split semi summary into option target, perp lock, and sizing lines.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-08 15:12:56 +08:00
parent 43e2df2b62
commit 723c5fc1fc
2 changed files with 86 additions and 145 deletions
+76 -139
View File
@@ -1079,7 +1079,7 @@ export default function PlanPage() {
/>
</div>
</div>
<div className="mono meta plan-semi-summary">
<div className="mono plan-semi-lines">
{(() => {
const shown = filterSemiLadderRows(
ladder?.rows || [],
@@ -1095,34 +1095,51 @@ export default function PlanPage() {
? Number(snap.index_px)
: null;
const strike =
pick?.strike != null
? Number(pick.strike)
: ladder?.atm_strike != null
? Number(ladder.atm_strike)
: null;
open && pos?.strike != null && Number.isFinite(Number(pos.strike))
? Number(pos.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;
const moveN = Number(semiMove) || 50;
let optQty = Number(semiOptU) || 4;
let perpQty = Number(semiPerpU) || 1;
if (riskLocked) {
let kShow: number | null = riskLocked
? plan.risk_last_k != null
? Number(plan.risk_last_k)
: null
: null;
let budgetShow: number | null = null;
let sizingLine = "";
if (!riskBased) {
sizingLine = `以损定仓 · 未开启(手动 期权 ${fmt(semiOptU, 2)} / 永续 ${fmt(semiPerpU, 2)} ETH`;
} else 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) {
sizingLine = `以损定仓(锁) · 期权 ${fmt(optQty, 2)} ETH / 永续 ${fmt(perpQty, 2)} ETH${
kShow != null ? ` · k=${fmt(kShow, 1)}` : ""
}`;
} else {
const budget = Number(plan.risk_sizing_preview?.budget);
budgetShow = Number.isFinite(budget) ? budget : null;
if (
ask != null &&
idx != null &&
Number.isFinite(budget) &&
budget > 0
budgetShow != null &&
budgetShow > 0
) {
const est = estimateSemiRiskQty({
budget,
budget: budgetShow,
indexPx: idx,
optionAsk: ask,
feeRate: feeOk,
@@ -1133,12 +1150,33 @@ export default function PlanPage() {
if (est.ok && est.optionQty != null && est.perpQty != null) {
optQty = est.optionQty;
perpQty = est.perpQty;
kShow = est.k ?? null;
sizingLine = `以损定仓 · 期权 ${fmt(optQty, 2)} ETH / 永续 ${fmt(perpQty, 2)} ETH · k=${fmt(kShow, 1)} · 预算${fmt(budgetShow, 2)}U · 卖一${fmtExPx("option", ask)}${
pick != null ? ` · K${Math.round(pick.strike)}` : ""
}`;
} else {
sizingLine = `以损定仓 · ${est.detail}`;
}
} 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);
kShow =
prev.k != null ? Number(prev.k) : kShow;
sizingLine = `以损定仓 · 期权 ${fmt(optQty, 2)} ETH / 永续 ${fmt(perpQty, 2)} ETH${
kShow != null ? ` · k=${fmt(kShow, 1)}` : ""
}${
prev.budget != null
? ` · 预算${fmt(Number(prev.budget), 2)}U`
: ""
} · 待本单卖一`;
} else {
sizingLine = `以损定仓 · ${
prev?.ok === false
? String(prev.detail || "预览失败")
: "待卖一/预算推算开仓量"
}`;
}
}
}
@@ -1148,146 +1186,45 @@ export default function PlanPage() {
(riskBased
? Number(semiExitU) *
Number(
plan.risk_last_k ??
kShow ??
plan.risk_last_k ??
plan.risk_sizing_preview?.k ??
1,
)
: semiExitU),
);
let line1 = "";
if (strike == null || !Number.isFinite(strike)) {
return `期权目标 行权价${semiView === "short" ? "" : "+"}${fmt(Number(semiMove) || 50, 0)} · 永续锁定≥${fmt(reverseTgt, 2)}U`;
}
if (idx == null || !Number.isFinite(idx)) {
line1 = `期权目标 · 行权价${semiView === "short" ? "" : "+"}${fmt(moveN, 0)}(待选约)`;
} else if (idx == null || !Number.isFinite(idx)) {
const tgt =
semiView === "long"
? strike + Number(semiMove)
: strike - Number(semiMove);
return `期权目标 K${Math.round(strike)}${fmtExPx("index", tgt)} · 永续锁定≥${fmt(reverseTgt, 2)}U`;
semiView === "long" ? strike + moveN : strike - moveN;
line1 = `期权目标 · K${Math.round(strike)}${fmtExPx("index", tgt)}`;
} else {
const prof = estimateSemiMoveProfit({
view: semiView,
indexPx: idx,
strike,
movePoints: moveN,
optionQty: optQty,
perpQty: perpQty,
feeRate: 0,
});
if (!prof.ok) {
line1 = `期权目标 · ${prof.detail}`;
} else {
line1 = `期权目标 · K${Math.round(strike)}${fmtExPx("index", prof.targetPx!)} · 现价${fmtExPx("index", idx)} · 到点预估≈${fmt(prof.netEst, 2)}U(期权${fmt(prof.optionPnl, 1)}/永续${fmt(prof.perpPnl, 1)}`;
}
}
const prof = estimateSemiMoveProfit({
view: semiView,
indexPx: idx,
strike,
movePoints: Number(semiMove) || 50,
optionQty: optQty,
perpQty: perpQty,
feeRate: 0,
});
if (!prof.ok) {
return prof.detail;
}
const line2 = `永续目标 · 净利锁定≥${fmt(reverseTgt, 2)}U(基数${fmt(semiExitU, 1)}×k`;
return (
<>
K{Math.round(strike)}
{fmtExPx("index", prof.targetPx!)} ·
{fmtExPx("index", idx)} · {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">
{(() => {
const shown = filterSemiLadderRows(
ladder?.rows || [],
semiMny,
Number(semiOtmOff) || 25,
semiView,
);
const pick = pickSizingAskRow(shown);
const ask = pick?.ask != null ? Number(pick.ask) : null;
const idx =
ladder?.index_px != null
? Number(ladder.index_px)
: snap?.index_px != null
? Number(snap.index_px)
: null;
if (!riskBased) {
return (
<>
· {fmt(semiOptU, 2)} ETH / {" "}
{fmt(semiPerpU, 2)} ETH
<span className="meta">
{" "}
</span>
</>
);
}
if (riskLocked) {
return (
<>
() · {" "}
{fmt(plan.option_qty_eth ?? pos?.option_qty_eth, 2)} ETH /
{fmt(plan.perp_qty_eth ?? pos?.perp_qty_eth, 2)} ETH
{plan.risk_last_k != null
? ` · k=${fmt(plan.risk_last_k, 1)}`
: ""}
</>
);
}
const budget = Number(plan.risk_sizing_preview?.budget);
const fee = Number(plan.fee_rate ?? 0.0005);
if (
ask == null ||
idx == null ||
!Number.isFinite(budget) ||
budget <= 0
) {
const prev = plan.risk_sizing_preview;
if (prev?.ok && prev.option_qty_eth != null) {
return (
<>
· {fmt(Number(prev.option_qty_eth), 2)} ETH
/ {fmt(Number(prev.perp_qty_eth), 2)} ETH
{prev.k != null ? ` · k=${fmt(Number(prev.k), 1)}` : ""}
{prev.budget != null
? ` · 预算${fmt(Number(prev.budget), 2)}U`
: ""}
<span className="meta"> · </span>
</>
);
}
return (
<>
·{" "}
{prev?.ok === false
? String(prev.detail || "预览失败")
: "待卖一/预算推算开仓量"}
</>
);
}
const est = estimateSemiRiskQty({
budget,
indexPx: idx,
optionAsk: ask,
feeRate: Number.isFinite(fee) && fee >= 0 ? fee : 0.0005,
perpUnit: Number(semiPerpU) || 1,
optionUnit: Number(semiOptU) || 4,
exitUnit: Number(semiExitU) || 5,
});
if (!est.ok) {
return <> · {est.detail}</>;
}
return (
<>
· {fmt(est.optionQty, 2)} ETH / {" "}
{fmt(est.perpQty, 2)} ETH · k={fmt(est.k, 1)} ·
{fmt(budget, 2)}U · {fmtExPx("option", ask)}
{pick != null
? ` · K${Math.round(pick.strike)}`
: ""}
{est.netTarget != null
? ` · 逆向目标≈${fmt(est.netTarget, 2)}U`
: ""}
<div className="plan-semi-line">{line1}</div>
<div className="plan-semi-line">{line2}</div>
<div className="plan-semi-line">{sizingLine}</div>
</>
);
})()}
+10 -6
View File
@@ -572,15 +572,19 @@ input {
min-height: 32px;
}
.plan-semi-summary {
margin-top: 6px !important;
.plan-semi-lines {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 3px;
font-size: 12px;
line-height: 1.4;
opacity: 0.95;
}
.plan-semi-sizing {
margin-top: 2px !important;
font-size: 12px;
opacity: 0.95;
.plan-semi-line {
white-space: normal;
word-break: break-word;
}
.plan-semi-btns {