Show trade open/close times and hold period by target exit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-27 15:37:37 +08:00
parent 0e3a8b08ec
commit 14c6ba51a8
9 changed files with 279 additions and 30 deletions
+73 -3
View File
@@ -27,6 +27,10 @@ type Group = {
close_reason: string | null;
open_at_ms: number | null;
close_at_ms: number | null;
hold_open_at_ms?: number | null;
hold_close_at_ms?: number | null;
hold_ms?: number | null;
hold_basis?: string | null;
pnl_summary?: PnlSummary;
};
@@ -50,9 +54,37 @@ function fmt(n: number | null | undefined, digits = 2) {
return Number(n).toFixed(digits);
}
/** 上海时区开/平仓时间 */
function fmtTime(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,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
/** 持仓周期:x时y分z秒 */
function fmtHold(ms: number | null | undefined) {
if (ms == null || !Number.isFinite(ms) || ms < 0) return "—";
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}`;
}
export default function TradesPage() {
const [groups, setGroups] = useState<Group[]>([]);
const [selected, setSelected] = useState<string | null>(null);
const [selectedGroup, setSelectedGroup] = useState<Group | null>(null);
const [fills, setFills] = useState<Fill[]>([]);
const [summary, setSummary] = useState<PnlSummary | null>(null);
const [err, setErr] = useState("");
@@ -66,12 +98,16 @@ export default function TradesPage() {
async function openGroup(id: string) {
setSelected(id);
setSummary(null);
setSelectedGroup(null);
try {
const r = await apiFetch<{ fills: Fill[]; pnl_summary: PnlSummary }>(
`/api/trades/groups/${id}`
);
const r = await apiFetch<{
group: Group;
fills: Fill[];
pnl_summary: PnlSummary;
}>(`/api/trades/groups/${id}`);
setFills(r.fills);
setSummary(r.pnl_summary);
setSelectedGroup(r.group);
} catch (e) {
setErr(e instanceof Error ? e.message : String(e));
}
@@ -80,6 +116,9 @@ export default function TradesPage() {
return (
<div className="trades-page">
<h2 style={{ marginTop: 0 }}></h2>
<p className="trade-hold-note">
</p>
{err ? <div className="err">{err}</div> : null}
<div className="card trade-list" style={{ marginBottom: 12 }}>
{groups.length === 0 ? (
@@ -88,6 +127,8 @@ export default function TradesPage() {
groups.map((g) => {
const listPnl =
g.net_pnl ?? g.pnl_summary?.net_pnl ?? g.realized_pnl;
const openMs = g.hold_open_at_ms ?? g.open_at_ms;
const closeMs = g.hold_close_at_ms ?? g.close_at_ms;
return (
<button
key={g.group_id}
@@ -101,6 +142,10 @@ export default function TradesPage() {
{statusZh(g.status)} ·{" "}
{positionSidesZh(g.perp_side, g.option_side)}
</span>
<span className="trade-row-times">
{fmtTime(openMs)} · {fmtTime(closeMs)} · {" "}
{fmtHold(g.hold_ms)}
</span>
</div>
<div className={`trade-row-pnl mono ${pnlClass(listPnl)}`}>
<span> {fmt(listPnl)}</span>
@@ -118,6 +163,31 @@ export default function TradesPage() {
{selected ? (
<div className="card trade-detail">
<h3 style={{ marginTop: 0 }}>{selected} </h3>
{selectedGroup ? (
<div className="trade-hold-summary">
<div className="kv">
<span></span>
<span className="mono">
{fmtTime(selectedGroup.hold_open_at_ms ?? selectedGroup.open_at_ms)}
</span>
</div>
<div className="kv">
<span></span>
<span className="mono">
{fmtTime(
selectedGroup.hold_close_at_ms ?? selectedGroup.close_at_ms,
)}
{selectedGroup.hold_basis === "perp" ? (
<span className="trade-hold-basis"></span>
) : null}
</span>
</div>
<div className="kv">
<span></span>
<span className="mono">{fmtHold(selectedGroup.hold_ms)}</span>
</div>
</div>
) : null}
<p style={{ color: "var(--muted)", fontSize: 13, marginTop: 0 }}>
= +
+26
View File
@@ -558,6 +558,32 @@ input {
word-break: break-word;
}
.trade-row-times {
color: var(--muted);
font-size: 12px;
line-height: 1.4;
word-break: break-word;
}
.trade-hold-note {
margin: -4px 0 12px;
color: var(--muted);
font-size: 13px;
line-height: 1.4;
}
.trade-hold-summary {
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--line);
}
.trade-hold-basis {
margin-left: 6px;
color: var(--muted);
font-size: 12px;
}
.trade-row-pnl {
display: flex;
flex-direction: column;