Show OKX-style perp/option position cards on plan page.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -389,6 +389,7 @@ class Matcher:
|
||||
}
|
||||
gw = get_gateway()
|
||||
snap = gw.snapshot()
|
||||
s = get_settings()
|
||||
index_px = snap.index_px
|
||||
if index_px is None and snap.perp:
|
||||
index_px = snap.perp.mark_px
|
||||
@@ -425,11 +426,37 @@ class Matcher:
|
||||
move = abs(float(index_px) - entry_idx) if index_px is not None and entry_idx else 0.0
|
||||
initial_premium = float(pos["initial_premium"] or 0)
|
||||
premium_gap = initial_premium - perp_upl
|
||||
|
||||
group_id = pos.get("group_id")
|
||||
g = (
|
||||
self.db.fetchone("SELECT * FROM groups WHERE group_id=?", (group_id,))
|
||||
if group_id
|
||||
else None
|
||||
)
|
||||
strike = float(g["strike"]) if g and g["strike"] is not None else None
|
||||
expiry_ymd = str(g["expiry_ymd"]) if g and g["expiry_ymd"] else None
|
||||
perp_inst_id = (
|
||||
str(g["perp_inst_id"])
|
||||
if g and g["perp_inst_id"]
|
||||
else s.perp_inst_id
|
||||
)
|
||||
|
||||
return {
|
||||
"has_position": True,
|
||||
"group_id": pos["group_id"],
|
||||
"group_id": group_id,
|
||||
"perp_side": perp_side,
|
||||
"option_side": option_side,
|
||||
"perp_inst_id": perp_inst_id,
|
||||
"perp_entry_px": perp_entry,
|
||||
"perp_qty_eth": perp_qty,
|
||||
"perp_mark_px": float(mark) if mark is not None else None,
|
||||
"option_inst_id": pos.get("option_inst_id"),
|
||||
"option_entry_px": float(pos["option_entry_px"] or 0),
|
||||
"option_qty_eth": float(pos["option_qty_eth"] or 0),
|
||||
"option_qty_contracts": float(pos["option_qty_contracts"] or 0),
|
||||
"option_mark_px": float(opt_mark) if opt_mark is not None else None,
|
||||
"strike": strike,
|
||||
"expiry_ymd": expiry_ymd,
|
||||
"perp_upl": perp_upl,
|
||||
"option_upl": option_upl,
|
||||
"index_px": index_px,
|
||||
|
||||
@@ -129,6 +129,17 @@ export type PlanState = {
|
||||
group_id?: string;
|
||||
perp_side?: string;
|
||||
option_side?: string;
|
||||
perp_inst_id?: string;
|
||||
perp_entry_px?: number;
|
||||
perp_qty_eth?: number;
|
||||
perp_mark_px?: number | null;
|
||||
option_inst_id?: string;
|
||||
option_entry_px?: number;
|
||||
option_qty_eth?: number;
|
||||
option_qty_contracts?: number;
|
||||
option_mark_px?: number | null;
|
||||
strike?: number | null;
|
||||
expiry_ymd?: string | null;
|
||||
perp_upl?: number;
|
||||
option_upl?: number;
|
||||
index_px?: number | null;
|
||||
|
||||
+135
-28
@@ -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">
|
||||
|
||||
@@ -290,3 +290,138 @@ input {
|
||||
color: var(--down);
|
||||
border-color: rgba(246, 70, 93, 0.35);
|
||||
}
|
||||
|
||||
/* OKX-style position cards (copied/adapted from crypto_monitor instance_page) */
|
||||
.pos-embed-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.pos-embed-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-card {
|
||||
background: #141923;
|
||||
border: 1px solid #2a3348;
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.pos-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pos-card-symbol {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pos-card-symbol strong {
|
||||
font-size: 0.95rem;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.pos-side-badge {
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.pos-side-long {
|
||||
background: #253a6e;
|
||||
color: #6eb5ff;
|
||||
}
|
||||
|
||||
.pos-side-short {
|
||||
background: #4a2230;
|
||||
color: #ff8a8a;
|
||||
}
|
||||
|
||||
.pos-meta {
|
||||
font-size: 0.74rem;
|
||||
color: #8b95a8;
|
||||
line-height: 1.45;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px 0;
|
||||
}
|
||||
|
||||
.pos-meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pos-meta-item:not(:last-child)::after {
|
||||
content: "|";
|
||||
margin: 0 8px;
|
||||
color: #3d4659;
|
||||
}
|
||||
|
||||
.pos-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.pos-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.pos-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pos-label {
|
||||
font-size: 0.72rem;
|
||||
color: #7d8799;
|
||||
}
|
||||
|
||||
.pos-value {
|
||||
font-size: 0.88rem;
|
||||
color: #e8ecf4;
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.pos-empty {
|
||||
padding: 18px;
|
||||
text-align: center;
|
||||
color: #8892b0;
|
||||
font-size: 0.85rem;
|
||||
background: #141923;
|
||||
border: 1px dashed #2a3348;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.pos-pnl-profit {
|
||||
color: #7ee787 !important;
|
||||
}
|
||||
|
||||
.pos-pnl-loss {
|
||||
color: #ff8b8b !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user