diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index 729046b..52ddcdd 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -1079,7 +1079,7 @@ export default function PlanPage() { /> -
+
{(() => { 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 - - {" "} - (期权{fmt(prof.optionPnl, 1)}/永续 - {fmt(prof.perpPnl, 1)}) - - {" · "} - 永续锁定≥{fmt(reverseTgt, 2)}U - - ); - })()} -
-
- {(() => { - 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 - - {" "} - (系统设置可开以损定仓) - - - ); - } - 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` - : ""} - · 待本单卖一 - - ); - } - 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` - : ""} +
{line1}
+
{line2}
+
{sizingLine}
); })()} diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index ee5784a..2ae5190 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -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 {