中控增加条件单委托

This commit is contained in:
dekun
2026-05-24 08:09:08 +08:00
parent 4b5fae2946
commit 3b97a59562
9 changed files with 731 additions and 14 deletions
+146 -8
View File
@@ -3,6 +3,7 @@
let settingsCache = null;
let monitorTimer = null;
let authState = { required: false, logged_in: true };
let tpslPending = null;
async function apiFetch(url, opts) {
const r = await fetch(url, opts);
@@ -146,8 +147,22 @@
);
});
box.querySelectorAll(".btn-cancel-cond-all").forEach((btn) => {
btn.onclick = () =>
btn.onclick = (ev) => {
ev.preventDefault();
ev.stopPropagation();
cancelSymbolOrders(btn.dataset.exId, btn.dataset.symbol, "conditional");
};
});
box.querySelectorAll(".btn-place-tpsl").forEach((btn) => {
btn.onclick = () =>
openTpslModal(
btn.dataset.exId,
btn.dataset.symbol,
btn.dataset.side,
btn.dataset.contracts,
btn.dataset.sl || "",
btn.dataset.tp || ""
);
});
} catch (e) {
box.innerHTML = `<div class="err">${esc(e)}</div>`;
@@ -180,15 +195,34 @@
return `<table class="data-table data-table-sub"><thead><tr><th>类型</th><th>数量</th><th>触发/价格</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table>`;
}
function guessTpslFromCondOrders(side, cond) {
const triggers = (cond || [])
.map((o) => o.trigger_price)
.filter((v) => v != null && !Number.isNaN(Number(v)))
.map(Number);
if (!triggers.length) return { sl: "", tp: "" };
triggers.sort((a, b) => a - b);
const s = (side || "long").toLowerCase();
if (s === "short") {
return { sl: triggers[triggers.length - 1], tp: triggers[0] };
}
return { sl: triggers[0], tp: triggers[triggers.length - 1] };
}
function renderPositionBlock(exchangeId, x) {
const symAttr = esc(x.symbol || "").replace(/"/g, "&quot;");
const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, "&quot;");
const contractsAttr = esc(String(x.contracts != null ? x.contracts : "")).replace(/"/g, "&quot;");
const cond = Array.isArray(x.conditional_orders) ? x.conditional_orders : [];
const reg = Array.isArray(x.regular_orders) ? x.regular_orders : [];
const guess = guessTpslFromCondOrders(x.side, cond);
const slAttr = esc(String(guess.sl)).replace(/"/g, "&quot;");
const tpAttr = esc(String(guess.tp)).replace(/"/g, "&quot;");
const condAllBtn =
cond.length > 0
? `<button type="button" class="btn-cancel-cond-all ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}">撤销全部条件单</button>`
? `<button type="button" class="btn-cancel-cond-all ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}">撤销全部</button>`
: "";
const condBody = renderOrderRows(exchangeId, x.symbol, cond, "conditional");
return `<div class="pos-block">
<table class="data-table"><thead><tr><th>合约</th><th>方向</th><th>张数</th><th>浮盈</th><th>操作</th></tr></thead><tbody>
<tr>
@@ -196,15 +230,20 @@
<td>${esc(x.side)}</td>
<td>${fmt(x.contracts, 4)}</td>
<td class="${pnlCls(x.unrealized_pnl)}">${fmt(x.unrealized_pnl, 4)}</td>
<td class="td-actions"><button type="button" class="btn-close-pos danger" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button></td>
<td class="td-actions td-actions-row">
<button type="button" class="btn-place-tpsl ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}" data-contracts="${contractsAttr}" data-sl="${slAttr}" data-tp="${tpAttr}">委托</button>
<button type="button" class="btn-close-pos danger" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button>
</td>
</tr>
</tbody></table>
<div class="pos-orders">
<div class="pos-orders-head">
<span class="pos-orders-title">条件单 · ${cond.length}</span>
${condAllBtn}
</div>
${renderOrderRows(exchangeId, x.symbol, cond, "conditional")}
<details class="orders-collapse">
<summary class="orders-collapse-summary">
<span class="pos-orders-title">条件单 · ${cond.length}</span>
${condAllBtn}
</summary>
<div class="orders-collapse-body">${condBody}</div>
</details>
<div class="pos-orders-head" style="margin-top:10px">
<span class="pos-orders-title">普通委托 · ${reg.length}</span>
</div>
@@ -213,6 +252,103 @@
</div>`;
}
function openTpslModal(exchangeId, symbol, side, contracts, slHint, tpHint) {
tpslPending = {
exchangeId,
symbol,
side: (side || "long").toLowerCase(),
contracts: parseFloat(contracts),
};
const modal = document.getElementById("tpsl-modal");
const meta = document.getElementById("tpsl-modal-meta");
const slIn = document.getElementById("tpsl-sl");
const tpIn = document.getElementById("tpsl-tp");
if (!modal || !meta || !slIn || !tpIn) return;
meta.textContent = `${symbol} · ${side} · ${contracts}`;
slIn.value = slHint !== "" && slHint != null ? String(slHint) : "";
tpIn.value = tpHint !== "" && tpHint != null ? String(tpHint) : "";
modal.classList.remove("hidden");
modal.setAttribute("aria-hidden", "false");
slIn.focus();
}
function closeTpslModal() {
tpslPending = null;
const modal = document.getElementById("tpsl-modal");
if (modal) {
modal.classList.add("hidden");
modal.setAttribute("aria-hidden", "true");
}
}
async function submitTpslModal() {
if (!tpslPending) return;
const slIn = document.getElementById("tpsl-sl");
const tpIn = document.getElementById("tpsl-tp");
const sl = parseFloat(slIn && slIn.value);
const tp = parseFloat(tpIn && tpIn.value);
if (!sl || sl <= 0 || !tp || tp <= 0) {
showToast("请填写有效的止损价与止盈价", true);
return;
}
const { exchangeId, symbol, side, contracts } = tpslPending;
if (
!confirm(
`确认 ${symbol} ${side}\n先撤销全部条件单,再挂止损 ${sl}、止盈 ${tp}`
)
) {
return;
}
const btn = document.getElementById("tpsl-submit");
if (btn) btn.disabled = true;
try {
const r = await apiFetch(
"/api/orders/" + encodeURIComponent(exchangeId) + "/place-tpsl",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
symbol,
side,
stop_loss: sl,
take_profit: tp,
contracts: contracts > 0 ? contracts : null,
}),
}
);
const j = await r.json();
const pl = j.payload || {};
const ok = j.ok && pl.ok !== false;
const n = pl.placed && pl.placed.cancelled_conditional;
showToast(
ok
? `已挂单(已撤 ${n != null ? n : "?"} 笔旧条件单)`
: pl.error || JSON.stringify(j),
!ok
);
if (ok) {
closeTpslModal();
loadMonitorBoard();
}
} catch (e) {
showToast(String(e), true);
} finally {
if (btn) btn.disabled = false;
}
}
function initTpslModal() {
const backdrop = document.getElementById("tpsl-modal-backdrop");
const cancel = document.getElementById("tpsl-cancel");
const submit = document.getElementById("tpsl-submit");
if (backdrop) backdrop.onclick = closeTpslModal;
if (cancel) cancel.onclick = closeTpslModal;
if (submit) submit.onclick = () => submitTpslModal();
document.addEventListener("keydown", (ev) => {
if (ev.key === "Escape") closeTpslModal();
});
}
async function cancelOneOrder(exchangeId, symbol, orderId, channel) {
if (!confirm(`撤销委托 ${symbol} #${orderId}`)) return;
try {
@@ -557,6 +693,8 @@
showToast("已添加一行,请填写 URL 后点「保存设置」");
};
initTpslModal();
initAuth().then((ok) => {
if (!ok) return;
loadSettings().catch(() => {});