Show option/perp PnL and fee-deducted net in trade details.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user