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:
@@ -7024,6 +7024,9 @@ def api_price_snapshot():
|
|||||||
symbol=r["symbol"],
|
symbol=r["symbol"],
|
||||||
)
|
)
|
||||||
apply_time_close_to_payload(payload, r)
|
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(
|
new_sl, new_tp, changed = order_monitor_tpsl_needs_sync(
|
||||||
r["stop_loss"], r["take_profit"], exchange_tpsl
|
r["stop_loss"], r["take_profit"], exchange_tpsl
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -467,6 +467,8 @@
|
|||||||
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
|
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
|
||||||
<span>杠杆: {{ o.leverage or '-' }}x</span>
|
<span>杠杆: {{ o.leverage or '-' }}x</span>
|
||||||
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
|
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
|
||||||
|
<span>开仓时间: {{ (o.opened_at or '-')[:16] }}</span>
|
||||||
|
<span>持仓时长: <span class="order-hold-duration" id="order-hold-duration-{{ o.id }}" data-order-opened-ms="{{ o.opened_at_ms or '' }}">—</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-ex-orders">
|
<div class="pos-ex-orders">
|
||||||
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
||||||
@@ -2194,10 +2196,41 @@ function refreshPriceSnapshotConditional(){
|
|||||||
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
||||||
paintPlanTpslDisplay(o.id, o);
|
paintPlanTpslDisplay(o.id, o);
|
||||||
if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(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(()=>{});
|
}).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 }});
|
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -7172,6 +7172,9 @@ def api_price_snapshot():
|
|||||||
symbol=r["symbol"],
|
symbol=r["symbol"],
|
||||||
)
|
)
|
||||||
apply_time_close_to_payload(payload, r)
|
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(
|
new_sl, new_tp, changed = order_monitor_tpsl_needs_sync(
|
||||||
r["stop_loss"], r["take_profit"], exchange_tpsl
|
r["stop_loss"], r["take_profit"], exchange_tpsl
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -447,6 +447,8 @@
|
|||||||
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
|
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
|
||||||
<span>杠杆: {{ o.leverage or '-' }}x</span>
|
<span>杠杆: {{ o.leverage or '-' }}x</span>
|
||||||
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
|
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
|
||||||
|
<span>开仓时间: {{ (o.opened_at or '-')[:16] }}</span>
|
||||||
|
<span>持仓时长: <span class="order-hold-duration" id="order-hold-duration-{{ o.id }}" data-order-opened-ms="{{ o.opened_at_ms or '' }}">—</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-ex-orders">
|
<div class="pos-ex-orders">
|
||||||
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
||||||
@@ -2174,10 +2176,41 @@ function refreshPriceSnapshotConditional(){
|
|||||||
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
||||||
paintPlanTpslDisplay(o.id, o);
|
paintPlanTpslDisplay(o.id, o);
|
||||||
if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(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(()=>{});
|
}).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 }});
|
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -7172,6 +7172,9 @@ def api_price_snapshot():
|
|||||||
symbol=r["symbol"],
|
symbol=r["symbol"],
|
||||||
)
|
)
|
||||||
apply_time_close_to_payload(payload, r)
|
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(
|
new_sl, new_tp, changed = order_monitor_tpsl_needs_sync(
|
||||||
r["stop_loss"], r["take_profit"], exchange_tpsl
|
r["stop_loss"], r["take_profit"], exchange_tpsl
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -447,6 +447,8 @@
|
|||||||
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
|
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
|
||||||
<span>杠杆: {{ o.leverage or '-' }}x</span>
|
<span>杠杆: {{ o.leverage or '-' }}x</span>
|
||||||
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
|
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
|
||||||
|
<span>开仓时间: {{ (o.opened_at or '-')[:16] }}</span>
|
||||||
|
<span>持仓时长: <span class="order-hold-duration" id="order-hold-duration-{{ o.id }}" data-order-opened-ms="{{ o.opened_at_ms or '' }}">—</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-ex-orders">
|
<div class="pos-ex-orders">
|
||||||
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
||||||
@@ -2174,10 +2176,41 @@ function refreshPriceSnapshotConditional(){
|
|||||||
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
||||||
paintPlanTpslDisplay(o.id, o);
|
paintPlanTpslDisplay(o.id, o);
|
||||||
if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(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(()=>{});
|
}).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 }});
|
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -6757,6 +6757,9 @@ def api_price_snapshot():
|
|||||||
symbol=r["symbol"],
|
symbol=r["symbol"],
|
||||||
)
|
)
|
||||||
apply_time_close_to_payload(payload, r)
|
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(
|
new_sl, new_tp, changed = order_monitor_tpsl_needs_sync(
|
||||||
r["stop_loss"], r["take_profit"], exchange_tpsl
|
r["stop_loss"], r["take_profit"], exchange_tpsl
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -476,6 +476,8 @@
|
|||||||
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
|
<span>计划基数: {{ funds_fmt(o.margin_capital) if o.margin_capital is not none else '-' }}U</span>
|
||||||
<span>杠杆: {{ o.leverage or '-' }}x</span>
|
<span>杠杆: {{ o.leverage or '-' }}x</span>
|
||||||
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
|
<span>仓位占比: {{ o.position_ratio if o.position_ratio is not none else '-' }}%</span>
|
||||||
|
<span>开仓时间: {{ (o.opened_at or '-')[:16] }}</span>
|
||||||
|
<span>持仓时长: <span class="order-hold-duration" id="order-hold-duration-{{ o.id }}" data-order-opened-ms="{{ o.opened_at_ms or '' }}">—</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-ex-orders">
|
<div class="pos-ex-orders">
|
||||||
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
||||||
@@ -2227,10 +2229,41 @@ function refreshPriceSnapshotConditional(){
|
|||||||
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
|
||||||
paintPlanTpslDisplay(o.id, o);
|
paintPlanTpslDisplay(o.id, o);
|
||||||
if(window.TimeCloseUI) TimeCloseUI.paintOrderTimeClose(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(()=>{});
|
}).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 }});
|
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1418,7 +1418,7 @@ def list_daily_trades(
|
|||||||
search: str = "",
|
search: str = "",
|
||||||
db_path: Path | None = None,
|
db_path: Path | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""按日期区间列出开仓记录(本日/本周/本月/自选),含犯病与盈亏统计。"""
|
"""按日期区间列出平仓记录(本日/本周/本月/自选,以平仓时间计),含犯病与盈亏统计。"""
|
||||||
init_db(db_path)
|
init_db(db_path)
|
||||||
p = (period or "today").strip().lower() or "today"
|
p = (period or "today").strip().lower() or "today"
|
||||||
start_ms, end_ms, df, dt, period_label = resolve_period_bounds(
|
start_ms, end_ms, df, dt, period_label = resolve_period_bounds(
|
||||||
@@ -1431,7 +1431,7 @@ def list_daily_trades(
|
|||||||
conn = _connect(db_path)
|
conn = _connect(db_path)
|
||||||
try:
|
try:
|
||||||
params: list[Any] = [start_ms, end_ms]
|
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:
|
if ex_filter:
|
||||||
where += " AND exchange_key=?"
|
where += " AND exchange_key=?"
|
||||||
params.append(ex_filter)
|
params.append(ex_filter)
|
||||||
@@ -1439,7 +1439,7 @@ def list_daily_trades(
|
|||||||
f"""
|
f"""
|
||||||
SELECT * FROM archive_trade_cache
|
SELECT * FROM archive_trade_cache
|
||||||
WHERE {where}
|
WHERE {where}
|
||||||
ORDER BY opened_at_ms DESC, trade_id DESC
|
ORDER BY closed_at_ms DESC, trade_id DESC
|
||||||
""",
|
""",
|
||||||
params,
|
params,
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|||||||
@@ -1359,6 +1359,8 @@ def _merge_flask_order_price_fields(hub_mon: dict | None, snap: dict | None) ->
|
|||||||
"stop_loss_display",
|
"stop_loss_display",
|
||||||
"take_profit_display",
|
"take_profit_display",
|
||||||
"display_rr_ratio",
|
"display_rr_ratio",
|
||||||
|
"exchange_initial_margin",
|
||||||
|
"plan_margin",
|
||||||
"time_close_enabled",
|
"time_close_enabled",
|
||||||
"time_close_hours",
|
"time_close_hours",
|
||||||
"time_close_at_ms",
|
"time_close_at_ms",
|
||||||
|
|||||||
@@ -611,11 +611,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveTrendSizingFooter(mo, trendPlan, isTrend) {
|
function resolveTrendSizingFooter(mo, trendPlan, isTrend) {
|
||||||
|
const m = mo || {};
|
||||||
if (!isTrend || !trendPlan || !trendPlan.id) {
|
if (!isTrend || !trendPlan || !trendPlan.id) {
|
||||||
return {
|
return {
|
||||||
leverage: mo.leverage,
|
margin: m.exchange_initial_margin ?? m.plan_margin ?? null,
|
||||||
planBase: mo.margin_capital,
|
leverage: m.leverage,
|
||||||
positionRatio: mo.position_ratio,
|
planBase: m.margin_capital,
|
||||||
|
positionRatio: m.position_ratio,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const base =
|
const base =
|
||||||
@@ -623,12 +625,73 @@
|
|||||||
? trendPlan.snapshot_available_usdt
|
? trendPlan.snapshot_available_usdt
|
||||||
: trendPlan.plan_margin_capital;
|
: trendPlan.plan_margin_capital;
|
||||||
return {
|
return {
|
||||||
|
margin: m.exchange_initial_margin ?? trendPlan.plan_margin_capital ?? null,
|
||||||
leverage: trendPlan.leverage,
|
leverage: trendPlan.leverage,
|
||||||
planBase: base,
|
planBase: base,
|
||||||
positionRatio: resolveTrendPositionRatioPct(trendPlan),
|
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) {
|
function formatMonitorRiskMeta(mo, trendPlan) {
|
||||||
const m = mo || {};
|
const m = mo || {};
|
||||||
const t = trendPlan || {};
|
const t = trendPlan || {};
|
||||||
@@ -1755,6 +1818,7 @@
|
|||||||
if (window.TimeCloseUI && TimeCloseUI.tickLocalCountdowns) {
|
if (window.TimeCloseUI && TimeCloseUI.tickLocalCountdowns) {
|
||||||
TimeCloseUI.tickLocalCountdowns();
|
TimeCloseUI.tickLocalCountdowns();
|
||||||
}
|
}
|
||||||
|
ensureHubHoldDurationTimer();
|
||||||
|
|
||||||
if (expandedExchangeId && fs && fsInner) {
|
if (expandedExchangeId && fs && fsInner) {
|
||||||
const row = rows.find((r) => String(r.id) === String(expandedExchangeId));
|
const row = rows.find((r) => String(r.id) === String(expandedExchangeId));
|
||||||
@@ -1768,6 +1832,7 @@
|
|||||||
if (window.TimeCloseUI && TimeCloseUI.tickLocalCountdowns) {
|
if (window.TimeCloseUI && TimeCloseUI.tickLocalCountdowns) {
|
||||||
TimeCloseUI.tickLocalCountdowns();
|
TimeCloseUI.tickLocalCountdowns();
|
||||||
}
|
}
|
||||||
|
ensureHubHoldDurationTimer();
|
||||||
fsInner.querySelectorAll(".btn-expand-back").forEach((btn) => {
|
fsInner.querySelectorAll(".btn-expand-back").forEach((btn) => {
|
||||||
btn.onclick = (ev) => {
|
btn.onclick = (ev) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
@@ -2548,6 +2613,15 @@
|
|||||||
const pnlFmt = formatFloatingPnlText(upnl, pos.notional_usdt);
|
const pnlFmt = formatFloatingPnlText(upnl, pos.notional_usdt);
|
||||||
const pnlText = pnlFmt.text;
|
const pnlText = pnlFmt.text;
|
||||||
const sizingFoot = resolveTrendSizingFooter(mo, trendPlan, isTrend);
|
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
|
const markDisplay = isTrend
|
||||||
? resolveTrendMarkPrice(pos, trendPlan, symbol, tickMap)
|
? resolveTrendMarkPrice(pos, trendPlan, symbol, tickMap)
|
||||||
: fmtMarkPrice(pos, tickMap);
|
: fmtMarkPrice(pos, tickMap);
|
||||||
@@ -2612,9 +2686,12 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-footer">
|
<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.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>仓位占比: ${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>
|
||||||
<div class="pos-ex-orders">
|
<div class="pos-ex-orders">
|
||||||
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user