import { useEffect, useId, useState } from "react"; import { apiFetch } from "../api/client"; import { closeReasonZh, fillDescZh, positionSidesZh, statusZh, } from "../labels"; type PnlSummary = { option_pnl: number | null; perp_pnl: number | null; fees_total: number; gross_pnl: number | null; net_pnl: number | null; }; type ExpirySettle = { settle_index_px: number | null; strike: number | null; intrinsic: number | null; formula: string; perp_note: string; }; type Group = { group_id: string; status: string; bias: string | null; option_side: string | null; perp_side: string | null; initial_premium: number; realized_pnl: number; net_pnl?: number | null; 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; strike?: number | null; settle_index_px?: number | null; expiry_settle?: ExpirySettle | null; pnl_summary?: PnlSummary; }; type Fill = { id: number; leg: string; action: string; side: string; fill_px: number; fee: number; qty_eth: number; }; function pnlClass(n: number | null | undefined) { if (n == null || Number.isNaN(n)) return ""; return n > 0 ? "pos-pnl-profit" : n < 0 ? "pos-pnl-loss" : ""; } function fmt(n: number | null | undefined, digits = 2) { if (n == null || Number.isNaN(n)) return "—"; 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 titleId = useId(); const [groups, setGroups] = useState([]); const [selected, setSelected] = useState(null); const [selectedGroup, setSelectedGroup] = useState(null); const [fills, setFills] = useState([]); const [summary, setSummary] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [detailErr, setDetailErr] = useState(""); const [err, setErr] = useState(""); useEffect(() => { apiFetch<{ groups: Group[] }>("/api/trades/groups") .then((r) => setGroups(r.groups)) .catch((e) => setErr(e instanceof Error ? e.message : String(e))); }, []); useEffect(() => { if (!selected) return; const onKey = (ev: KeyboardEvent) => { if (ev.key === "Escape") closeDetail(); }; window.addEventListener("keydown", onKey); const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = prev; }; }, [selected]); function closeDetail() { setSelected(null); setSelectedGroup(null); setFills([]); setSummary(null); setDetailErr(""); setDetailLoading(false); } async function openGroup(id: string) { setSelected(id); setSummary(null); setSelectedGroup(null); setFills([]); setDetailErr(""); setDetailLoading(true); try { 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) { setDetailErr(e instanceof Error ? e.message : String(e)); } finally { setDetailLoading(false); } } return (

交易记录

持仓周期按目标平仓计时;净盈利达标且只平永续时,以永续平仓时间为准(不含期权残留至到期)。点击一条可查看明细。

{err ?
{err}
: null}
{groups.length === 0 ? (

暂无成交组

) : ( 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 ( ); }) )}
{selected ? (
ev.stopPropagation()} >

{selected}

{detailLoading ? (

加载明细…

) : null} {detailErr ?
{detailErr}
: null} {!detailLoading && !detailErr && selectedGroup ? ( <>
状态 {statusZh(selectedGroup.status)} {selectedGroup.close_reason ? ` · ${closeReasonZh(selectedGroup.close_reason)}` : ""}
方向 {positionSidesZh( selectedGroup.perp_side, selectedGroup.option_side, )}
开仓时间 {fmtTime( selectedGroup.hold_open_at_ms ?? selectedGroup.open_at_ms, )}
平仓时间 {fmtTime( selectedGroup.hold_close_at_ms ?? selectedGroup.close_at_ms, )} {selectedGroup.hold_basis === "perp" ? ( (永续) ) : null}
持仓周期 {fmtHold(selectedGroup.hold_ms)}

{selectedGroup.close_reason === "expiry" ? "到期结算:期权按「指数 vs 行权价」的内在价值入账(非盘口);永续仍按市价平。净盈亏 = 期权盈亏 + 永续盈亏 − 全部手续费。" : "成交价为成交均价(未预先扣费);手续费单独列出。净盈亏 = 期权盈亏 + 永续盈亏 − 全部手续费。"}

{selectedGroup.expiry_settle ? (
结算指数 {fmt(selectedGroup.expiry_settle.settle_index_px)}
行权价 {fmt(selectedGroup.expiry_settle.strike, 0)}
内在价值 {fmt(selectedGroup.expiry_settle.intrinsic)} {selectedGroup.expiry_settle.formula ? ` · ${selectedGroup.expiry_settle.formula}` : ""}
) : null} {fills.map((f) => (
{fillDescZh( f.leg, f.action, f.side, selectedGroup.close_reason, )} 价 {f.fill_px.toFixed(4)} · 数量 {f.qty_eth} · 手续费{" "} {f.fee.toFixed(4)}
))} {summary ? (
期权盈亏 {fmt(summary.option_pnl)}
永续盈亏 {fmt(summary.perp_pnl)}
手续费合计 {fmt(summary.fees_total, 4)}
净盈亏(扣费后) {fmt(summary.net_pnl)}
) : null} ) : null}
) : null}
); }