feat: add timed position close (1h/2h/4h) for key levels and live orders

Program monitors open positions and market-closes at deadline; UI shows label and countdown on instance and hub boards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-11 19:30:16 +08:00
parent 879ea5e228
commit 959593cdab
17 changed files with 1152 additions and 69 deletions
+6
View File
@@ -1359,6 +1359,12 @@ def _merge_flask_order_price_fields(hub_mon: dict | None, snap: dict | None) ->
"stop_loss_display",
"take_profit_display",
"display_rr_ratio",
"time_close_enabled",
"time_close_hours",
"time_close_at_ms",
"time_close_label",
"time_close_countdown",
"time_close_remaining_sec",
):
if key in op and op[key] not in (None, ""):
o[key] = op[key]
+9
View File
@@ -2304,6 +2304,15 @@
meta.push(
`<span class="${beOn ? "pos-meta-on" : "pos-meta-off"}">移动保本:${beOn ? "开" : "关"}</span>`
);
if (mo.time_close_enabled) {
const tcLabel = mo.time_close_label || `时间平仓 ${mo.time_close_hours || ""}h`;
const tcCd = mo.time_close_countdown || "--:--:--";
const tcAt = mo.time_close_at_ms != null ? String(mo.time_close_at_ms) : "";
meta.push(
`<span class="pos-meta-item pos-meta-on pos-time-close-meta" data-close-at-ms="${esc(tcAt)}">` +
`${esc(tcLabel)} · 倒计时 <span class="pos-time-close-cd">${esc(tcCd)}</span></span>`
);
}
} else {
meta.push("来源: 交易所持仓");
meta.push("风格: —");
+2 -1
View File
@@ -588,6 +588,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/app.js?v=20260612-monitor-desktop-layout"></script>
<script src="/assets/time_close_ui.js?v=1"></script>
<script src="/assets/app.js?v=20260612-time-close"></script>
</body>
</html>
@@ -0,0 +1,93 @@
/**
* 时间平仓表单开关 + 持仓倒计时
*/
(function (global) {
"use strict";
function pad2(n) {
return n < 10 ? "0" + n : String(n);
}
function formatCountdown(sec) {
const s = Math.max(0, parseInt(sec, 10) || 0);
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const r = s % 60;
return pad2(h) + ":" + pad2(m) + ":" + pad2(r);
}
function bindTimeCloseForm(checkboxId, selectId, wrapId) {
const cb = document.getElementById(checkboxId);
const sel = document.getElementById(selectId);
const wrap = wrapId ? document.getElementById(wrapId) : null;
if (!cb || !sel) return;
function sync() {
const on = !!cb.checked;
sel.disabled = !on;
if (wrap) wrap.classList.toggle("is-disabled", !on);
}
cb.addEventListener("change", sync);
sync();
}
function paintOrderTimeClose(order) {
if (!order || order.id == null) return;
const wrap = document.getElementById("order-time-close-wrap-" + order.id);
const cd = document.getElementById("order-time-close-cd-" + order.id);
if (!wrap || !cd) return;
const enabled = !!(order.time_close_enabled || order.time_close_at_ms);
if (!enabled) {
wrap.style.display = "none";
return;
}
wrap.style.display = "";
const hours = order.time_close_hours;
const label = order.time_close_label || (hours ? "时间平仓 " + hours + "h" : "时间平仓");
const labelEl = wrap.querySelector(".pos-time-close-label");
if (labelEl) labelEl.textContent = label;
let rem =
order.time_close_remaining_sec != null
? Number(order.time_close_remaining_sec)
: null;
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) : "--:--:--";
wrap.dataset.closeAtMs = order.time_close_at_ms ? String(order.time_close_at_ms) : "";
}
function tickLocalCountdowns() {
document.querySelectorAll("[data-close-at-ms]").forEach(function (wrap) {
const closeAtRaw = wrap.dataset.closeAtMs || wrap.getAttribute("data-close-at-ms") || "";
const cd = wrap.querySelector(".pos-time-close-cd");
if (!cd) return;
const closeAt = Number(closeAtRaw);
if (!closeAt) return;
const rem = Math.max(0, Math.floor((closeAt - Date.now()) / 1000));
cd.textContent = formatCountdown(rem);
});
}
function paintOrders(orders) {
(orders || []).forEach(paintOrderTimeClose);
}
function syncKeyTimeCloseVisibility(show) {
const wrap = document.getElementById("key-time-close-wrap");
if (!wrap) return;
wrap.style.display = show ? "inline-flex" : "none";
}
global.TimeCloseUI = {
bindTimeCloseForm: bindTimeCloseForm,
paintOrderTimeClose: paintOrderTimeClose,
paintOrders: paintOrders,
tickLocalCountdowns: tickLocalCountdowns,
syncKeyTimeCloseVisibility: syncKeyTimeCloseVisibility,
formatCountdown: formatCountdown,
};
if (!global.__timeCloseCountdownTimer) {
global.__timeCloseCountdownTimer = setInterval(tickLocalCountdowns, 1000);
}
})(typeof window !== "undefined" ? window : globalThis);