|
|
|
@@ -88,22 +88,6 @@
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderMonitorCountChips(counts) {
|
|
|
|
|
const mc = counts || {};
|
|
|
|
|
const chips = [];
|
|
|
|
|
const keys = Number(mc.keys) || 0;
|
|
|
|
|
const orders = Number(mc.orders) || 0;
|
|
|
|
|
const trends = Number(mc.trends) || 0;
|
|
|
|
|
const rolls = Number(mc.rolls) || 0;
|
|
|
|
|
if (keys > 0) chips.push(`<span class="dash-monitor-chip dash-monitor-key">关键位 ${keys}</span>`);
|
|
|
|
|
if (orders > 0) {
|
|
|
|
|
chips.push(`<span class="dash-monitor-chip dash-monitor-order">下单监控 ${orders}</span>`);
|
|
|
|
|
}
|
|
|
|
|
if (trends > 0) chips.push(`<span class="dash-monitor-chip dash-monitor-trend">趋势回调 ${trends}</span>`);
|
|
|
|
|
if (rolls > 0) chips.push(`<span class="dash-monitor-chip dash-monitor-roll">顺势加仓 ${rolls}</span>`);
|
|
|
|
|
return chips;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dashOptionsExpiryCd(expMs) {
|
|
|
|
|
const ms = expMs != null && expMs !== "" ? String(expMs) : "";
|
|
|
|
|
if (!ms) return "—";
|
|
|
|
@@ -130,34 +114,75 @@
|
|
|
|
|
return perp.length > 0 || (ac && ac.options_layout && optionsPositions.length > 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sourceBadgeClass(source) {
|
|
|
|
|
const s = String(source || "");
|
|
|
|
|
if (s.indexOf("对冲") >= 0) return "is-hedge";
|
|
|
|
|
if (s.indexOf("纯期权") >= 0 || s === "期权") return "is-opt";
|
|
|
|
|
if (s.indexOf("顺势") >= 0) return "is-roll";
|
|
|
|
|
if (s.indexOf("趋势") >= 0) return "is-trend";
|
|
|
|
|
if (s.indexOf("关键位") >= 0) return "is-key";
|
|
|
|
|
if (s.indexOf("下单") >= 0) return "is-order";
|
|
|
|
|
return "is-none";
|
|
|
|
|
function exchangeLinkCell(ac) {
|
|
|
|
|
const name = esc((ac && ac.name) || "—");
|
|
|
|
|
const exId = ac && ac.id != null ? String(ac.id) : "";
|
|
|
|
|
if (!exId) return name;
|
|
|
|
|
return (
|
|
|
|
|
`<button type="button" class="dash-ex-link" data-dash-ex-id="${esc(exId)}" ` +
|
|
|
|
|
`title="打开监控区放大查看">${name}</button>`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderDashboardPerpTable(lines) {
|
|
|
|
|
const rows = Array.isArray(lines) ? lines : [];
|
|
|
|
|
if (!rows.length) return "";
|
|
|
|
|
function priceCell(fmtVal, raw) {
|
|
|
|
|
if (fmtVal != null && String(fmtVal).trim() !== "") return esc(String(fmtVal));
|
|
|
|
|
if (raw != null && Number.isFinite(Number(raw))) return esc(fmt(raw, 4).replace(/\.?0+$/, ""));
|
|
|
|
|
return "—";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function floatPctText(pnl, notional) {
|
|
|
|
|
const n = Number(pnl);
|
|
|
|
|
const notion = Number(notional);
|
|
|
|
|
if (!Number.isFinite(n) || !Number.isFinite(notion) || Math.abs(notion) < 1e-8) return "—";
|
|
|
|
|
const pct = (n / Math.abs(notion)) * 100;
|
|
|
|
|
const abs = Math.abs(pct).toFixed(2);
|
|
|
|
|
if (Math.abs(pct) < 1e-9) return "0.00%";
|
|
|
|
|
return `${pct > 0 ? "+" : "-"}${abs}%`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function collectUnifiedPositions(accounts) {
|
|
|
|
|
const perp = [];
|
|
|
|
|
const options = [];
|
|
|
|
|
(Array.isArray(accounts) ? accounts : []).forEach((ac) => {
|
|
|
|
|
if (!accountHasOpenPositions(ac)) return;
|
|
|
|
|
accountPerpLines(ac).forEach((ln) => {
|
|
|
|
|
if (!ln) return;
|
|
|
|
|
perp.push({ ac: ac, ln: ln });
|
|
|
|
|
});
|
|
|
|
|
if (ac && ac.options_layout) {
|
|
|
|
|
(Array.isArray(ac.options_positions) ? ac.options_positions : []).forEach((p) => {
|
|
|
|
|
if (!p) return;
|
|
|
|
|
options.push({ ac: ac, p: p });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return { perp: perp, options: options };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderUnifiedPerpTable(rows) {
|
|
|
|
|
if (!rows.length) {
|
|
|
|
|
return `<div class="dash-pos-block">
|
|
|
|
|
<div class="dash-ac-section-label">永续持仓</div>
|
|
|
|
|
<div class="dash-empty dash-empty-inline">当前无永续持仓</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
const body = rows
|
|
|
|
|
.map((ln) => {
|
|
|
|
|
const source = String((ln && ln.source) || "—");
|
|
|
|
|
.map(({ ac, ln }) => {
|
|
|
|
|
const symbol = esc((ln && (ln.symbol || ln.text)) || "—");
|
|
|
|
|
const side = esc((ln && ln.side) || "—");
|
|
|
|
|
const contracts =
|
|
|
|
|
ln && ln.contracts != null && ln.contracts !== "" ? esc(String(ln.contracts)) : "—";
|
|
|
|
|
const pnl = ln && ln.pnl != null ? Number(ln.pnl) : NaN;
|
|
|
|
|
const pct = floatPctText(pnl, ln && ln.notional_usdt);
|
|
|
|
|
return `<tr>
|
|
|
|
|
<td><span class="dash-pos-source ${sourceBadgeClass(source)}">${esc(source)}</span></td>
|
|
|
|
|
<td>${exchangeLinkCell(ac)}</td>
|
|
|
|
|
<td>${symbol}</td>
|
|
|
|
|
<td>${side}</td>
|
|
|
|
|
<td>${priceCell(ln && ln.entry_price_fmt, ln && ln.entry_price)}</td>
|
|
|
|
|
<td>${priceCell(ln && ln.mark_price_fmt, ln && ln.mark_price)}</td>
|
|
|
|
|
<td>${contracts}</td>
|
|
|
|
|
<td class="${pnlClass(pnl)}">${Number.isFinite(pnl) ? pnlSigned(pnl, 2) : "—"}</td>
|
|
|
|
|
<td class="${pnlClass(pnl)}">${pct}</td>
|
|
|
|
|
</tr>`;
|
|
|
|
|
})
|
|
|
|
|
.join("");
|
|
|
|
@@ -166,7 +191,7 @@
|
|
|
|
|
<div class="dash-table-wrap">
|
|
|
|
|
<table class="dash-table dash-pos-table">
|
|
|
|
|
<thead><tr>
|
|
|
|
|
<th>来源</th><th>合约</th><th>方向</th><th>张数</th><th>浮盈</th>
|
|
|
|
|
<th>交易所</th><th>合约</th><th>方向</th><th>开仓价</th><th>标记价</th><th>张数</th><th>盈利金额</th><th>浮盈</th>
|
|
|
|
|
</tr></thead>
|
|
|
|
|
<tbody>${body}</tbody>
|
|
|
|
|
</table>
|
|
|
|
@@ -188,23 +213,26 @@
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderDashboardOptionsTable(positions) {
|
|
|
|
|
const pos = Array.isArray(positions) ? positions : [];
|
|
|
|
|
if (!pos.length) return "";
|
|
|
|
|
const rows = pos
|
|
|
|
|
.map((p) => {
|
|
|
|
|
function renderUnifiedOptionsTable(rows) {
|
|
|
|
|
if (!rows.length) {
|
|
|
|
|
return `<div class="dash-pos-block dash-options-block">
|
|
|
|
|
<div class="dash-ac-section-label">期权持仓</div>
|
|
|
|
|
<div class="dash-empty dash-empty-inline">当前无期权持仓</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
const body = rows
|
|
|
|
|
.map(({ ac, p }) => {
|
|
|
|
|
const optType =
|
|
|
|
|
(p.opt_type || "").toUpperCase() === "C"
|
|
|
|
|
? "Call"
|
|
|
|
|
: (p.opt_type || "").toUpperCase() === "P"
|
|
|
|
|
? "Put"
|
|
|
|
|
: p.opt_type || "—";
|
|
|
|
|
const source = String(p.source_label || p.source || "纯期权");
|
|
|
|
|
const target = String(p.target_monitor_text || "—");
|
|
|
|
|
const targetCls = target && target !== "—" ? "dash-target-monitor is-on" : "dash-target-monitor";
|
|
|
|
|
const net = optionsNetPnl(p);
|
|
|
|
|
return `<tr>
|
|
|
|
|
<td><span class="dash-pos-source ${sourceBadgeClass(source)}">${esc(source)}</span></td>
|
|
|
|
|
<td>${exchangeLinkCell(ac)}</td>
|
|
|
|
|
<td title="${esc(p.inst_id || "")}">${esc(shortDashInst(p.inst_id))}</td>
|
|
|
|
|
<td>${esc(optType)}</td>
|
|
|
|
|
<td>${dashOptionsExpiryCd(p.exp_time_ms != null ? p.exp_time_ms : p.exp_time)}</td>
|
|
|
|
@@ -219,33 +247,17 @@
|
|
|
|
|
<div class="dash-table-wrap dash-options-table-wrap">
|
|
|
|
|
<table class="dash-table dash-options-table">
|
|
|
|
|
<thead><tr>
|
|
|
|
|
<th>来源</th><th>合约</th><th>类型</th><th>到期倒计时</th><th>指数</th><th>目标监控</th><th>净盈亏</th>
|
|
|
|
|
<th>交易所</th><th>合约</th><th>类型</th><th>到期倒计时</th><th>指数</th><th>目标监控</th><th>净盈亏</th>
|
|
|
|
|
</tr></thead>
|
|
|
|
|
<tbody>${rows}</tbody>
|
|
|
|
|
<tbody>${body}</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderAccountPositions(ac) {
|
|
|
|
|
const perpLines = accountPerpLines(ac);
|
|
|
|
|
const optionsPositions = Array.isArray(ac && ac.options_positions) ? ac.options_positions : [];
|
|
|
|
|
const issues = Array.isArray(ac && ac.issues) ? ac.issues : [];
|
|
|
|
|
const chips = renderMonitorCountChips((ac && ac.monitor_counts) || {});
|
|
|
|
|
const monitorRow = chips.length
|
|
|
|
|
? `<div class="dash-ac-monitor-row">${chips.join("")}</div>`
|
|
|
|
|
: "";
|
|
|
|
|
const perpHtml = renderDashboardPerpTable(perpLines);
|
|
|
|
|
const optionsHtml = ac && ac.options_layout ? renderDashboardOptionsTable(optionsPositions) : "";
|
|
|
|
|
const issueHtml = issues
|
|
|
|
|
.map((text) => `<div class="dash-ac-remark-line dash-ac-remark-issue">${esc(text)}</div>`)
|
|
|
|
|
.join("");
|
|
|
|
|
return `${monitorRow}${perpHtml}${optionsHtml}${issueHtml}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bindDashboardExpand() {
|
|
|
|
|
if (!elAccounts) return;
|
|
|
|
|
elAccounts.querySelectorAll(".dash-ac-expand-btn").forEach((btn) => {
|
|
|
|
|
elAccounts.querySelectorAll("[data-dash-ex-id]").forEach((btn) => {
|
|
|
|
|
btn.addEventListener("click", (ev) => {
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
ev.stopPropagation();
|
|
|
|
@@ -256,44 +268,24 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderAccounts(accounts, threshold) {
|
|
|
|
|
const rows = (Array.isArray(accounts) ? accounts : []).filter(accountHasOpenPositions);
|
|
|
|
|
if (!rows.length) {
|
|
|
|
|
const unified = collectUnifiedPositions(accounts);
|
|
|
|
|
if (!unified.perp.length && !unified.options.length) {
|
|
|
|
|
elAccounts.innerHTML = '<div class="dash-empty">当前无持仓账户</div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
elAccounts.innerHTML = rows
|
|
|
|
|
.map((ac) => {
|
|
|
|
|
const alert = !!ac.loss_alert;
|
|
|
|
|
const unmon = !ac.monitored;
|
|
|
|
|
const lossPct = Number(ac.daily_loss_pct);
|
|
|
|
|
const barW =
|
|
|
|
|
alert && Number.isFinite(lossPct)
|
|
|
|
|
? Math.min(100, (lossPct / Math.max(threshold, 1)) * 100)
|
|
|
|
|
: 0;
|
|
|
|
|
const badge = alert
|
|
|
|
|
? `<span class="dash-ac-badge alert">单日亏损 ≥${threshold}%</span>`
|
|
|
|
|
: `<span class="dash-ac-badge ok">${esc(ac.status || "—")}</span>`;
|
|
|
|
|
const exId = ac && ac.id != null ? String(ac.id) : "";
|
|
|
|
|
const expandBtn = exId
|
|
|
|
|
? `<button type="button" class="dash-ac-expand-btn" data-dash-ex-id="${esc(exId)}" title="放大查看监控详情" aria-label="放大查看监控详情">` +
|
|
|
|
|
`<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M15 3h6v6h-2V6.41l-7.29 7.3-1.42-1.42 7.3-7.29H15V3zM3 9h2v10h10v2H3V9z"/></svg>` +
|
|
|
|
|
`</button>`
|
|
|
|
|
: "";
|
|
|
|
|
const lossBar =
|
|
|
|
|
alert && barW > 0
|
|
|
|
|
? `<div class="dash-loss-bar" title="占资金合计 ${fmt(lossPct, 2)}%"><i style="width:${barW}%"></i></div>`
|
|
|
|
|
: "";
|
|
|
|
|
const cardCls = ac.options_layout ? " dash-ac-card-options" : "";
|
|
|
|
|
return `<article class="dash-ac-card dash-ac-card-pos-only${cardCls}${alert ? " is-alert" : ""}${unmon ? " is-unmon" : ""}">
|
|
|
|
|
<div class="dash-ac-top">
|
|
|
|
|
<div class="dash-ac-name">${esc(ac.name || "—")}</div>
|
|
|
|
|
<div class="dash-ac-top-actions">${badge}${expandBtn}</div>
|
|
|
|
|
</div>
|
|
|
|
|
${lossBar}
|
|
|
|
|
<div class="dash-ac-pos-body">${renderAccountPositions(ac)}</div>
|
|
|
|
|
</article>`;
|
|
|
|
|
})
|
|
|
|
|
.join("");
|
|
|
|
|
const alertAccounts = (Array.isArray(accounts) ? accounts : []).filter((a) => a && a.loss_alert);
|
|
|
|
|
const alertNote = alertAccounts.length
|
|
|
|
|
? `<div class="dash-pos-alert-note">风险: ${esc(
|
|
|
|
|
alertAccounts.map((a) => a.name || "").filter(Boolean).join("、")
|
|
|
|
|
)} 单日亏损 ≥${threshold}%</div>`
|
|
|
|
|
: "";
|
|
|
|
|
elAccounts.innerHTML = `<article class="dash-ac-card dash-pos-unified-card">
|
|
|
|
|
<div class="dash-ac-pos-body">
|
|
|
|
|
${renderUnifiedPerpTable(unified.perp)}
|
|
|
|
|
${renderUnifiedOptionsTable(unified.options)}
|
|
|
|
|
${alertNote}
|
|
|
|
|
</div>
|
|
|
|
|
</article>`;
|
|
|
|
|
bindDashboardExpand();
|
|
|
|
|
if (window.OptionsExpiryCountdown && OptionsExpiryCountdown.ensureTimer) {
|
|
|
|
|
OptionsExpiryCountdown.ensureTimer();
|
|
|
|
|