Add strategy residual table with manual close at panel bottom.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-03 07:47:22 +08:00
parent 09abb8d35b
commit 1252f45aac
5 changed files with 191 additions and 62 deletions
+104 -21
View File
@@ -94,11 +94,22 @@ const PHASE_ZH: Record<string, string> = {
wait_funds: "资金不足",
};
function fmtBidLiquidity(
bidPx: number | null | undefined,
bidSzEth: number | null | undefined,
): string {
if (bidPx == null && bidSzEth == null) return "—";
const px = bidPx == null ? "—" : fmtExPx("option", bidPx);
const sz = bidSzEth == null ? "—" : fmt(bidSzEth, 2);
return `${px} / ${sz}`;
}
export default function PlanPage() {
const [snap, setSnap] = useState<MarketSnapshot | null>(null);
const [plan, setPlan] = useState<PlanState | null>(null);
const [err, setErr] = useState("");
const [busy, setBusy] = useState("");
const [residualBusy, setResidualBusy] = useState("");
const [nowMs, setNowMs] = useState(() => Date.now());
async function refresh() {
@@ -139,6 +150,26 @@ export default function PlanPage() {
}
}
async function closeResidual(groupId: string, optionInstId: string) {
const ok = window.confirm(
`确认平掉残留期权?\n\n组:${groupId}\n合约:${optionInstId}\n\n仅校验买一流动性,不要求权利金回收比例。`,
);
if (!ok) return;
setResidualBusy(groupId);
setErr("");
try {
await apiFetch("/api/plan/residual/close", {
method: "POST",
body: JSON.stringify({ group_id: groupId }),
});
await refresh();
} catch (e) {
setErr(e instanceof Error ? e.message : String(e));
} finally {
setResidualBusy("");
}
}
const bias = snap?.ask_compare?.bias;
const biasTag =
bias === "strike_below_spot" ||
@@ -485,27 +516,6 @@ export default function PlanPage() {
</span>
</span>
</div>
<div className="kv">
<span></span>
<span className="mono">{pos?.group_id || "—"}</span>
</div>
<div className="kv kv-residuals">
<span>()</span>
{plan?.residuals && plan.residuals.length > 0 ? (
<div className="residual-list mono">
{plan.residuals.map((r) => (
<div
key={`${r.group_id}:${r.option_inst_id || ""}`}
className="residual-item"
>
{r.group_id}:{r.option_inst_id || "?"}@{r.expiry_ymd || "?"}
</div>
))}
</div>
) : (
<span className="mono"></span>
)}
</div>
<div className="kv">
<span></span>
<span className="mono">{biasTag}</span>
@@ -559,6 +569,79 @@ export default function PlanPage() {
</div>
) : null}
</div>
{plan?.residuals && plan.residuals.length > 0 ? (
<div className="plan-residual-block">
<div className="plan-residual-head">
<h4></h4>
<span className="meta">
</span>
</div>
<div className="plan-residual-wrap">
<table className="plan-residual-table">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th title="最新买一价格 / 买一数量(ETH)"></th>
<th title="买一价 × 持仓数量"></th>
<th title="买一权利金 ÷ 开仓权利金"></th>
<th></th>
</tr>
</thead>
<tbody>
{plan.residuals.map((r) => {
const gid = String(r.group_id || "");
const inst = String(r.option_inst_id || "—");
const liqOk = r.liquidity_ok === true;
return (
<tr key={`${gid}:${inst}`}>
<td className="mono">{gid || "—"}</td>
<td className="mono">{inst}</td>
<td className="mono">{fmt(r.option_qty_eth, 4)}</td>
<td className="mono">{fmt(r.initial_premium, 2)}</td>
<td
className="mono"
title={
liqOk
? "买一深度可覆盖持仓"
: "买一不足或盘口不可用"
}
>
{fmtBidLiquidity(r.bid_px, r.bid_sz_eth)}
</td>
<td className="mono">{fmt(r.current_premium, 2)}</td>
<td className="mono">
{r.recovery_pct == null
? "—"
: `${fmt(r.recovery_pct, 1)}%`}
</td>
<td>
<button
type="button"
className="btn ghost"
style={{ padding: "4px 10px", fontSize: 12 }}
disabled={!!residualBusy || !!busy || !liqOk}
title={
liqOk
? "按最新买一 IOC 平仓(不验权利金比例)"
: "买一流动性不足或盘口不可用"
}
onClick={() => void closeResidual(gid, inst)}
>
{residualBusy === gid ? "平仓中…" : "平仓"}
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
) : null}
</div>
<div className="plan-positions">