diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 2d26f1f..ed6adec 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -7024,6 +7024,9 @@ def api_price_snapshot(): symbol=r["symbol"], ) apply_time_close_to_payload(payload, r) + payload["opened_at"] = r["opened_at"] if "opened_at" in r.keys() else None + open_ms = r["opened_at_ms"] if "opened_at_ms" in r.keys() else None + payload["opened_at_ms"] = int(open_ms) if open_ms not in (None, "") else None new_sl, new_tp, changed = order_monitor_tpsl_needs_sync( r["stop_loss"], r["take_profit"], exchange_tpsl ) diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 4f642d1..b2dca61 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -467,6 +467,8 @@ 计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U 杠杆: {{ o.leverage or '-' }}x 仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}% + 开仓时间: {{ (o.opened_at or '-')[:16] }} + 持仓时长:
交易所止盈止损
@@ -2194,10 +2196,41 @@ function refreshPriceSnapshotConditional(){ paintExchangeTpslRow(o.id, o.exchange_tpsl || {}); paintPlanTpslDisplay(o.id, o); if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(o); + const holdEl = document.getElementById(`order-hold-duration-${o.id}`); + if(holdEl && o.opened_at_ms != null && o.opened_at_ms !== ""){ + holdEl.setAttribute("data-order-opened-ms", String(o.opened_at_ms)); + } }); + tickOrderHoldDurations(); } }).catch(()=>{}); } +function formatLiveHoldDurationFromMs(openedMs, nowMs){ + if(openedMs == null || openedMs === "" || !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(""); +} +function tickOrderHoldDurations(){ + const now = Date.now(); + document.querySelectorAll(".order-hold-duration[data-order-opened-ms]").forEach(el=>{ + const ms = Number(el.getAttribute("data-order-opened-ms")); + if(!Number.isFinite(ms) || ms <= 0) return; + el.textContent = formatLiveHoldDurationFromMs(ms, now); + }); +} +setInterval(tickOrderHoldDurations, 1000); +tickOrderHoldDurations(); setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }}); diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 9aa9072..e4ce29e 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -7172,6 +7172,9 @@ def api_price_snapshot(): symbol=r["symbol"], ) apply_time_close_to_payload(payload, r) + payload["opened_at"] = r["opened_at"] if "opened_at" in r.keys() else None + open_ms = r["opened_at_ms"] if "opened_at_ms" in r.keys() else None + payload["opened_at_ms"] = int(open_ms) if open_ms not in (None, "") else None new_sl, new_tp, changed = order_monitor_tpsl_needs_sync( r["stop_loss"], r["take_profit"], exchange_tpsl ) diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index 95af7a1..a3def8c 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -447,6 +447,8 @@ 计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U 杠杆: {{ o.leverage or '-' }}x 仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}% + 开仓时间: {{ (o.opened_at or '-')[:16] }} + 持仓时长:
交易所止盈止损
@@ -2174,10 +2176,41 @@ function refreshPriceSnapshotConditional(){ paintExchangeTpslRow(o.id, o.exchange_tpsl || {}); paintPlanTpslDisplay(o.id, o); if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(o); + const holdEl = document.getElementById(`order-hold-duration-${o.id}`); + if(holdEl && o.opened_at_ms != null && o.opened_at_ms !== ""){ + holdEl.setAttribute("data-order-opened-ms", String(o.opened_at_ms)); + } }); + tickOrderHoldDurations(); } }).catch(()=>{}); } +function formatLiveHoldDurationFromMs(openedMs, nowMs){ + if(openedMs == null || openedMs === "" || !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(""); +} +function tickOrderHoldDurations(){ + const now = Date.now(); + document.querySelectorAll(".order-hold-duration[data-order-opened-ms]").forEach(el=>{ + const ms = Number(el.getAttribute("data-order-opened-ms")); + if(!Number.isFinite(ms) || ms <= 0) return; + el.textContent = formatLiveHoldDurationFromMs(ms, now); + }); +} +setInterval(tickOrderHoldDurations, 1000); +tickOrderHoldDurations(); setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }}); diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 0d75a82..4ea6b65 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -7172,6 +7172,9 @@ def api_price_snapshot(): symbol=r["symbol"], ) apply_time_close_to_payload(payload, r) + payload["opened_at"] = r["opened_at"] if "opened_at" in r.keys() else None + open_ms = r["opened_at_ms"] if "opened_at_ms" in r.keys() else None + payload["opened_at_ms"] = int(open_ms) if open_ms not in (None, "") else None new_sl, new_tp, changed = order_monitor_tpsl_needs_sync( r["stop_loss"], r["take_profit"], exchange_tpsl ) diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index 95af7a1..a3def8c 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -447,6 +447,8 @@ 计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U 杠杆: {{ o.leverage or '-' }}x 仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}% + 开仓时间: {{ (o.opened_at or '-')[:16] }} + 持仓时长:
交易所止盈止损
@@ -2174,10 +2176,41 @@ function refreshPriceSnapshotConditional(){ paintExchangeTpslRow(o.id, o.exchange_tpsl || {}); paintPlanTpslDisplay(o.id, o); if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(o); + const holdEl = document.getElementById(`order-hold-duration-${o.id}`); + if(holdEl && o.opened_at_ms != null && o.opened_at_ms !== ""){ + holdEl.setAttribute("data-order-opened-ms", String(o.opened_at_ms)); + } }); + tickOrderHoldDurations(); } }).catch(()=>{}); } +function formatLiveHoldDurationFromMs(openedMs, nowMs){ + if(openedMs == null || openedMs === "" || !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(""); +} +function tickOrderHoldDurations(){ + const now = Date.now(); + document.querySelectorAll(".order-hold-duration[data-order-opened-ms]").forEach(el=>{ + const ms = Number(el.getAttribute("data-order-opened-ms")); + if(!Number.isFinite(ms) || ms <= 0) return; + el.textContent = formatLiveHoldDurationFromMs(ms, now); + }); +} +setInterval(tickOrderHoldDurations, 1000); +tickOrderHoldDurations(); setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }}); diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index e75df06..7827b56 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -6757,6 +6757,9 @@ def api_price_snapshot(): symbol=r["symbol"], ) apply_time_close_to_payload(payload, r) + payload["opened_at"] = r["opened_at"] if "opened_at" in r.keys() else None + open_ms = r["opened_at_ms"] if "opened_at_ms" in r.keys() else None + payload["opened_at_ms"] = int(open_ms) if open_ms not in (None, "") else None new_sl, new_tp, changed = order_monitor_tpsl_needs_sync( r["stop_loss"], r["take_profit"], exchange_tpsl ) diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index 9cbf097..35bda90 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -476,6 +476,8 @@ 计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U 杠杆: {{ o.leverage or '-' }}x 仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}% + 开仓时间: {{ (o.opened_at or '-')[:16] }} + 持仓时长:
交易所止盈止损
@@ -2227,10 +2229,41 @@ function refreshPriceSnapshotConditional(){ paintExchangeTpslRow(o.id, o.exchange_tpsl || {}); paintPlanTpslDisplay(o.id, o); if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(o); + const holdEl = document.getElementById(`order-hold-duration-${o.id}`); + if(holdEl && o.opened_at_ms != null && o.opened_at_ms !== ""){ + holdEl.setAttribute("data-order-opened-ms", String(o.opened_at_ms)); + } }); + tickOrderHoldDurations(); } }).catch(()=>{}); } +function formatLiveHoldDurationFromMs(openedMs, nowMs){ + if(openedMs == null || openedMs === "" || !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(""); +} +function tickOrderHoldDurations(){ + const now = Date.now(); + document.querySelectorAll(".order-hold-duration[data-order-opened-ms]").forEach(el=>{ + const ms = Number(el.getAttribute("data-order-opened-ms")); + if(!Number.isFinite(ms) || ms <= 0) return; + el.textContent = formatLiveHoldDurationFromMs(ms, now); + }); +} +setInterval(tickOrderHoldDurations, 1000); +tickOrderHoldDurations(); setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }}); diff --git a/hub_symbol_archive_lib.py b/hub_symbol_archive_lib.py index 92e470a..6861b8a 100644 --- a/hub_symbol_archive_lib.py +++ b/hub_symbol_archive_lib.py @@ -1418,7 +1418,7 @@ def list_daily_trades( search: str = "", db_path: Path | None = None, ) -> dict[str, Any]: - """按日期区间列出开仓记录(本日/本周/本月/自选),含犯病与盈亏统计。""" + """按日期区间列出平仓记录(本日/本周/本月/自选,以平仓时间计),含犯病与盈亏统计。""" init_db(db_path) p = (period or "today").strip().lower() or "today" start_ms, end_ms, df, dt, period_label = resolve_period_bounds( @@ -1431,7 +1431,7 @@ def list_daily_trades( conn = _connect(db_path) try: params: list[Any] = [start_ms, end_ms] - where = "opened_at_ms >= ? AND opened_at_ms < ?" + where = "closed_at_ms IS NOT NULL AND closed_at_ms >= ? AND closed_at_ms < ?" if ex_filter: where += " AND exchange_key=?" params.append(ex_filter) @@ -1439,7 +1439,7 @@ def list_daily_trades( f""" SELECT * FROM archive_trade_cache WHERE {where} - ORDER BY opened_at_ms DESC, trade_id DESC + ORDER BY closed_at_ms DESC, trade_id DESC """, params, ).fetchall() diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 8e169bd..ee238ff 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -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", diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index b16db80..f19db15 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -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 @@ }
交易所止盈止损