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()
|
gw = get_gateway()
|
||||||
snap = gw.snapshot()
|
snap = gw.snapshot()
|
||||||
|
s = get_settings()
|
||||||
index_px = snap.index_px
|
index_px = snap.index_px
|
||||||
if index_px is None and snap.perp:
|
if index_px is None and snap.perp:
|
||||||
index_px = snap.perp.mark_px
|
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
|
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)
|
initial_premium = float(pos["initial_premium"] or 0)
|
||||||
premium_gap = initial_premium - perp_upl
|
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 {
|
return {
|
||||||
"has_position": True,
|
"has_position": True,
|
||||||
"group_id": pos["group_id"],
|
"group_id": group_id,
|
||||||
"perp_side": perp_side,
|
"perp_side": perp_side,
|
||||||
"option_side": option_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,
|
"perp_upl": perp_upl,
|
||||||
"option_upl": option_upl,
|
"option_upl": option_upl,
|
||||||
"index_px": index_px,
|
"index_px": index_px,
|
||||||
|
|||||||
@@ -129,6 +129,17 @@ export type PlanState = {
|
|||||||
group_id?: string;
|
group_id?: string;
|
||||||
perp_side?: string;
|
perp_side?: string;
|
||||||
option_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;
|
perp_upl?: number;
|
||||||
option_upl?: number;
|
option_upl?: number;
|
||||||
index_px?: number | null;
|
index_px?: number | null;
|
||||||
|
|||||||
+135
-28
@@ -6,6 +6,24 @@ function fmt(n: number | null | undefined, d = 2) {
|
|||||||
return n.toFixed(d);
|
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() {
|
export default function PlanPage() {
|
||||||
const [snap, setSnap] = useState<MarketSnapshot | null>(null);
|
const [snap, setSnap] = useState<MarketSnapshot | null>(null);
|
||||||
const [plan, setPlan] = useState<PlanState | null>(null);
|
const [plan, setPlan] = useState<PlanState | null>(null);
|
||||||
@@ -56,22 +74,10 @@ export default function PlanPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const pos = plan?.position;
|
const pos = plan?.position;
|
||||||
|
const open = !!pos?.has_position;
|
||||||
const exitN = plan?.exit_move_points ?? 30;
|
const exitN = plan?.exit_move_points ?? 30;
|
||||||
const move = pos?.move_points ?? 0;
|
const move = pos?.move_points ?? 0;
|
||||||
|
const phaseLabel = PHASE_ZH[plan?.phase || ""] || plan?.phase || "—";
|
||||||
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 || "—";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -125,7 +131,7 @@ export default function PlanPage() {
|
|||||||
{busy ? <span className="meta">{busy}…</span> : null}
|
{busy ? <span className="meta">{busy}…</span> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card" style={{ marginBottom: 12 }}>
|
<div className="card plan-summary" style={{ marginBottom: 12 }}>
|
||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>策略</span>
|
<span>策略</span>
|
||||||
<span>
|
<span>
|
||||||
@@ -154,29 +160,24 @@ export default function PlanPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>权益</span>
|
<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>
|
||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>当前组</span>
|
<span>当前组</span>
|
||||||
<span className="mono">{pos?.group_id || "—"}</span>
|
<span className="mono">{pos?.group_id || "—"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>方向</span>
|
<span>信号方向</span>
|
||||||
<span className="mono">
|
<span className="mono">{biasTag}</span>
|
||||||
{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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>永续浮盈 / 距覆盖</span>
|
<span>永续浮盈 / 距覆盖</span>
|
||||||
<span className="mono">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="kv">
|
<div className="kv">
|
||||||
@@ -193,6 +194,112 @@ export default function PlanPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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>
|
||||||
|
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
|
|||||||
@@ -290,3 +290,138 @@ input {
|
|||||||
color: var(--down);
|
color: var(--down);
|
||||||
border-color: rgba(246, 70, 93, 0.35);
|
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