feat: show force-close badge and countdown when FORCE_CLOSE is enabled
Add shared lib/UI for midnight force-close indicator on instance and hub pages, and document Gate intraday 0-point exit alignment. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1784,6 +1784,13 @@ _ORDER_PRICE_MERGE_KEYS = (
|
||||
"time_close_label",
|
||||
"time_close_countdown",
|
||||
"time_close_remaining_sec",
|
||||
"force_close_enabled",
|
||||
"force_close_bj_hour",
|
||||
"force_close_at_ms",
|
||||
"force_close_label",
|
||||
"force_close_countdown",
|
||||
"force_close_remaining_sec",
|
||||
"force_close_active",
|
||||
)
|
||||
|
||||
|
||||
@@ -2141,6 +2148,7 @@ async def _assemble_board_row(
|
||||
"available_trading_usdt": account.get("available_trading_usdt") if acct_ok else None,
|
||||
"account_ok": acct_ok,
|
||||
"day_stats": _day_stats_from_trades_body(trades_today),
|
||||
"force_close": snap.get("force_close") if isinstance(snap, dict) else None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1605,6 +1605,27 @@ body.market-chart-fs-open {
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.hub-pos-card .pos-symbol-force-close,
|
||||
.hub-mini-title .pos-symbol-force-close,
|
||||
.td-symbol .pos-symbol-force-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
color: #ffc870;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 200, 112, 0.12);
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.hub-pos-card .pos-symbol-force-close .pos-force-close-cd,
|
||||
.hub-mini-title .pos-symbol-force-close .pos-force-close-cd,
|
||||
.td-symbol .pos-symbol-force-close .pos-force-close-cd {
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.hub-pos-card .pos-card-symbol strong {
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
|
||||
@@ -2792,6 +2792,18 @@
|
||||
);
|
||||
}
|
||||
|
||||
function forceCloseSymbolBadgeHtml(item) {
|
||||
if (!item || !item.force_close_enabled) return "";
|
||||
const fcLabel = item.force_close_label || "强制清仓";
|
||||
const fcCd = item.force_close_countdown || "--:--:--";
|
||||
const fcAt = item.force_close_at_ms != null ? String(item.force_close_at_ms) : "";
|
||||
const fcActive = item.force_close_active ? "1" : "0";
|
||||
return (
|
||||
`<span class="pos-symbol-force-close pos-force-close-meta" data-force-close-at-ms="${esc(fcAt)}" data-force-close-active="${esc(fcActive)}">` +
|
||||
`${esc(fcLabel)} · <span class="pos-force-close-cd">${esc(fcCd)}</span></span>`
|
||||
);
|
||||
}
|
||||
|
||||
function renderTrendDcaTable(t, tickMap) {
|
||||
const levels = resolveTrendDcaLevels(t);
|
||||
if (!levels.length) return "";
|
||||
@@ -3043,11 +3055,12 @@
|
||||
}
|
||||
const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : "";
|
||||
const tcSymBadge = !isTrend && mo.time_close_enabled ? timeCloseSymbolBadgeHtml(mo) : "";
|
||||
const fcSymBadge = !isTrend && mo.force_close_enabled ? forceCloseSymbolBadgeHtml(mo) : "";
|
||||
const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan);
|
||||
return `<div class="pos-card hub-pos-card">
|
||||
<div class="pos-card-head">
|
||||
<div class="pos-card-symbol">
|
||||
<button type="button" class="btn-open-market sym-link pos-symbol-link" ${mktAttrs} title="打开行情区(含入场/止盈止损)"><strong>${esc(symbol)}</strong></button>${tcSymBadge}${symBeBadge}
|
||||
<button type="button" class="btn-open-market sym-link pos-symbol-link" ${mktAttrs} title="打开行情区(含入场/止盈止损)"><strong>${esc(symbol)}</strong></button>${tcSymBadge}${fcSymBadge}${symBeBadge}
|
||||
<span class="pos-side-badge ${sideCls}">${sideCn}</span>
|
||||
</div>
|
||||
<div class="pos-head-actions">
|
||||
@@ -3132,8 +3145,9 @@
|
||||
.map((o) => {
|
||||
const sym = o.exchange_symbol || o.symbol || "";
|
||||
const tcBadge = o.time_close_enabled ? timeCloseSymbolBadgeHtml(o) : "";
|
||||
const fcBadge = o.force_close_enabled ? forceCloseSymbolBadgeHtml(o) : "";
|
||||
return `<div class="hub-mini-card">
|
||||
<div class="hub-mini-title">#${esc(o.id)} · ${esc(o.symbol || o.exchange_symbol)} ${tcBadge} · ${renderDirectionHtml(o.direction)}</div>
|
||||
<div class="hub-mini-title">#${esc(o.id)} · ${esc(o.symbol || o.exchange_symbol)} ${tcBadge}${fcBadge} · ${renderDirectionHtml(o.direction)}</div>
|
||||
<div class="hub-mini-line">触发 ${fmtSymbolPrice(o.trigger_price, sym, tickMap)} · SL ${fmtSymbolPrice(o.stop_loss, sym, tickMap)} · TP ${fmtSymbolPrice(o.take_profit, sym, tickMap)} · ${esc(o.trade_style || o.monitor_type || "下单监控")}</div>
|
||||
</div>`;
|
||||
})
|
||||
@@ -3202,6 +3216,8 @@
|
||||
const mo = monitorOrder || {};
|
||||
const tcBadge =
|
||||
!isTrendContext(mo, trendPlan) && mo.time_close_enabled ? timeCloseSymbolBadgeHtml(mo) : "";
|
||||
const fcBadge =
|
||||
!isTrendContext(mo, trendPlan) && mo.force_close_enabled ? forceCloseSymbolBadgeHtml(mo) : "";
|
||||
const actionCell = `<div class="pos-action-group">
|
||||
<button type="button" class="btn-place-tpsl btn-sm 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 btn-sm danger" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button>
|
||||
@@ -3210,7 +3226,7 @@
|
||||
? `<td class="${pnlCls(x.unrealized_pnl)}">${fmt(x.unrealized_pnl, 2)}</td>`
|
||||
: "";
|
||||
return `<tr>
|
||||
<td class="td-symbol"><button type="button" class="btn-open-market sym-link" ${mktAttrs} title="打开行情区(含入场/止盈止损)">${esc(x.symbol)}</button>${tcBadge}${symBeBadge}</td>
|
||||
<td class="td-symbol"><button type="button" class="btn-open-market sym-link" ${mktAttrs} title="打开行情区(含入场/止盈止损)">${esc(x.symbol)}</button>${tcBadge}${fcBadge}${symBeBadge}</td>
|
||||
<td class="${sideDirCls(x.side)}">${renderDirectionHtml(x.side)}</td>
|
||||
<td class="td-entry">${fmtEntryPrice(x, tickMap)}</td>
|
||||
<td>${fmtMarkPrice(x, tickMap)}</td>
|
||||
|
||||
@@ -1114,7 +1114,7 @@
|
||||
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
|
||||
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
|
||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||
<script src="/assets/time_close_ui.js?v=2"></script>
|
||||
<script src="/assets/time_close_ui.js?v=3"></script>
|
||||
<script src="/assets/backup.js?v=1"></script>
|
||||
<script src="/assets/app.js?v=20260704-monitor-stats-v2"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 时间平仓:表单开关 + 持仓倒计时。
|
||||
* 时间平仓 + 整点强制清仓:表单开关 + 持仓/顶栏倒计时。
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
@@ -16,6 +16,15 @@
|
||||
return pad2(h) + ":" + pad2(m) + ":" + pad2(r);
|
||||
}
|
||||
|
||||
function isForceCloseActive(wrap) {
|
||||
if (!wrap) return false;
|
||||
const raw =
|
||||
wrap.dataset.forceCloseActive ||
|
||||
wrap.getAttribute("data-force-close-active") ||
|
||||
"";
|
||||
return raw === "1" || raw === "true";
|
||||
}
|
||||
|
||||
function bindTimeCloseForm(checkboxId, selectId, wrapId) {
|
||||
const cb = document.getElementById(checkboxId);
|
||||
const sel = document.getElementById(selectId);
|
||||
@@ -37,6 +46,15 @@
|
||||
sync();
|
||||
}
|
||||
|
||||
function paintCountdownEl(cd, rem, active) {
|
||||
if (!cd) return;
|
||||
if (active) {
|
||||
cd.textContent = "执行中";
|
||||
return;
|
||||
}
|
||||
cd.textContent = Number.isFinite(rem) ? formatCountdown(rem) : "--:--:--";
|
||||
}
|
||||
|
||||
function paintOrderTimeClose(order) {
|
||||
if (!order || order.id == null) return;
|
||||
const wrap = document.getElementById("order-time-close-wrap-" + order.id);
|
||||
@@ -59,10 +77,71 @@
|
||||
if ((rem == null || !Number.isFinite(rem)) && order.time_close_at_ms) {
|
||||
rem = Math.max(0, Math.floor((Number(order.time_close_at_ms) - Date.now()) / 1000));
|
||||
}
|
||||
cd.textContent = Number.isFinite(rem) ? formatCountdown(rem) : "--:--:--";
|
||||
paintCountdownEl(cd, rem, false);
|
||||
wrap.dataset.closeAtMs = order.time_close_at_ms ? String(order.time_close_at_ms) : "";
|
||||
}
|
||||
|
||||
function paintOrderForceClose(order) {
|
||||
if (!order || order.id == null) return;
|
||||
const wrap = document.getElementById("order-force-close-wrap-" + order.id);
|
||||
const cd = document.getElementById("order-force-close-cd-" + order.id);
|
||||
if (!wrap || !cd) return;
|
||||
const enabled = !!order.force_close_enabled;
|
||||
if (!enabled) {
|
||||
wrap.style.display = "none";
|
||||
return;
|
||||
}
|
||||
wrap.style.display = "";
|
||||
const label = order.force_close_label || "强制清仓";
|
||||
const labelEl = wrap.querySelector(".pos-force-close-label");
|
||||
if (labelEl) labelEl.textContent = label;
|
||||
let rem =
|
||||
order.force_close_remaining_sec != null
|
||||
? Number(order.force_close_remaining_sec)
|
||||
: null;
|
||||
const atMs = order.force_close_at_ms;
|
||||
if ((rem == null || !Number.isFinite(rem)) && atMs) {
|
||||
rem = Math.max(0, Math.floor((Number(atMs) - Date.now()) / 1000));
|
||||
}
|
||||
const active = !!order.force_close_active;
|
||||
paintCountdownEl(cd, rem, active);
|
||||
wrap.dataset.forceCloseAtMs = atMs ? String(atMs) : "";
|
||||
wrap.dataset.forceCloseActive = active ? "1" : "0";
|
||||
}
|
||||
|
||||
function paintForceCloseHeader(state) {
|
||||
const wrap = document.getElementById("force-close-header-badge");
|
||||
if (!wrap) return;
|
||||
if (!state || !state.enabled) {
|
||||
wrap.style.display = "none";
|
||||
return;
|
||||
}
|
||||
wrap.style.display = "";
|
||||
const label = state.label || "强制清仓";
|
||||
const labelPrefix = label + " 已开启 · ";
|
||||
let prefixNode = wrap.querySelector(".force-close-header-prefix");
|
||||
if (!prefixNode) {
|
||||
wrap.textContent = "";
|
||||
prefixNode = document.createElement("span");
|
||||
prefixNode.className = "force-close-header-prefix";
|
||||
prefixNode.textContent = labelPrefix;
|
||||
wrap.appendChild(prefixNode);
|
||||
const cd = document.createElement("span");
|
||||
cd.className = "force-close-header-cd";
|
||||
wrap.appendChild(cd);
|
||||
} else {
|
||||
prefixNode.textContent = labelPrefix;
|
||||
}
|
||||
const cd = wrap.querySelector(".force-close-header-cd");
|
||||
let rem = state.remaining_sec != null ? Number(state.remaining_sec) : null;
|
||||
if ((rem == null || !Number.isFinite(rem)) && state.next_at_ms) {
|
||||
rem = Math.max(0, Math.floor((Number(state.next_at_ms) - Date.now()) / 1000));
|
||||
}
|
||||
paintCountdownEl(cd, rem, !!state.active);
|
||||
wrap.dataset.forceCloseAtMs = state.next_at_ms ? String(state.next_at_ms) : "";
|
||||
wrap.dataset.forceCloseActive = state.active ? "1" : "0";
|
||||
}
|
||||
|
||||
function tickLocalCountdowns() {
|
||||
document.querySelectorAll("[data-close-at-ms]").forEach(function (wrap) {
|
||||
const closeAtRaw = wrap.dataset.closeAtMs || wrap.getAttribute("data-close-at-ms") || "";
|
||||
@@ -73,10 +152,23 @@
|
||||
const rem = Math.max(0, Math.floor((closeAt - Date.now()) / 1000));
|
||||
cd.textContent = formatCountdown(rem);
|
||||
});
|
||||
document.querySelectorAll("[data-force-close-at-ms]").forEach(function (wrap) {
|
||||
const closeAtRaw =
|
||||
wrap.dataset.forceCloseAtMs || wrap.getAttribute("data-force-close-at-ms") || "";
|
||||
const cd = wrap.querySelector(".pos-force-close-cd, .force-close-header-cd");
|
||||
if (!cd) return;
|
||||
const closeAt = Number(closeAtRaw);
|
||||
if (!closeAt) return;
|
||||
const rem = Math.max(0, Math.floor((closeAt - Date.now()) / 1000));
|
||||
paintCountdownEl(cd, rem, isForceCloseActive(wrap));
|
||||
});
|
||||
}
|
||||
|
||||
function paintOrders(orders) {
|
||||
(orders || []).forEach(paintOrderTimeClose);
|
||||
(orders || []).forEach(function (order) {
|
||||
paintOrderTimeClose(order);
|
||||
paintOrderForceClose(order);
|
||||
});
|
||||
}
|
||||
|
||||
function syncKeyTimeCloseVisibility(show) {
|
||||
@@ -88,6 +180,8 @@
|
||||
global.TimeCloseUI = {
|
||||
bindTimeCloseForm: bindTimeCloseForm,
|
||||
paintOrderTimeClose: paintOrderTimeClose,
|
||||
paintOrderForceClose: paintOrderForceClose,
|
||||
paintForceCloseHeader: paintForceCloseHeader,
|
||||
paintOrders: paintOrders,
|
||||
tickLocalCountdowns: tickLocalCountdowns,
|
||||
syncKeyTimeCloseVisibility: syncKeyTimeCloseVisibility,
|
||||
|
||||
Reference in New Issue
Block a user