Improve hedge-plan option board: moneyness, px/sz, accounts, and denser UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-14 12:27:09 +08:00
parent 4df2cb4253
commit 09cd680e4a
6 changed files with 333 additions and 69 deletions
+173 -41
View File
@@ -9,6 +9,7 @@
tab: "perp_options",
mode: "perp_options",
underlying: root.getAttribute("data-default-underly") || "ETH",
moneyFilter: "all",
chain: null,
selected: null,
legA: null,
@@ -34,10 +35,71 @@
return Number(v).toFixed(d == null ? 2 : d);
}
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).replace(/(\.\d*?[1-9])0+$/, "$1").replace(/\.0+$/, "");
}
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";
}
/** 价格/流动性(张),价格按 tick_sz 对齐交易所精度. */
function fmtPxSz(px, sz, estimated, tickSz) {
if (px === null || px === undefined || Number.isNaN(Number(px))) return "—";
let price = fmtOptionPx(px, tickSz);
if (price === "—") return "—";
if (estimated) price += "~";
if (sz === null || sz === undefined || sz === "" || Number.isNaN(Number(sz))) return price;
const s = Number(sz);
const size = Math.abs(s - Math.round(s)) < 1e-9 ? String(Math.round(s)) : String(s);
return price + "/" + size;
}
function moneynessBadge(c) {
const m = (c && c.moneyness) || "";
const label = (c && c.moneyness_label) || "—";
return '<span class="opt-moneyness opt-moneyness-' + m + '">' + label + "</span>";
}
function matchesMoneyFilter(c) {
const f = state.moneyFilter || "all";
if (f === "all") return true;
const m = (c.moneyness || "").toLowerCase();
if (f === "itm") return m === "itm" || m === "atm";
if (f === "otm") return m === "otm";
return true;
}
function optTypeForDirection(dir) {
return dir === "short" ? "C" : "P";
}
function syncUnderlyingUI() {
const uly = state.underlying || "ETH";
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
const on = b.getAttribute("data-uly") === uly;
b.classList.toggle("active", on);
b.setAttribute("aria-pressed", on ? "true" : "false");
});
const lab = $("hp-perp-uly-label");
if (lab) lab.textContent = uly;
const ooLab = $("hp-oo-uly-label");
if (ooLab) ooLab.textContent = uly;
}
function syncMoneyUI() {
document.querySelectorAll(".hp-money-btn").forEach(function (b) {
const on = b.getAttribute("data-money") === state.moneyFilter;
b.classList.toggle("active", on);
});
}
function syncTabUI() {
const tab = state.tab || "perp_options";
document.querySelectorAll(".hp-tab").forEach(function (b) {
@@ -58,10 +120,6 @@
}
}
function syncModeUI() {
syncTabUI();
}
function setGateLine(gates) {
const el = $("hp-gate-line");
if (!el) return;
@@ -80,6 +138,23 @@
if (start) start.disabled = !gates.can_start;
}
function setOptionsBalance(chain) {
const acct = (chain && chain.options_account) || {};
const label = (chain && chain.account_label) || acct.label || "期权账户";
const tag = $("hp-opt-acct-tag");
if (tag) tag.textContent = label;
const line =
label +
" · 交易 USDC " +
fmt(acct.trading_usdc, 2) +
" · 资金 USDC " +
fmt(acct.funding_usdc, 2);
const el = $("hp-opt-bal-line");
if (el) el.textContent = line;
const oo = $("hp-oo-bal-line");
if (oo) oo.textContent = line;
}
async function loadGates() {
try {
const d = await apiJson("/api/hedge-plan/gates?plan_type=" + encodeURIComponent(state.mode));
@@ -99,10 +174,19 @@
);
state.market = d;
setGateLine(d.gates);
const acctLabel = d.account_label || "合约账户";
const tag = $("hp-perp-acct-tag");
if (tag) tag.textContent = acctLabel;
const q = $("hp-perp-quote");
if (q) {
q.innerHTML =
"标记 <strong>" +
"<strong>" +
(d.base || state.underlying) +
"</strong> · " +
acctLabel +
"可用 <strong>" +
fmt(d.available_usdt, 2) +
"</strong> U<br/>标记 <strong>" +
fmt(d.mark, 2) +
"</strong> · 最新 " +
fmt(d.last, 2) +
@@ -110,18 +194,17 @@
fmt(d.ask, 2) +
" · 买一 " +
fmt(d.bid, 2) +
"<br/>面值 " +
fmt(d.contract_size, 4) +
" · 可用 " +
fmt(d.available_usdt, 2) +
" U";
" · 面值 " +
fmt(d.contract_size, 4);
}
const sz = $("hp-sizing-line");
if (sz) {
if (d.full_margin_sizing) {
const s = d.full_margin_sizing;
sz.textContent =
"全仓建议:保证金 " +
"全仓建议(" +
acctLabel +
"):保证金 " +
fmt(s.margin_capital, 2) +
"U × " +
s.leverage +
@@ -166,10 +249,15 @@
"/api/hedge-plan/options-chain?underlying=" + encodeURIComponent(state.underlying)
);
state.chain = d;
const uly = d.underlying || state.underlying;
const idx = $("hp-index-line");
if (idx) idx.textContent = "指数 " + fmt(d.index_px, 2) + " · " + (d.inst_family || "");
if (idx) {
idx.textContent =
"指数 " + uly + " " + fmt(d.index_px, 2) + " · " + (d.inst_family || "") + " · 实值含平值 · 虚值=价外";
}
const ooIdx = $("hp-oo-index");
if (ooIdx) ooIdx.textContent = "指数 " + fmt(d.index_px, 2);
if (ooIdx) ooIdx.textContent = "指数 " + uly + " " + fmt(d.index_px, 2);
setOptionsBalance(d);
fillExpSelect($("hp-exp-select"), d);
fillExpSelect($("hp-oo-exp-select"), d);
renderListStrikes();
@@ -194,13 +282,14 @@
const dir = ($("hp-direction") && $("hp-direction").value) || "long";
const want = optTypeForDirection(dir);
const exp = currentExp("hp-exp-select");
const prevInst = state.selected && state.selected.inst_id;
tbody.innerHTML = "";
if (!exp) {
tbody.innerHTML = '<tr><td colspan="5" class="muted">请选择到期日</td></tr>';
return;
}
const list = (exp.contracts || []).filter(function (c) {
return String(c.opt_type || "").toUpperCase() === want;
return String(c.opt_type || "").toUpperCase() === want && matchesMoneyFilter(c);
});
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="5" class="muted">无匹配合约</td></tr>';
@@ -208,16 +297,21 @@
}
list.forEach(function (c) {
const tr = document.createElement("tr");
tr.className = "opt-strike-row" + (c.moneyness ? " opt-row-" + c.moneyness : "");
if (prevInst && c.inst_id === prevInst) tr.classList.add("opt-row-selected");
tr.setAttribute("data-inst", c.inst_id);
tr.innerHTML =
"<td>" +
c.strike +
"</td><td>" +
c.opt_type +
"</td><td>" +
fmt(c.ask, 4) +
"</td><td>" +
fmt(c.bid, 4) +
'</td><td><button type="button" class="btn-secondary hp-pick" data-inst="' +
moneynessBadge(c) +
'</td><td class="opt-px-sz">' +
fmtPxSz(c.ask, c.ask_sz, c.ask_estimated, c.tick_sz) +
'</td><td class="opt-px-sz">' +
fmtPxSz(c.bid, c.bid_sz, false, c.tick_sz) +
'</td><td><button type="button" class="btn-secondary hp-pick' +
(prevInst && c.inst_id === prevInst ? " active" : "") +
'" data-inst="' +
c.inst_id +
'">选用</button></td>';
tbody.appendChild(tr);
@@ -232,6 +326,12 @@
state.selected = c;
const el = $("hp-sel-inst");
if (el) el.textContent = c.inst_id;
tbody.querySelectorAll(".opt-strike-row").forEach(function (r) {
r.classList.toggle("opt-row-selected", r.getAttribute("data-inst") === inst);
});
tbody.querySelectorAll(".hp-pick").forEach(function (b) {
b.classList.toggle("active", b.getAttribute("data-inst") === inst);
});
updatePremiumLine();
});
});
@@ -247,7 +347,7 @@
const ct = Number(state.selected.ct_mult || 0.01);
const ask = Number(state.selected.ask || 0);
const prem = ask * sheets * ct;
line.textContent = "预估权利金 ≈ " + fmt(prem, 4) + " USDC";
line.textContent = "预估权利金 ≈ " + fmt(prem, 4) + " USDC(期权账户)";
}
function buildStraddleRows(contracts) {
@@ -274,31 +374,37 @@
const exp = currentExp("hp-oo-exp-select");
tbody.innerHTML = "";
if (!exp) {
tbody.innerHTML = '<tr><td colspan="5" class="muted">请选择到期日</td></tr>';
tbody.innerHTML = '<tr><td colspan="7" class="muted">请选择到期日</td></tr>';
return;
}
const rows = buildStraddleRows(exp.contracts);
rows.forEach(function (row) {
const tr = document.createElement("tr");
const callAsk = row.call ? fmt(row.call.ask, 4) : "—";
const putAsk = row.put ? fmt(row.put.ask, 4) : "—";
const call = row.call;
const put = row.put;
const callAsk = call ? fmtPxSz(call.ask, call.ask_sz, call.ask_estimated, call.tick_sz) : "—";
const putAsk = put ? fmtPxSz(put.ask, put.ask_sz, put.ask_estimated, put.tick_sz) : "—";
tr.innerHTML =
"<td>" +
'<td class="opt-t-call opt-px-sz">' +
callAsk +
'</td><td>' +
(row.call
'</td><td class="opt-t-call">' +
(call ? moneynessBadge(call) : "—") +
"</td><td>" +
(call
? '<button type="button" class="btn-secondary hp-oo-pick" data-side="C" data-inst="' +
row.call.inst_id +
call.inst_id +
'">Call</button>'
: "—") +
'</td><td class="opt-t-strike"><strong>' +
row.strike +
"</strong></td><td>" +
'</strong></td><td class="opt-t-put">' +
(put ? moneynessBadge(put) : "—") +
'</td><td class="opt-t-put opt-px-sz">' +
putAsk +
'</td><td>' +
(row.put
"</td><td>" +
(put
? '<button type="button" class="btn-secondary hp-oo-pick" data-side="P" data-inst="' +
row.put.inst_id +
put.inst_id +
'">Put</button>'
: "—") +
"</td>";
@@ -334,8 +440,10 @@
c.opt_type +
" K" +
c.strike +
" " +
(c.moneyness_label || "") +
" ask=" +
fmt(c.ask, 4) +
fmtPxSz(c.ask, c.ask_sz, c.ask_estimated, c.tick_sz) +
" (" +
c.inst_id +
")"
@@ -355,6 +463,26 @@
};
}
function setUnderlying(uly, forceReload) {
const next = (uly || "ETH").toUpperCase();
const changed = next !== state.underlying;
state.underlying = next;
syncUnderlyingUI();
if (!changed && !forceReload) return;
state.selected = null;
state.legA = null;
state.legB = null;
if ($("hp-entry")) $("hp-entry").value = "";
if ($("hp-contracts")) $("hp-contracts").value = "";
if ($("hp-tp")) $("hp-tp").value = "";
if ($("hp-sl")) $("hp-sl").value = "";
if ($("hp-target")) $("hp-target").value = "";
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
if ($("hp-premium-line")) $("hp-premium-line").textContent = "";
if ($("hp-oo-legs")) $("hp-oo-legs").textContent = "尚未选用两腿";
void refreshAll();
}
async function runPreview() {
const isOo = state.mode === "options_options";
const tbody = $(isOo ? "hp-result-tbody-oo" : "hp-result-tbody");
@@ -477,19 +605,21 @@
});
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
b.addEventListener("click", function () {
state.underlying = b.getAttribute("data-uly") || "ETH";
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (x) {
x.classList.toggle("active", x.getAttribute("data-uly") === state.underlying);
});
state.selected = null;
state.legA = null;
state.legB = null;
void refreshAll();
setUnderlying(b.getAttribute("data-uly") || "ETH", true);
});
});
document.querySelectorAll(".hp-money-btn").forEach(function (b) {
b.addEventListener("click", function () {
state.moneyFilter = b.getAttribute("data-money") || "all";
syncMoneyUI();
renderListStrikes();
});
});
const dir = $("hp-direction");
if (dir) {
dir.addEventListener("change", function () {
state.selected = null;
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
void loadMarket().then(function () {
renderListStrikes();
});
@@ -541,6 +671,8 @@
}
syncTabUI();
syncUnderlyingUI();
syncMoneyUI();
bind();
void refreshAll();
})();
+85 -8
View File
@@ -3169,6 +3169,18 @@ html[data-theme="light"] .opt-be-dist-down {
}
/* 对冲计划 Tab:高对比选中态 */
.hedge-plan-page-wrap {
font-size: 0.8rem;
}
.hedge-plan-page-wrap .card {
padding: 12px 14px;
}
.hedge-plan-page-wrap .card h2,
.hedge-plan-page-wrap .hp-title {
font-size: 0.9rem;
margin: 0 0 8px;
font-weight: 600;
}
.hedge-plan-page-wrap .hp-head-card {
margin-bottom: 12px;
}
@@ -3178,20 +3190,55 @@ html[data-theme="light"] .opt-be-dist-down {
gap: 10px;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
margin-bottom: 10px;
}
.hedge-plan-page-wrap .hp-title {
margin: 0;
}
.hedge-plan-page-wrap .hp-title-sub {
font-size: 0.85rem;
font-size: 0.75rem;
font-weight: 400;
}
.hedge-plan-page-wrap .hp-quote-line,
.hedge-plan-page-wrap .hp-acct-hint,
.hedge-plan-page-wrap #hp-sizing-line,
.hedge-plan-page-wrap #hp-gate-line {
font-size: 0.72rem;
line-height: 1.45;
margin: 4px 0 6px;
}
.hedge-plan-page-wrap .hp-acct-tag {
font-size: 0.68rem;
font-weight: 500;
margin-left: 4px;
}
.hedge-plan-page-wrap .form-row label,
.hedge-plan-page-wrap .form-row select,
.hedge-plan-page-wrap .form-row input,
.hedge-plan-page-wrap .form-row .btn-secondary,
.hedge-plan-page-wrap .form-row .primary {
font-size: 0.74rem;
}
.hedge-plan-page-wrap .form-row input[type="number"] {
max-width: 110px;
padding: 4px 6px;
min-height: 28px;
}
.hedge-plan-page-wrap .options-strike-table {
font-size: 0.74rem;
}
.hedge-plan-page-wrap .options-strike-table th,
.hedge-plan-page-wrap .options-strike-table td {
padding: 5px 4px;
}
.hedge-plan-page-wrap .options-strike-table code {
font-size: 0.66rem;
}
.hedge-plan-page-wrap .hp-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 0 0 10px;
margin: 0 0 8px;
padding: 6px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.28);
@@ -3202,9 +3249,9 @@ html[data-theme="light"] .opt-be-dist-down {
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.04);
color: #aeb6c5;
font-size: 0.92rem;
font-size: 0.82rem;
font-weight: 600;
padding: 9px 16px;
padding: 7px 14px;
border-radius: 8px;
cursor: pointer;
line-height: 1.2;
@@ -3221,6 +3268,31 @@ html[data-theme="light"] .opt-be-dist-down {
border-color: #fff;
box-shadow: 0 0 0 2px rgba(142, 182, 255, 0.55), 0 6px 16px rgba(0, 0, 0, 0.35);
}
.hedge-plan-page-wrap .hp-uly-btn,
.hedge-plan-page-wrap .hp-uly-btn-oo,
.hedge-plan-page-wrap .hp-money-btn {
min-width: 52px;
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.04);
color: #9aa4b2;
}
.hedge-plan-page-wrap .hp-uly-btn.active,
.hedge-plan-page-wrap .hp-uly-btn-oo.active,
.hedge-plan-page-wrap .hp-money-btn.active {
color: #0b1220;
background: linear-gradient(180deg, #ffffff 0%, #9ec0ff 100%);
border-color: #fff;
box-shadow: 0 0 0 2px rgba(100, 160, 255, 0.5);
}
.hedge-plan-page-wrap .hp-money-hint {
font-size: 0.68rem;
margin-left: 4px;
}
.hedge-plan-page-wrap .hp-pick.active,
.hedge-plan-page-wrap .opt-row-selected td {
background: rgba(90, 140, 255, 0.18);
}
.hedge-plan-page-wrap .hp-tab-panel.hidden,
.hedge-plan-page-wrap .hp-tab-panel[hidden] {
display: none !important;
@@ -3233,9 +3305,7 @@ html[data-theme="light"] .opt-be-dist-down {
border: 1px dashed rgba(255, 255, 255, 0.16);
color: #9aa4b2;
background: rgba(255, 255, 255, 0.03);
}
.hedge-plan-page-wrap #hp-gate-line {
margin: 0;
font-size: 0.78rem;
}
html[data-theme="light"] .hedge-plan-page-wrap .hp-tabs {
background: rgba(15, 23, 42, 0.06);
@@ -3252,6 +3322,13 @@ html[data-theme="light"] .hedge-plan-page-wrap .hp-tab.active {
border-color: #2f6fed;
box-shadow: 0 0 0 2px rgba(47, 111, 237, 0.25);
}
html[data-theme="light"] .hedge-plan-page-wrap .hp-uly-btn.active,
html[data-theme="light"] .hedge-plan-page-wrap .hp-uly-btn-oo.active,
html[data-theme="light"] .hedge-plan-page-wrap .hp-money-btn.active {
color: #0b1220;
background: linear-gradient(180deg, #ffffff 0%, #b9d2ff 100%);
border-color: #2f6fed;
}
html[data-theme="light"] .hedge-plan-page-wrap .hp-placeholder {
border-color: rgba(15, 23, 42, 0.18);
background: rgba(15, 23, 42, 0.03);