Show option/perp PnL and fee-deducted net in trade details.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-26 16:14:48 +08:00
parent 21b8f587a2
commit 96e8e5cb70
4 changed files with 206 additions and 38 deletions
+84 -18
View File
@@ -7,6 +7,14 @@ import {
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 Group = {
group_id: string;
status: string;
@@ -15,9 +23,11 @@ type Group = {
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;
pnl_summary?: PnlSummary;
};
type Fill = {
@@ -30,10 +40,21 @@ type Fill = {
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);
}
export default function TradesPage() {
const [groups, setGroups] = useState<Group[]>([]);
const [selected, setSelected] = useState<string | null>(null);
const [fills, setFills] = useState<Fill[]>([]);
const [summary, setSummary] = useState<PnlSummary | null>(null);
const [err, setErr] = useState("");
useEffect(() => {
@@ -44,9 +65,13 @@ export default function TradesPage() {
async function openGroup(id: string) {
setSelected(id);
setSummary(null);
try {
const r = await apiFetch<{ fills: Fill[] }>(`/api/trades/groups/${id}`);
const r = await apiFetch<{ fills: Fill[]; pnl_summary: PnlSummary }>(
`/api/trades/groups/${id}`
);
setFills(r.fills);
setSummary(r.pnl_summary);
} catch (e) {
setErr(e instanceof Error ? e.message : String(e));
}
@@ -60,28 +85,35 @@ export default function TradesPage() {
{groups.length === 0 ? (
<p style={{ color: "var(--muted)" }}></p>
) : (
groups.map((g) => (
<div
key={g.group_id}
className="kv"
style={{ cursor: "pointer" }}
onClick={() => openGroup(g.group_id)}
>
<span className="mono">
{g.group_id} · {statusZh(g.status)} ·{" "}
{positionSidesZh(g.perp_side, g.option_side)}
</span>
<span className="mono">
{Number(g.realized_pnl || 0).toFixed(2)} ·{" "}
{closeReasonZh(g.close_reason)}
</span>
</div>
))
groups.map((g) => {
const listPnl =
g.net_pnl ?? g.pnl_summary?.net_pnl ?? g.realized_pnl;
return (
<div
key={g.group_id}
className="kv"
style={{ cursor: "pointer" }}
onClick={() => openGroup(g.group_id)}
>
<span className="mono">
{g.group_id} · {statusZh(g.status)} ·{" "}
{positionSidesZh(g.perp_side, g.option_side)}
</span>
<span className={`mono ${pnlClass(listPnl)}`}>
{fmt(listPnl)} · {closeReasonZh(g.close_reason)}
</span>
</div>
);
})
)}
</div>
{selected ? (
<div className="card">
<h3 style={{ marginTop: 0 }}>{selected} </h3>
<p style={{ color: "var(--muted)", fontSize: 13, marginTop: 0 }}>
= +
</p>
{fills.map((f) => (
<div key={f.id} className="kv">
<span className="mono">
@@ -93,6 +125,40 @@ export default function TradesPage() {
</span>
</div>
))}
{summary ? (
<div
style={{
marginTop: 14,
paddingTop: 12,
borderTop: "1px solid var(--border, #333)",
}}
>
<div className="kv">
<span></span>
<span className={`mono ${pnlClass(summary.option_pnl)}`}>
{fmt(summary.option_pnl)}
</span>
</div>
<div className="kv">
<span></span>
<span className={`mono ${pnlClass(summary.perp_pnl)}`}>
{fmt(summary.perp_pnl)}
</span>
</div>
<div className="kv">
<span></span>
<span className="mono">{fmt(summary.fees_total, 4)}</span>
</div>
<div className="kv">
<span>
<strong></strong>
</span>
<span className={`mono ${pnlClass(summary.net_pnl)}`}>
<strong>{fmt(summary.net_pnl)}</strong>
</span>
</div>
</div>
) : null}
</div>
) : null}
</div>