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:
@@ -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]
|
||||
|
||||
@@ -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("风格: —");
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user