Show open time and hold duration on position cards.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1039,6 +1039,7 @@ class Matcher:
|
||||
)
|
||||
strike = float(g["strike"]) if g and g["strike"] is not None else None
|
||||
expiry_ymd = str(g["expiry_ymd"]) if g and g["expiry_ymd"] else None
|
||||
open_at_ms = int(g["open_at_ms"]) if g and g["open_at_ms"] else None
|
||||
expiry_ms = None
|
||||
if expiry_ymd and len(expiry_ymd) == 6:
|
||||
try:
|
||||
@@ -1056,6 +1057,7 @@ class Matcher:
|
||||
return {
|
||||
"has_position": True,
|
||||
"group_id": group_id,
|
||||
"open_at_ms": open_at_ms,
|
||||
"perp_side": perp_side,
|
||||
"option_side": option_side,
|
||||
"perp_inst_id": perp_inst_id,
|
||||
|
||||
@@ -5,6 +5,15 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-29 — 持仓卡显示开仓时间 / 持仓时长
|
||||
|
||||
### 变更
|
||||
|
||||
1. 监控页永续、期权持仓卡增加 **开仓时间**(上海时区)与 **持仓时长**(每秒刷新)。
|
||||
2. 接口 `position.open_at_ms` 取自当前组成交组开仓时刻。
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-27 — 残留期权一行一条
|
||||
|
||||
### 变更
|
||||
|
||||
@@ -238,6 +238,7 @@ export type PlanState = {
|
||||
position: {
|
||||
has_position: boolean;
|
||||
group_id?: string;
|
||||
open_at_ms?: number | null;
|
||||
perp_side?: string;
|
||||
option_side?: string;
|
||||
perp_inst_id?: string;
|
||||
|
||||
@@ -41,6 +41,33 @@ function formatCountdown(msLeft: number): string {
|
||||
return `${m}分 ${String(sec).padStart(2, "0")}秒`;
|
||||
}
|
||||
|
||||
/** 上海时区开仓时间 */
|
||||
function fmtOpenTime(ms: number | null | undefined) {
|
||||
if (ms == null || !Number.isFinite(ms) || ms <= 0) return "—";
|
||||
return new Date(ms).toLocaleString("zh-CN", {
|
||||
timeZone: "Asia/Shanghai",
|
||||
hour12: false,
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
/** 持仓时长(实时) */
|
||||
function fmtHoldDuration(openAtMs: number | null | undefined, now: number) {
|
||||
if (openAtMs == null || !Number.isFinite(openAtMs) || openAtMs <= 0) return "—";
|
||||
const ms = Math.max(0, now - openAtMs);
|
||||
const totalSec = Math.floor(ms / 1000);
|
||||
const h = Math.floor(totalSec / 3600);
|
||||
const m = Math.floor((totalSec % 3600) / 60);
|
||||
const s = totalSec % 60;
|
||||
if (h > 0) return `${h}时${m}分${s}秒`;
|
||||
if (m > 0) return `${m}分${s}秒`;
|
||||
return `${s}秒`;
|
||||
}
|
||||
|
||||
const PHASE_ZH: Record<string, string> = {
|
||||
idle: "空闲",
|
||||
wait_signal: "等待信号",
|
||||
@@ -132,6 +159,9 @@ export default function PlanPage() {
|
||||
plan?.mode === "LIVE"
|
||||
? `实盘·${(snap?.exchange || "okx").toUpperCase()}`
|
||||
: "模拟盘";
|
||||
const openAtMs = pos?.open_at_ms ?? null;
|
||||
const openTimeLabel = fmtOpenTime(openAtMs);
|
||||
const holdDurationLabel = open ? fmtHoldDuration(openAtMs, nowMs) : "—";
|
||||
|
||||
return (
|
||||
<div className="plan-page">
|
||||
@@ -429,6 +459,14 @@ export default function PlanPage() {
|
||||
<span className="pos-label">波动比例</span>
|
||||
<span className="pos-value mono">{fmt(movePct, 2)}%</span>
|
||||
</div>
|
||||
<div className="pos-cell">
|
||||
<span className="pos-label">开仓时间</span>
|
||||
<span className="pos-value mono">{openTimeLabel}</span>
|
||||
</div>
|
||||
<div className="pos-cell">
|
||||
<span className="pos-label">持仓时长</span>
|
||||
<span className="pos-value mono">{holdDurationLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -482,6 +520,14 @@ export default function PlanPage() {
|
||||
<span className="pos-label">初始权利金</span>
|
||||
<span className="pos-value mono">{fmt(pos?.initial_premium)}</span>
|
||||
</div>
|
||||
<div className="pos-cell">
|
||||
<span className="pos-label">开仓时间</span>
|
||||
<span className="pos-value mono">{openTimeLabel}</span>
|
||||
</div>
|
||||
<div className="pos-cell">
|
||||
<span className="pos-label">持仓时长</span>
|
||||
<span className="pos-value mono">{holdDurationLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user