feat: archive trades by close time and show open time on live positions

Sort inner-archive daily trades by closed_at_ms; add open time and live hold duration to instance and hub position cards, with exchange margin on hub footer.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 06:20:18 +08:00
parent ad1c08a2cc
commit 869728ce10
11 changed files with 230 additions and 7 deletions
+2
View File
@@ -1359,6 +1359,8 @@ def _merge_flask_order_price_fields(hub_mon: dict | None, snap: dict | None) ->
"stop_loss_display",
"take_profit_display",
"display_rr_ratio",
"exchange_initial_margin",
"plan_margin",
"time_close_enabled",
"time_close_hours",
"time_close_at_ms",
+81 -4
View File
@@ -611,11 +611,13 @@
}
function resolveTrendSizingFooter(mo, trendPlan, isTrend) {
const m = mo || {};
if (!isTrend || !trendPlan || !trendPlan.id) {
return {
leverage: mo.leverage,
planBase: mo.margin_capital,
positionRatio: mo.position_ratio,
margin: m.exchange_initial_margin ?? m.plan_margin ?? null,
leverage: m.leverage,
planBase: m.margin_capital,
positionRatio: m.position_ratio,
};
}
const base =
@@ -623,12 +625,73 @@
? trendPlan.snapshot_available_usdt
: trendPlan.plan_margin_capital;
return {
margin: m.exchange_initial_margin ?? trendPlan.plan_margin_capital ?? null,
leverage: trendPlan.leverage,
planBase: base,
positionRatio: resolveTrendPositionRatioPct(trendPlan),
};
}
function resolvePositionOpenMeta(mo, trendPlan, isTrend) {
const useTrend = isTrend && trendPlan && trendPlan.id;
const src = useTrend ? trendPlan : mo || {};
let ms = Number(src.opened_at_ms);
if (!Number.isFinite(ms) || ms <= 0) {
const s = String(src.opened_at || "").trim();
if (s) {
const parsed = Date.parse(s.replace(" ", "T"));
ms = Number.isFinite(parsed) ? parsed : null;
} else {
ms = null;
}
} else {
ms = Math.round(ms);
}
let display = "—";
if (src.opened_at) {
display = String(src.opened_at).replace("T", " ").slice(0, 16);
} else if (ms) {
display = new Date(ms).toISOString().slice(0, 16).replace("T", " ");
}
return { openedAtMs: ms, openedAtDisplay: display };
}
function formatLiveHoldDuration(openedMs, nowMs) {
if (openedMs == null || !Number.isFinite(Number(openedMs))) return "—";
const ms = Number(openedMs);
const now = nowMs != null ? nowMs : Date.now();
let sec = Math.floor((now - ms) / 1000);
if (sec < 0) sec = 0;
if (sec <= 0) return "0分钟";
const d = Math.floor(sec / 86400);
sec %= 86400;
const h = Math.floor(sec / 3600);
sec %= 3600;
const m = Math.floor(sec / 60);
const parts = [];
if (d) parts.push(`${d}`);
if (h) parts.push(`${h}小时`);
if (m || !parts.length) parts.push(`${m}分钟`);
return parts.join("");
}
let hubHoldDurationTimer = null;
function tickHubHoldDurations() {
const now = Date.now();
document.querySelectorAll(".pos-hold-duration[data-opened-ms]").forEach((el) => {
const ms = Number(el.getAttribute("data-opened-ms"));
if (!Number.isFinite(ms) || ms <= 0) return;
el.textContent = formatLiveHoldDuration(ms, now);
});
}
function ensureHubHoldDurationTimer() {
tickHubHoldDurations();
if (hubHoldDurationTimer) return;
hubHoldDurationTimer = setInterval(tickHubHoldDurations, 1000);
}
function formatMonitorRiskMeta(mo, trendPlan) {
const m = mo || {};
const t = trendPlan || {};
@@ -1755,6 +1818,7 @@
if (window.TimeCloseUI && TimeCloseUI.tickLocalCountdowns) {
TimeCloseUI.tickLocalCountdowns();
}
ensureHubHoldDurationTimer();
if (expandedExchangeId && fs && fsInner) {
const row = rows.find((r) => String(r.id) === String(expandedExchangeId));
@@ -1768,6 +1832,7 @@
if (window.TimeCloseUI && TimeCloseUI.tickLocalCountdowns) {
TimeCloseUI.tickLocalCountdowns();
}
ensureHubHoldDurationTimer();
fsInner.querySelectorAll(".btn-expand-back").forEach((btn) => {
btn.onclick = (ev) => {
ev.stopPropagation();
@@ -2548,6 +2613,15 @@
const pnlFmt = formatFloatingPnlText(upnl, pos.notional_usdt);
const pnlText = pnlFmt.text;
const sizingFoot = resolveTrendSizingFooter(mo, trendPlan, isTrend);
const openMeta = resolvePositionOpenMeta(mo, trendPlan, isTrend);
const marginText =
sizingFoot.margin != null && sizingFoot.margin !== "" && Number.isFinite(Number(sizingFoot.margin))
? fmt(Number(sizingFoot.margin), 2) + "U"
: "—";
const holdMsAttr =
openMeta.openedAtMs != null && Number.isFinite(openMeta.openedAtMs)
? String(openMeta.openedAtMs)
: "";
const markDisplay = isTrend
? resolveTrendMarkPrice(pos, trendPlan, symbol, tickMap)
: fmtMarkPrice(pos, tickMap);
@@ -2612,9 +2686,12 @@
}
</div>
<div class="pos-footer">
<span>杠杆: ${sizingFoot.leverage != null && sizingFoot.leverage !== "" ? esc(sizingFoot.leverage) + "x" : "—"}</span>
<span>保证金: ${marginText}</span>
<span>计划基数: ${sizingFoot.planBase != null && sizingFoot.planBase !== "" ? fmt(sizingFoot.planBase, 2) + "U" : "—"}</span>
<span>杠杆: ${sizingFoot.leverage != null && sizingFoot.leverage !== "" ? esc(sizingFoot.leverage) + "x" : "—"}</span>
<span>仓位占比: ${sizingFoot.positionRatio != null && sizingFoot.positionRatio !== "" ? fmt(sizingFoot.positionRatio, 2) + "%" : "—"}</span>
<span>开仓时间: ${esc(openMeta.openedAtDisplay)}</span>
<span>持仓时长: <span class="pos-hold-duration" data-opened-ms="${esc(holdMsAttr)}">${esc(formatLiveHoldDuration(openMeta.openedAtMs))}</span></span>
</div>
<div class="pos-ex-orders">
<div class="pos-ex-orders-title">交易所止盈止损</div>