Align option display precision with OKX and use exchange position history.
Format prices by tickSz, move bid depth/recovery to card end with plain styling, and load option history from OKX positions-history instead of local DB. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3600,62 +3600,15 @@ html[data-theme="light"] .options-estimate-row {
|
||||
.options-page-wrap .opt-pos-cell--depth {
|
||||
grid-column: span 2;
|
||||
}
|
||||
.options-page-wrap .opt-bid-depth {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
.options-page-wrap .opt-bid-level {
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto auto;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-height: 24px;
|
||||
padding: 3px 9px;
|
||||
border: 1px solid rgba(82, 101, 143, 0.55);
|
||||
border-radius: 999px;
|
||||
background: rgba(18, 24, 37, 0.72);
|
||||
.options-page-wrap .opt-bid-plain {
|
||||
color: #dbe6ff;
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.35;
|
||||
white-space: normal;
|
||||
}
|
||||
.options-page-wrap .opt-bid-level--best {
|
||||
border-color: rgba(93, 143, 255, 0.9);
|
||||
background: linear-gradient(135deg, rgba(47, 86, 170, 0.72), rgba(28, 39, 68, 0.84));
|
||||
box-shadow: inset 0 0 0 1px rgba(122, 164, 255, 0.1), 0 6px 14px rgba(24, 46, 92, 0.24);
|
||||
}
|
||||
.options-page-wrap .opt-bid-rank {
|
||||
color: #91a4cc;
|
||||
font-size: 0.64rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.options-page-wrap .opt-bid-level--best .opt-bid-rank {
|
||||
color: #c8d8ff;
|
||||
}
|
||||
.options-page-wrap .opt-bid-price {
|
||||
color: #ffffff;
|
||||
.options-page-wrap .opt-close-value {
|
||||
font-weight: 700;
|
||||
}
|
||||
.options-page-wrap .opt-close-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.options-page-wrap .opt-close-main {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
color: #f2f6ff;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.05;
|
||||
}
|
||||
.options-page-wrap .opt-close-main span {
|
||||
color: #9aa8c2;
|
||||
font-size: 0.62rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.options-page-wrap .opt-close-rule {
|
||||
margin-top: 8px;
|
||||
@@ -3774,11 +3727,6 @@ html[data-theme="light"] .options-estimate-row {
|
||||
.opt-history-table td:nth-child(6) {
|
||||
width: 20%;
|
||||
}
|
||||
.opt-history-table th:nth-child(7),
|
||||
.opt-history-table td:nth-child(7) {
|
||||
width: 8%;
|
||||
text-align: center;
|
||||
}
|
||||
.opt-hist-time {
|
||||
font-size: 0.64rem;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -30,6 +30,24 @@
|
||||
return Number(v).toFixed(d == null ? 2 : d);
|
||||
}
|
||||
|
||||
function fmtDisplay(v, fallback) {
|
||||
if (v !== null && v !== undefined && String(v).trim() !== "") return String(v);
|
||||
if (fallback !== undefined) return fmtDisplay(fallback);
|
||||
return "—";
|
||||
}
|
||||
|
||||
function fmtOptionPx(v, tickSz) {
|
||||
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
||||
const n = Number(v);
|
||||
const tick = Number(tickSz);
|
||||
if (!tickSz || Number.isNaN(tick) || tick <= 0) return String(n);
|
||||
let decimals = 0;
|
||||
if (tick < 1) decimals = Math.max(0, -Math.round(Math.log10(tick)));
|
||||
else if (String(tick).indexOf(".") >= 0) decimals = String(tick).split(".")[1].length;
|
||||
let s = n.toFixed(decimals).replace(/\.?0+$/, "");
|
||||
return s || "0";
|
||||
}
|
||||
|
||||
async function apiJson(url, opts) {
|
||||
const r = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {}));
|
||||
return r.json();
|
||||
@@ -185,28 +203,32 @@
|
||||
return price + "/" + size;
|
||||
}
|
||||
|
||||
function fmtCloseLevels(preview) {
|
||||
function fmtCloseLevels(preview, tickSz) {
|
||||
const levels = ((preview && preview.levels) || []).slice(0, 5);
|
||||
if (!levels.length) return "—";
|
||||
return '<div class="opt-bid-depth">' + levels.map(function (x, idx) {
|
||||
return levels.map(function (x, idx) {
|
||||
const levelNo = x.level != null ? x.level : idx + 1;
|
||||
const levelCls = idx === 0 ? " opt-bid-level--best" : "";
|
||||
return (
|
||||
'<span class="opt-bid-level' + levelCls + '">' +
|
||||
'<span class="opt-bid-rank">买' + levelNo + "</span>" +
|
||||
'<span class="opt-bid-price">' + fmt(x.px, 4) + "</span>" +
|
||||
"</span>"
|
||||
);
|
||||
}).join("") + "</div>";
|
||||
const pxTxt = fmtOptionPx(x.px, tickSz);
|
||||
return "买" + levelNo + " " + pxTxt;
|
||||
}).join(" · ");
|
||||
}
|
||||
|
||||
function fmtClosePreview(preview) {
|
||||
function fmtUsdc(v) {
|
||||
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
||||
return Number(v).toFixed(4).replace(/\.?0+$/, "") || "0";
|
||||
}
|
||||
|
||||
function fmtClosePreview(preview, premiumPaid) {
|
||||
if (!preview || preview.total_received == null) return "—";
|
||||
return (
|
||||
'<div class="opt-close-preview">' +
|
||||
'<div class="opt-close-main">' + fmt(preview.total_received, 4) + '<span>USDC</span></div>' +
|
||||
"</div>"
|
||||
);
|
||||
const recvTxt = fmtUsdc(preview.total_received);
|
||||
let cls = "";
|
||||
const prem = Number(premiumPaid);
|
||||
const recv = Number(preview.total_received);
|
||||
if (!Number.isNaN(prem) && !Number.isNaN(recv)) {
|
||||
if (recv > prem) cls = " pos-pnl-profit";
|
||||
else if (recv < prem) cls = " pos-pnl-loss";
|
||||
}
|
||||
return '<span class="opt-close-value' + cls + '">' + recvTxt + " USDC</span>";
|
||||
}
|
||||
|
||||
function fmtClosePreviewText(preview) {
|
||||
@@ -502,6 +524,10 @@
|
||||
const expAttr = expMs != null && expMs !== "" ? String(expMs) : "";
|
||||
const closePreview = p.close_preview || {};
|
||||
const closeSheets = p.avail_pos != null && Number(p.avail_pos) > 0 ? p.avail_pos : p.pos;
|
||||
const tickSz = p.tick_sz;
|
||||
const premTxt = fmtDisplay(p.premium_paid_fmt, p.premium_paid != null ? fmt(p.premium_paid, 4).replace(/\.?0+$/, "") : null);
|
||||
const avgTxt = fmtDisplay(p.avg_px_fmt, fmtOptionPx(p.avg_px, tickSz));
|
||||
const markTxt = fmtDisplay(p.mark_px_fmt, fmtOptionPx(p.mark_px, tickSz));
|
||||
return (
|
||||
'<div class="pos-card-head">' +
|
||||
'<div class="pos-card-symbol"><strong>' + (p.inst_id || "") + '</strong>' +
|
||||
@@ -517,17 +543,17 @@
|
||||
: "") +
|
||||
"</div>" +
|
||||
'<div class="pos-grid">' +
|
||||
'<div class="pos-cell"><span class="pos-label">权利金</span><span class="pos-value">' + fmt(p.premium_paid, 4) + " USDC</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">开仓均价</span><span class="pos-value">' + fmt(p.avg_px, 4) + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">' + fmt(p.mark_px, 4) + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">权利金</span><span class="pos-value">' + premTxt + " USDC</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">开仓均价</span><span class="pos-value">' + avgTxt + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">' + markTxt + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">指数价</span><span class="pos-value">' + fmt(p.idx_px, 0) + "</span></div>" +
|
||||
'<div class="pos-cell opt-pos-cell--depth"><span class="pos-label">买盘深度</span><span class="pos-value">' + fmtCloseLevels(closePreview) + "</span></div>" +
|
||||
'<div class="pos-cell opt-pos-cell--close"><span class="pos-label">按买盘收回</span><span class="pos-value">' + fmtClosePreview(closePreview) + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">到期平衡</span><span class="pos-value">' + fmt(p.expiry_be_px, 0) + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">平掉回本</span><span class="pos-value">' + fmt(p.close_be_px, 0) + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">浮盈亏</span><span class="pos-value ' + uplCls + '">' + fmt(p.upl, 2) + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
|
||||
(p.upl_ratio_pct != null ? fmt(p.upl_ratio_pct, 2) + "%" : "—") + "</span></div>" +
|
||||
'<div class="pos-cell opt-pos-cell--depth"><span class="pos-label">买盘深度</span><span class="pos-value opt-bid-plain">' + fmtCloseLevels(closePreview, tickSz) + "</span></div>" +
|
||||
'<div class="pos-cell opt-pos-cell--close"><span class="pos-label">按买盘回收</span><span class="pos-value">' + fmtClosePreview(closePreview, p.premium_paid) + "</span></div>" +
|
||||
"</div>"
|
||||
);
|
||||
}
|
||||
@@ -873,25 +899,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHistoryRow(id, status) {
|
||||
const warn = status === "open"
|
||||
? "该记录仍为持仓中,仅删除本地记录,不影响交易所持仓.确认删除?"
|
||||
: "确认删除该条期权历史记录?";
|
||||
if (!confirm(warn)) return;
|
||||
const r = await apiJson("/api/options/history/" + encodeURIComponent(id), { method: "DELETE" });
|
||||
if (!r.ok) {
|
||||
alert(r.msg || "删除失败");
|
||||
return;
|
||||
}
|
||||
refreshAllPositions();
|
||||
}
|
||||
|
||||
function optHistoryStatus(h) {
|
||||
if (h.status_label) return h.status_label;
|
||||
if (h.status === "open") return "持仓中";
|
||||
if (h.status !== "closed") return "持仓中";
|
||||
if ((h.signal_note || "").indexOf("到期结算") >= 0) return "到期";
|
||||
if (h.premium_received === 0 && h.realized_pnl != null && h.realized_pnl < 0 && !h.close_ord_id) {
|
||||
return "到期";
|
||||
}
|
||||
return "已平";
|
||||
}
|
||||
|
||||
@@ -899,7 +910,7 @@
|
||||
const s = optHistoryStatus(h);
|
||||
let cls = "opt-hist-status";
|
||||
if (s === "已平") cls += " opt-hist-status--closed";
|
||||
else if (s === "到期") cls += " opt-hist-status--expired";
|
||||
else if (s === "到期" || s === "强平") cls += " opt-hist-status--expired";
|
||||
else cls += " opt-hist-status--open";
|
||||
return '<span class="' + cls + '">' + s + "</span>";
|
||||
}
|
||||
@@ -910,13 +921,14 @@
|
||||
tbody.innerHTML = "";
|
||||
const list = (d.ok && d.history) || [];
|
||||
if (!list.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="muted">暂无历史记录</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="muted">暂无历史记录</td></tr>';
|
||||
return;
|
||||
}
|
||||
list.forEach(function (h) {
|
||||
const tr = document.createElement("tr");
|
||||
const premTxt = h.premium_paid != null ? fmt(h.premium_paid, 2) : "—";
|
||||
const pnl = h.realized_pnl;
|
||||
const premTxt = fmtDisplay(h.premium_paid_fmt, h.premium_paid != null ? fmt(h.premium_paid, 2) : null);
|
||||
const isOpen = h.status === "open";
|
||||
const pnl = isOpen ? null : h.realized_pnl;
|
||||
const pnlTxt = pnl != null ? fmt(pnl, 2) : "—";
|
||||
const pnlCls = pnl > 0 ? "pos-pnl-profit" : pnl < 0 ? "pos-pnl-loss" : "";
|
||||
const timeTxt = (h.closed_at || h.created_at || "—").replace("T", " ").slice(0, 19);
|
||||
@@ -926,15 +938,9 @@
|
||||
"<td>" + premTxt + "</td>" +
|
||||
"<td>" + optHistoryStatusHtml(h) + "</td>" +
|
||||
'<td class="' + pnlCls + '">' + pnlTxt + "</td>" +
|
||||
"<td class=\"opt-hist-time\">" + timeTxt + "</td>" +
|
||||
'<td><button type="button" class="btn-secondary btn-sm opt-history-del" data-id="' + h.id + '" data-status="' + (h.status || "") + '">删除</button></td>';
|
||||
'<td class="opt-hist-time">' + timeTxt + "</td>";
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
tbody.querySelectorAll(".opt-history-del").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
deleteHistoryRow(btn.getAttribute("data-id"), btn.getAttribute("data-status"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function refreshAllPositions() {
|
||||
|
||||
Reference in New Issue
Block a user