Show OKX-style perp/option position cards on plan page.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-24 17:32:57 +08:00
parent 5125c6702b
commit 1155cf082d
4 changed files with 309 additions and 29 deletions
+135 -28
View File
@@ -6,6 +6,24 @@ function fmt(n: number | null | undefined, d = 2) {
return n.toFixed(d);
}
function pnlClass(n: number | null | undefined) {
if (n == null || Number.isNaN(n) || n === 0) return "";
return n > 0 ? "pos-pnl-profit" : "pos-pnl-loss";
}
const PHASE_ZH: Record<string, string> = {
idle: "空闲",
wait_signal: "等待信号",
opening: "开仓中",
open: "持仓中",
closing: "平仓中",
resting: "休息中",
paused: "已暂停",
stopped: "已停开",
outside_window: "窗外",
liquidity_wait: "流动性等待",
};
export default function PlanPage() {
const [snap, setSnap] = useState<MarketSnapshot | null>(null);
const [plan, setPlan] = useState<PlanState | null>(null);
@@ -56,22 +74,10 @@ export default function PlanPage() {
);
const pos = plan?.position;
const open = !!pos?.has_position;
const exitN = plan?.exit_move_points ?? 30;
const move = pos?.move_points ?? 0;
const phaseZh: Record<string, string> = {
idle: "空闲",
wait_signal: "等待信号",
opening: "开仓中",
open: "持仓中",
closing: "平仓中",
resting: "休息中",
paused: "已暂停",
stopped: "已停开",
outside_window: "窗外",
liquidity_wait: "流动性等待",
};
const phaseLabel = phaseZh[plan?.phase || ""] || plan?.phase || "—";
const phaseLabel = PHASE_ZH[plan?.phase || ""] || plan?.phase || "—";
return (
<div>
@@ -125,7 +131,7 @@ export default function PlanPage() {
{busy ? <span className="meta">{busy}</span> : null}
</div>
<div className="card" style={{ marginBottom: 12 }}>
<div className="card plan-summary" style={{ marginBottom: 12 }}>
<div className="kv">
<span></span>
<span>
@@ -154,29 +160,24 @@ export default function PlanPage() {
</div>
<div className="kv">
<span></span>
<span className="mono">{fmt(plan?.ledger?.equity)} / {fmt(plan?.ledger?.available)}</span>
<span className="mono">
{fmt(plan?.ledger?.equity)} / {fmt(plan?.ledger?.available)}
</span>
</div>
<div className="kv">
<span></span>
<span className="mono">{pos?.group_id || "—"}</span>
</div>
<div className="kv">
<span></span>
<span className="mono">
{pos?.has_position
? `永续${pos.perp_side === "long" ? "多" : "空"} + 买${pos.option_side === "call" ? "Call" : "Put"}`
: "—"}{" "}
{biasTag}
</span>
</div>
<div className="kv">
<span></span>
<span className="mono">{fmt(pos?.initial_premium)}</span>
<span></span>
<span className="mono">{biasTag}</span>
</div>
<div className="kv">
<span> / </span>
<span className="mono">
{fmt(pos?.perp_upl)} / {fmt(pos?.premium_gap)}
<span className={pnlClass(pos?.perp_upl)}>{fmt(pos?.perp_upl)}</span>
{" / "}
{fmt(pos?.premium_gap)}
</span>
</div>
<div className="kv">
@@ -193,6 +194,112 @@ export default function PlanPage() {
</span>
</div>
) : null}
<div className="pos-embed-grid">
{open ? (
<>
<div className="pos-card">
<div className="pos-card-head">
<div className="pos-card-symbol">
<strong>{pos?.perp_inst_id || "ETH-USDT-SWAP"}</strong>
<span
className={
pos?.perp_side === "long"
? "pos-side-badge pos-side-long"
: "pos-side-badge pos-side-short"
}
>
{pos?.perp_side === "long" ? "做多" : "做空"}
</span>
</div>
</div>
<div className="pos-meta">
<span className="pos-meta-item"></span>
<span className="pos-meta-item"> {pos?.group_id}</span>
<span className="pos-meta-item mono"> {fmt(pos?.perp_qty_eth, 2)} ETH</span>
</div>
<div className="pos-grid">
<div className="pos-cell">
<span className="pos-label"></span>
<span className="pos-value mono">{fmt(pos?.perp_entry_px)}</span>
</div>
<div className="pos-cell">
<span className="pos-label"></span>
<span className="pos-value mono">{fmt(pos?.perp_mark_px)}</span>
</div>
<div className="pos-cell">
<span className="pos-label"></span>
<span className={`pos-value mono ${pnlClass(pos?.perp_upl)}`}>
{fmt(pos?.perp_upl)}
</span>
</div>
<div className="pos-cell">
<span className="pos-label"></span>
<span className="pos-value mono">{fmt(pos?.index_px)}</span>
</div>
<div className="pos-cell">
<span className="pos-label"></span>
<span className="pos-value mono">{fmt(pos?.entry_index_px)}</span>
</div>
<div className="pos-cell">
<span className="pos-label"></span>
<span className="pos-value mono">{fmt(move, 1)}</span>
</div>
</div>
</div>
<div className="pos-card">
<div className="pos-card-head">
<div className="pos-card-symbol">
<strong>{pos?.option_inst_id || "期权"}</strong>
<span
className={
pos?.option_side === "call"
? "pos-side-badge pos-side-long"
: "pos-side-badge pos-side-short"
}
>
{pos?.option_side === "call" ? "看涨 Call" : "看跌 Put"}
</span>
</div>
</div>
<div className="pos-meta">
<span className="pos-meta-item"></span>
<span className="pos-meta-item"> {pos?.expiry_ymd || "—"}</span>
<span className="pos-meta-item"> {fmt(pos?.strike, 0)}</span>
<span className="pos-meta-item mono">
{fmt(pos?.option_qty_eth, 2)} ETH · {fmt(pos?.option_qty_contracts, 0)}
</span>
</div>
<div className="pos-grid">
<div className="pos-cell">
<span className="pos-label"></span>
<span className="pos-value mono">{fmt(pos?.option_entry_px)}</span>
</div>
<div className="pos-cell">
<span className="pos-label">/</span>
<span className="pos-value mono">{fmt(pos?.option_mark_px)}</span>
</div>
<div className="pos-cell">
<span className="pos-label"></span>
<span className={`pos-value mono ${pnlClass(pos?.option_upl)}`}>
{fmt(pos?.option_upl)}
</span>
</div>
<div className="pos-cell">
<span className="pos-label"></span>
<span className="pos-value mono">{fmt(pos?.initial_premium)}</span>
</div>
</div>
</div>
</>
) : (
<>
<div className="pos-empty"> · </div>
<div className="pos-empty"> · </div>
</>
)}
</div>
</div>
<div className="grid-2">