Perp hedge: dialog order like options; right panel shows positions/orders/history/stats.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+240
-85
@@ -15,12 +15,13 @@
|
||||
function pickDefaultTab() {
|
||||
if (showPerp) return "perp_options";
|
||||
if (showOo) return "options_options";
|
||||
return "active";
|
||||
return "perp_options";
|
||||
}
|
||||
|
||||
const state = {
|
||||
tab: pickDefaultTab(),
|
||||
mode: showPerp ? "perp_options" : showOo ? "options_options" : "perp_options",
|
||||
posTab: "live",
|
||||
underlying: root.getAttribute("data-default-underly") || "ETH",
|
||||
moneyFilter: root.getAttribute("data-option-primary") !== "0" ? "all" : "itm",
|
||||
ooMoneyFilter: "atm_otm", // 期期锁定:平值+虚值
|
||||
@@ -29,6 +30,7 @@
|
||||
poStrikeExpandAll: false, // 永期手动选约:默认平值+实3+虚3
|
||||
chain: null,
|
||||
selected: null,
|
||||
orderDialogOpen: false,
|
||||
legA: null,
|
||||
legB: null,
|
||||
market: null,
|
||||
@@ -72,7 +74,7 @@
|
||||
if (isOptionPrimary()) {
|
||||
hint.textContent =
|
||||
(dir === "short" ? "买 Put + 永续多" : "买 Call + 永续空") +
|
||||
" · 点「选择」填入右侧手动下单;比例默认 2(期权 1 → 永续 0.5)。";
|
||||
" · 点「选择」弹窗下单;比例默认 2(期权 1 → 永续 0.5)。";
|
||||
} else {
|
||||
hint.textContent = dir === "short" ? "永续空 + 保险 Call" : "永续多 + 保险 Put";
|
||||
}
|
||||
@@ -89,8 +91,11 @@
|
||||
state.direction = next;
|
||||
syncPoDirUI();
|
||||
if (!changed && !forceReload) return;
|
||||
state.selected = null;
|
||||
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
|
||||
if (state.orderDialogOpen) closeOrderDialog();
|
||||
else {
|
||||
state.selected = null;
|
||||
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
|
||||
}
|
||||
void loadMarket().then(function () {
|
||||
renderListStrikes();
|
||||
});
|
||||
@@ -681,7 +686,7 @@
|
||||
if (!btn) return;
|
||||
if (isOptionPrimary()) {
|
||||
btn.textContent = "手动下单";
|
||||
btn.title = "按左侧已选合约卖一限价开仓(买期权+对冲永续);非交易所市价单、非条件盯盘";
|
||||
btn.title = "弹窗内确认:按已选合约卖一限价开仓(买期权+对冲永续);非交易所市价单";
|
||||
} else {
|
||||
btn.textContent = "计算";
|
||||
btn.title = "情景测算后再启动";
|
||||
@@ -777,13 +782,110 @@
|
||||
return (Math.round((1 / a) * 10) / 10).toFixed(1) + "×";
|
||||
}
|
||||
|
||||
function clearPoSelection() {
|
||||
const orderPanelHome = (function () {
|
||||
const host = document.getElementById("hp-order-panel-host");
|
||||
return host ? host.parentElement : null;
|
||||
})();
|
||||
|
||||
function orderPanel() {
|
||||
return $("hp-order-panel");
|
||||
}
|
||||
|
||||
function orderPanelHost() {
|
||||
return $("hp-order-panel-host");
|
||||
}
|
||||
|
||||
function parkOrderPanel() {
|
||||
const panel = orderPanel();
|
||||
const host = orderPanelHost();
|
||||
if (panel && host && panel.parentElement !== host) host.appendChild(panel);
|
||||
if (host) {
|
||||
host.hidden = true;
|
||||
host.setAttribute("aria-hidden", "true");
|
||||
if (orderPanelHome && host.parentElement !== orderPanelHome) {
|
||||
orderPanelHome.appendChild(host);
|
||||
}
|
||||
}
|
||||
if (panel) panel.style.display = "none";
|
||||
state.orderDialogOpen = false;
|
||||
const tbody = $("hp-strike-tbody");
|
||||
if (tbody) {
|
||||
tbody.querySelectorAll(".opt-strike-row").forEach(function (r) {
|
||||
r.classList.remove("opt-row-selected");
|
||||
});
|
||||
tbody.querySelectorAll(".hp-pick").forEach(function (b) {
|
||||
b.classList.remove("active");
|
||||
if (b.dataset.origText) b.textContent = b.dataset.origText;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openOrderDialog(instId) {
|
||||
const panel = orderPanel();
|
||||
const host = orderPanelHost();
|
||||
if (!panel || !host) return false;
|
||||
if (panel.parentElement !== host) host.appendChild(panel);
|
||||
if (host.parentElement !== document.body) document.body.appendChild(host);
|
||||
host.hidden = false;
|
||||
host.setAttribute("aria-hidden", "false");
|
||||
panel.style.display = "";
|
||||
state.orderDialogOpen = true;
|
||||
const tbody = $("hp-strike-tbody");
|
||||
if (tbody && instId) {
|
||||
tbody.querySelectorAll(".opt-strike-row").forEach(function (r) {
|
||||
r.classList.toggle("opt-row-selected", r.getAttribute("data-inst") === instId);
|
||||
});
|
||||
tbody.querySelectorAll(".hp-pick").forEach(function (b) {
|
||||
if (!b.dataset.origText) b.dataset.origText = "选择";
|
||||
const on = b.getAttribute("data-inst") === instId;
|
||||
b.classList.toggle("active", on);
|
||||
b.textContent = on ? "已选" : b.dataset.origText;
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function closeOrderDialog() {
|
||||
state.selected = null;
|
||||
parkOrderPanel();
|
||||
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
|
||||
updatePremiumLine();
|
||||
updatePerpPnlHint();
|
||||
}
|
||||
|
||||
function clearPoSelection() {
|
||||
state.selected = null;
|
||||
if ($("hp-sel-inst")) $("hp-sel-inst").textContent = "—";
|
||||
if (state.orderDialogOpen) parkOrderPanel();
|
||||
updatePremiumLine();
|
||||
updatePerpPnlHint();
|
||||
}
|
||||
|
||||
function setHpPosTab(tab) {
|
||||
const next = tab === "pending" || tab === "history" || tab === "stats" ? tab : "live";
|
||||
state.posTab = next;
|
||||
document.querySelectorAll(".hp-pos-tab").forEach(function (btn) {
|
||||
const on = btn.getAttribute("data-hp-pos-tab") === next;
|
||||
btn.classList.toggle("active", on);
|
||||
btn.setAttribute("aria-selected", on ? "true" : "false");
|
||||
});
|
||||
document.querySelectorAll("[data-hp-pos-pane]").forEach(function (pane) {
|
||||
const on = pane.getAttribute("data-hp-pos-pane") === next;
|
||||
pane.classList.toggle("is-active", on);
|
||||
if (on) pane.removeAttribute("hidden");
|
||||
else pane.setAttribute("hidden", "");
|
||||
});
|
||||
if (next === "live" || next === "pending") void loadActivePlans();
|
||||
else if (next === "history") void loadHistory();
|
||||
else if (next === "stats") void loadStats();
|
||||
}
|
||||
|
||||
function refreshHpPosPanel() {
|
||||
void loadActivePlans();
|
||||
void loadHistory();
|
||||
if (state.posTab === "stats") void loadStats();
|
||||
}
|
||||
|
||||
/** 手动选约:不自动代选;仅在已选失效(换 Call/Put 或筛掉)时清空. */
|
||||
function autoMatchPoOption(force) {
|
||||
if (!isOptionPrimary()) return;
|
||||
@@ -873,7 +975,7 @@
|
||||
b.classList.toggle("active", on);
|
||||
b.setAttribute("aria-selected", on ? "true" : "false");
|
||||
});
|
||||
["perp_options", "options_options", "active", "history", "stats"].forEach(function (id) {
|
||||
["perp_options", "options_options"].forEach(function (id) {
|
||||
const panel = $("hp-tab-" + id);
|
||||
if (!panel) return;
|
||||
const on = id === tab;
|
||||
@@ -884,6 +986,9 @@
|
||||
if (tab === "perp_options" || tab === "options_options") {
|
||||
state.mode = tab;
|
||||
}
|
||||
if (tab === "perp_options") {
|
||||
refreshHpPosPanel();
|
||||
}
|
||||
const hint = $("hp-acct-hint");
|
||||
if (hint) {
|
||||
if (tab === "options_options") {
|
||||
@@ -1472,18 +1577,9 @@
|
||||
state.selected = c;
|
||||
const el = $("hp-sel-inst");
|
||||
if (el) el.textContent = c.inst_id;
|
||||
const tbody = $("hp-strike-tbody");
|
||||
if (tbody) {
|
||||
tbody.querySelectorAll(".opt-strike-row").forEach(function (r) {
|
||||
r.classList.toggle("opt-row-selected", r.getAttribute("data-inst") === c.inst_id);
|
||||
});
|
||||
tbody.querySelectorAll(".hp-pick").forEach(function (b) {
|
||||
b.classList.toggle("active", b.getAttribute("data-inst") === c.inst_id);
|
||||
});
|
||||
}
|
||||
updatePremiumLine();
|
||||
if (isOptionPrimary()) {
|
||||
// 手动下单以张数为主:选约后若未填张数则默认 1,并同步左右
|
||||
// 手动下单以张数为主:选约后若未填张数则默认 1
|
||||
if (!(orderSheets() > 0)) {
|
||||
if ($("hp-sheets")) $("hp-sheets").value = "1";
|
||||
if ($("hp-order-sheets")) $("hp-order-sheets").value = "1";
|
||||
@@ -1491,10 +1587,11 @@
|
||||
syncSheetsInputs("hp-order-sheets");
|
||||
if (!(numInput("hp-order-sheets", 0) > 0)) syncSheetsInputs("hp-sheets");
|
||||
}
|
||||
void loadMarket();
|
||||
updatePerpPnlHint();
|
||||
updatePoOpenSummary();
|
||||
}
|
||||
openOrderDialog(c.inst_id);
|
||||
void loadMarket();
|
||||
}
|
||||
|
||||
function renderListStrikes() {
|
||||
@@ -1608,11 +1705,11 @@
|
||||
if (line) line.textContent = "";
|
||||
return;
|
||||
}
|
||||
const sheets = Number(($("hp-sheets") && $("hp-sheets").value) || 1);
|
||||
const sheets = Math.max(1, orderSheets() || Number(($("hp-sheets") && $("hp-sheets").value) || 1));
|
||||
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) + " 币(币本位)";
|
||||
line.textContent = "预估权利金 ≈ " + fmt(prem, 4) + " 币(币本位) · 卖一 " + fmt(ask, 4);
|
||||
}
|
||||
|
||||
function buildStraddleRows(contracts) {
|
||||
@@ -1881,7 +1978,8 @@
|
||||
state.underlying = next;
|
||||
syncUnderlyingUI();
|
||||
if (!changed && !forceReload) return;
|
||||
state.selected = null;
|
||||
if (state.orderDialogOpen) closeOrderDialog();
|
||||
else state.selected = null;
|
||||
state.legA = null;
|
||||
state.legB = null;
|
||||
state.ooRecommend = null;
|
||||
@@ -2133,18 +2231,36 @@
|
||||
if (state.tab === "perp_options" || state.tab === "options_options") {
|
||||
void loadGates();
|
||||
if (state.tab === "perp_options") void refreshPoStrategyStatus();
|
||||
} else if (state.tab === "active") {
|
||||
void loadActivePlans();
|
||||
} else if (state.tab === "history") {
|
||||
void loadHistory();
|
||||
} else if (state.tab === "stats") {
|
||||
void loadStats();
|
||||
} else {
|
||||
const el = $("hp-gate-line");
|
||||
if (el) el.textContent = "";
|
||||
}
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".hp-pos-tab").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
setHpPosTab(btn.getAttribute("data-hp-pos-tab") || "live");
|
||||
});
|
||||
});
|
||||
if ($("hp-pos-refresh")) {
|
||||
$("hp-pos-refresh").addEventListener("click", function () {
|
||||
refreshHpPosPanel();
|
||||
});
|
||||
}
|
||||
function onOrderDialogClose() {
|
||||
closeOrderDialog();
|
||||
}
|
||||
if ($("hp-order-close-btn")) $("hp-order-close-btn").addEventListener("click", onOrderDialogClose);
|
||||
if ($("hp-order-cancel-btn")) $("hp-order-cancel-btn").addEventListener("click", onOrderDialogClose);
|
||||
const orderHost = orderPanelHost();
|
||||
if (orderHost) {
|
||||
orderHost.addEventListener("click", function (ev) {
|
||||
if (ev.target === orderHost) closeOrderDialog();
|
||||
});
|
||||
}
|
||||
document.addEventListener("keydown", function (ev) {
|
||||
if (ev.key === "Escape" && state.orderDialogOpen) closeOrderDialog();
|
||||
});
|
||||
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
|
||||
b.addEventListener("click", function () {
|
||||
setUnderlying(b.getAttribute("data-uly") || "ETH", true);
|
||||
@@ -2617,65 +2733,92 @@
|
||||
return map[st] || st || "—";
|
||||
}
|
||||
|
||||
async function loadActivePlans() {
|
||||
const tbody = $("hp-active-tbody");
|
||||
function bindActivePlanRowActions(tbody) {
|
||||
if (!tbody) return;
|
||||
tbody.querySelectorAll(".hp-btn-detail").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
void showPlanDetail(Number(btn.getAttribute("data-id")));
|
||||
});
|
||||
});
|
||||
tbody.querySelectorAll(".hp-btn-complete").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
void completeMissingLeg(Number(btn.getAttribute("data-id")));
|
||||
});
|
||||
});
|
||||
tbody.querySelectorAll(".hp-btn-end").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
void endActivePlan(Number(btn.getAttribute("data-id")));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isPendingPlan(p) {
|
||||
const st = String((p && p.status) || "").toLowerCase();
|
||||
return st === "watching" || st === "pending";
|
||||
}
|
||||
|
||||
function renderActivePlanRows(tbody, rows, emptyMsg, colMode) {
|
||||
if (!tbody) return;
|
||||
if (!rows.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="muted">' + (emptyMsg || "暂无") + "</td></tr>";
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = "";
|
||||
rows.forEach(function (p) {
|
||||
const tr = document.createElement("tr");
|
||||
const typeLabel = p.plan_type === "perp_options" ? "永期" : "期期";
|
||||
const contracts = p.contracts_summary || "—";
|
||||
const midCol =
|
||||
colMode === "pending"
|
||||
? p.note || activeTargetLabel(p) || "盯盘/待成交"
|
||||
: activeTargetLabel(p);
|
||||
tr.innerHTML =
|
||||
"<td>#" +
|
||||
p.id +
|
||||
"</td><td>" +
|
||||
typeLabel +
|
||||
"</td><td>" +
|
||||
(p.underlying || "") +
|
||||
"</td><td class=\"hp-contracts-cell\" title=\"" +
|
||||
String(contracts).replace(/"/g, """) +
|
||||
"\"><code>" +
|
||||
contracts +
|
||||
"</code></td><td>" +
|
||||
activeStatusLabel(p) +
|
||||
"</td><td>" +
|
||||
midCol +
|
||||
"</td><td>" +
|
||||
(p.opened_at || "—") +
|
||||
'</td><td class="hp-hist-actions">' +
|
||||
completeLegButtonHtml(p) +
|
||||
'<button type="button" class="btn-secondary hp-btn-detail" data-id="' +
|
||||
p.id +
|
||||
'">成交细节</button> ' +
|
||||
'<button type="button" class="btn-secondary hp-btn-end" data-id="' +
|
||||
p.id +
|
||||
'">结束计划</button></td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
bindActivePlanRowActions(tbody);
|
||||
}
|
||||
|
||||
async function loadActivePlans() {
|
||||
const liveBody = $("hp-active-tbody");
|
||||
const pendingBody = $("hp-pending-tbody");
|
||||
if (!liveBody && !pendingBody) return;
|
||||
try {
|
||||
const d = await apiJson("/api/hedge-plan/active");
|
||||
const rows = d.plans || [];
|
||||
if (!rows.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="muted">暂无进行中的计划</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = "";
|
||||
rows.forEach(function (p) {
|
||||
const tr = document.createElement("tr");
|
||||
const typeLabel = p.plan_type === "perp_options" ? "永期" : "期期";
|
||||
const contracts = p.contracts_summary || "—";
|
||||
tr.innerHTML =
|
||||
"<td>#" +
|
||||
p.id +
|
||||
"</td><td>" +
|
||||
typeLabel +
|
||||
"</td><td>" +
|
||||
(p.underlying || "") +
|
||||
"</td><td class=\"hp-contracts-cell\" title=\"" +
|
||||
String(contracts).replace(/"/g, """) +
|
||||
"\"><code>" +
|
||||
contracts +
|
||||
"</code></td><td>" +
|
||||
activeStatusLabel(p) +
|
||||
"</td><td>" +
|
||||
activeTargetLabel(p) +
|
||||
"</td><td>" +
|
||||
(p.opened_at || "—") +
|
||||
'</td><td class="hp-hist-actions">' +
|
||||
completeLegButtonHtml(p) +
|
||||
'<button type="button" class="btn-secondary hp-btn-detail" data-id="' +
|
||||
p.id +
|
||||
'">成交细节</button> ' +
|
||||
'<button type="button" class="btn-secondary hp-btn-end" data-id="' +
|
||||
p.id +
|
||||
'">结束计划</button></td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
tbody.querySelectorAll(".hp-btn-detail").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
void showPlanDetail(Number(btn.getAttribute("data-id")));
|
||||
});
|
||||
});
|
||||
tbody.querySelectorAll(".hp-btn-complete").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
void completeMissingLeg(Number(btn.getAttribute("data-id")));
|
||||
});
|
||||
});
|
||||
tbody.querySelectorAll(".hp-btn-end").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
void endActivePlan(Number(btn.getAttribute("data-id")));
|
||||
});
|
||||
const pending = rows.filter(isPendingPlan);
|
||||
const live = rows.filter(function (p) {
|
||||
return !isPendingPlan(p);
|
||||
});
|
||||
renderActivePlanRows(liveBody, live, "暂无当前持仓", "live");
|
||||
renderActivePlanRows(pendingBody, pending, "暂无当前委托", "pending");
|
||||
} catch (e) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="err">' + (e.message || e) + "</td></tr>";
|
||||
const msg = '<tr><td colspan="8" class="err">' + (e.message || e) + "</td></tr>";
|
||||
if (liveBody) liveBody.innerHTML = msg;
|
||||
if (pendingBody) pendingBody.innerHTML = msg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2832,7 +2975,7 @@
|
||||
try {
|
||||
await apiJson("/api/hedge-plan/" + planId, { method: "DELETE" });
|
||||
await loadHistory();
|
||||
if (state.tab === "stats") await loadStats();
|
||||
if (state.posTab === "stats") await loadStats();
|
||||
} catch (e) {
|
||||
window.alert(e.message || String(e));
|
||||
}
|
||||
@@ -3107,16 +3250,14 @@
|
||||
closePreviewModal();
|
||||
const refreshHint =
|
||||
d.refresh && d.refresh.msg ? "\n" + String(d.refresh.msg) : "";
|
||||
if (state.orderDialogOpen) closeOrderDialog();
|
||||
if (d.partial) {
|
||||
alert(
|
||||
(d.msg || "半腿失败,已挂待补") +
|
||||
(d.plan_id ? "\n计划 #" + d.plan_id : "") +
|
||||
refreshHint +
|
||||
"\n请到「进行中的计划」补开缺失腿"
|
||||
"\n请到右侧「当前持仓」补开缺失腿"
|
||||
);
|
||||
state.tab = "active";
|
||||
syncTabUI();
|
||||
void loadActivePlans();
|
||||
} else {
|
||||
alert(
|
||||
"计划已启动 #" +
|
||||
@@ -3125,6 +3266,12 @@
|
||||
refreshHint
|
||||
);
|
||||
}
|
||||
if (showPerp) {
|
||||
state.tab = "perp_options";
|
||||
syncTabUI();
|
||||
setHpPosTab(d.partial ? "live" : "live");
|
||||
}
|
||||
refreshHpPosPanel();
|
||||
void loadGates();
|
||||
if (isOo) void loadChain();
|
||||
} catch (e) {
|
||||
@@ -3164,10 +3311,18 @@
|
||||
updateOoBudgetLine(null);
|
||||
bind();
|
||||
syncOptionPrimaryUI();
|
||||
setHpPosTab(state.posTab || "live");
|
||||
parkOrderPanel();
|
||||
void refreshAll().then(function () {
|
||||
void refreshPoStrategyStatus();
|
||||
refreshHpPosPanel();
|
||||
});
|
||||
setInterval(function () {
|
||||
if (state.tab === "perp_options" && isOptionPrimary()) void refreshPoStrategyStatus();
|
||||
}, 15000);
|
||||
setInterval(function () {
|
||||
if (state.tab === "perp_options" && (state.posTab === "live" || state.posTab === "pending")) {
|
||||
void loadActivePlans();
|
||||
}
|
||||
}, 20000);
|
||||
})();
|
||||
|
||||
@@ -3911,6 +3911,29 @@ html[data-theme="light"] .opt-be-dist-down {
|
||||
.hedge-plan-page-wrap #hp-po-layout > .card {
|
||||
align-self: stretch;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-po-pos-card {
|
||||
min-height: 0;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-po-pos-card .options-pos-tab-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 240px;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-po-pos-card .options-pos-pane {
|
||||
overflow: auto;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-po-order-dialog .hp-po-section-title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-po-order-dialog .hp-po-fields--section {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-pos-tab.active,
|
||||
.hedge-plan-page-wrap .opt-pos-tab.active {
|
||||
border-color: #5b8cff;
|
||||
color: #cfe0ff;
|
||||
background: rgba(74, 124, 255, 0.28);
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-po-left-stack,
|
||||
.hedge-plan-page-wrap .hp-po-right-stack {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user