Fix OO trade records: include Put PnL and show dual-leg detail clearly.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-11 09:34:35 +08:00
parent 26e6d98338
commit 4a5d19e30f
6 changed files with 481 additions and 141 deletions
+37 -9
View File
@@ -11,11 +11,14 @@ const SIDE_ZH: Record<string, string> = {
short: "空",
call: "看涨",
put: "看跌",
buy: "多",
sell: "平",
};
const LEG_ZH: Record<string, string> = {
perp: "永续",
option: "期权",
option2: "Put",
};
const ACTION_ZH: Record<string, string> = {
@@ -27,6 +30,7 @@ const CLOSE_REASON_ZH: Record<string, string> = {
fixed_usdt: "固定净盈利达标·双腿全平",
premium_multiple: "权利金倍数达标·双腿全平",
target_perp_only: "净盈利达标·只平永续(期权归档)",
target_oo_win: "期期达标·平盈利腿(亏损腿残留)",
residual_premium_close: "残留期权·权利金回收中途平",
semi_target_points: "半自动·指数到行权价±波动点且净利>0·双腿全平",
semi_perp_exit: "半自动·永续净利锁定达标·双腿全平",
@@ -47,11 +51,20 @@ export function sideZh(v: string | null | undefined): string {
return SIDE_ZH[v] || v;
}
/** 永续方向 / 期权方向,如「多/看跌」 */
/** 永续方向 / 期权方向,如「多/看跌」;期期为「期期·看涨+看跌」 */
export function positionSidesZh(
perp: string | null | undefined,
option: string | null | undefined,
opts?: {
isOo?: boolean;
option2?: string | null;
},
): string {
if (opts?.isOo) {
const a = sideZh(option || "call");
const b = sideZh(opts.option2 || "put");
return `期期·${a}+${b}`;
}
return `${sideZh(perp)}/${sideZh(option)}`;
}
@@ -60,18 +73,33 @@ export function fillDescZh(
action: string,
side: string,
closeReason?: string | null,
isOo?: boolean,
): string {
const isExpiry = closeReason === "expiry";
if (leg === "option2") {
if (action === "open") return "Put开多";
if (action === "close" && isExpiry) return "Put到期结算";
if (action === "close") return "Put平多";
}
if (leg === "option") {
if (isOo) {
if (action === "open") return "Call开多";
if (action === "close" && isExpiry) return "Call到期结算";
if (action === "close") return "Call平多";
}
if (action === "open") return "期权开多";
if (action === "close" && isExpiry) return "期权到期结算";
if (action === "close") return "期权平多";
}
if (leg === "perp" && action === "open")
return sideZh(side) === "多" ? "永续开多" : "永续开空";
if (leg === "perp" && action === "close") {
const s = sideZh(side);
return s === "多" || side === "long" ? "永续平多" : "永续平空";
}
const l = LEG_ZH[leg] || leg;
const a = ACTION_ZH[action] || action;
const s = SIDE_ZH[side] || side;
if (leg === "option" && action === "close" && closeReason === "expiry") {
return "期权到期结算";
}
// 期权买入开仓:「期权开多」;永续:「永续开多/开空」
if (leg === "option" && action === "open") return "期权开多";
if (leg === "option" && action === "close") return "期权平多";
if (leg === "perp" && action === "open") return s === "多" ? "永续开多" : "永续开空";
if (leg === "perp" && action === "close") return s === "多" ? "永续平多" : "永续平空";
return `${l}${a}${s}`;
}
+222 -76
View File
@@ -10,9 +10,11 @@ import {
type PnlSummary = {
option_pnl: number | null;
option2_pnl?: number | null;
perp_pnl: number | null;
fees_perp?: number;
fees_option?: number;
fees_option2?: number;
fees_total: number;
slip_total?: number;
gross_pnl: number | null;
@@ -25,6 +27,11 @@ type ExpirySettle = {
intrinsic: number | null;
formula: string;
perp_note: string;
is_oo?: boolean;
strike2?: number | null;
intrinsic2?: number | null;
formula2?: string;
option2_side?: string | null;
};
type Group = {
@@ -34,9 +41,13 @@ type Group = {
option_side: string | null;
perp_side: string | null;
option_inst_id?: string | null;
option2_inst_id?: string | null;
option2_side?: string | null;
perp_inst_id?: string | null;
expiry_ymd?: string | null;
initial_premium: number;
initial_premium2?: number | null;
total_initial_premium?: number | null;
realized_pnl: number;
net_pnl?: number | null;
close_reason: string | null;
@@ -47,16 +58,30 @@ type Group = {
hold_ms?: number | null;
hold_basis?: string | null;
strike?: number | null;
strike2?: number | null;
settle_index_px?: number | null;
entry_index_px?: number | null;
close_index_px?: number | null;
move_points?: number | null;
option_leverage?: number | null;
option2_leverage?: number | null;
exec_mode?: string | null;
hedge_mode?: string | null;
is_oo?: boolean;
expiry_settle?: ExpirySettle | null;
pnl_summary?: PnlSummary;
};
function isOoGroup(g: Group | null | undefined): boolean {
if (!g) return false;
return (
g.is_oo === true ||
g.hedge_mode === "option_option" ||
!!g.option2_inst_id ||
g.bias === "option_option"
);
}
type Fill = {
id: number;
leg: string;
@@ -111,6 +136,24 @@ function groupPnl(g: Group) {
return g.net_pnl ?? g.pnl_summary?.net_pnl ?? g.realized_pnl;
}
function groupDirectionZh(g: Group) {
return positionSidesZh(g.perp_side, g.option_side, {
isOo: isOoGroup(g),
option2: g.option2_side,
});
}
function groupLeverageZh(g: Group) {
if (isOoGroup(g)) {
const a =
g.option_leverage != null ? `${fmt(g.option_leverage, 0)}x` : "—";
const b =
g.option2_leverage != null ? `${fmt(g.option2_leverage, 0)}x` : "—";
return `${a}/${b}`;
}
return g.option_leverage != null ? `${fmt(g.option_leverage, 0)}x` : "—";
}
/** 开仓→平仓指数点数(带符号) */
function fmtMovePoints(n: number | null | undefined) {
if (n == null || Number.isNaN(n)) return "—";
@@ -272,8 +315,7 @@ export default function TradesPage() {
<div className="trade-row-main mono">
<span className="trade-row-id">{g.group_id}</span>
<span className="trade-row-meta">
{statusZh(g.status)} ·{" "}
{positionSidesZh(g.perp_side, g.option_side)}
{statusZh(g.status)} · {groupDirectionZh(g)}
</span>
<span className="trade-row-times">
{fmtTime(openMs)} · {fmtTime(closeMs)} · {" "}
@@ -281,8 +323,8 @@ export default function TradesPage() {
{g.move_points != null
? ` · 波动 ${fmtMovePoints(g.move_points)}`
: ""}
{g.option_leverage != null
? ` · 期权杠杆 ${fmt(g.option_leverage, 0)}x`
{isOoGroup(g) || g.option_leverage != null
? ` · ${isOoGroup(g) ? "杠杆" : "期权杠杆"} ${groupLeverageZh(g)}`
: ""}
</span>
</div>
@@ -343,7 +385,7 @@ export default function TradesPage() {
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
@@ -366,18 +408,12 @@ export default function TradesPage() {
>
<td className="mono">{seq}</td>
<td className="mono trade-table-id">{g.group_id}</td>
<td className="mono">
{positionSidesZh(g.perp_side, g.option_side)}
</td>
<td className="mono">{groupDirectionZh(g)}</td>
<td className="mono">{statusZh(g.status)}</td>
<td className="mono">{fmtTime(openMs)}</td>
<td className="mono">{fmtTime(closeMs)}</td>
<td className="mono">{fmtHold(g.hold_ms)}</td>
<td className="mono">
{g.option_leverage != null
? `${fmt(g.option_leverage, 0)}x`
: "—"}
</td>
<td className="mono">{groupLeverageZh(g)}</td>
<td className="mono">{fmtMovePoints(g.move_points)}</td>
<td className={`mono ${pnlClass(listPnl)}`}>
{fmt(listPnl)}
@@ -487,6 +523,19 @@ export default function TradesPage() {
{!detailLoading && !detailErr && selectedGroup ? (
<>
{(() => {
const oo = isOoGroup(selectedGroup);
const callInst =
selectedGroup.option_inst_id ||
optionInstFromFills(fills) ||
"—";
const putInst = selectedGroup.option2_inst_id || "—";
const totalPrem =
selectedGroup.total_initial_premium ??
(Number(selectedGroup.initial_premium || 0) +
Number(selectedGroup.initial_premium2 || 0));
return (
<>
<div className="trade-hold-summary">
<div className="kv">
<span></span>
@@ -498,22 +547,32 @@ export default function TradesPage() {
</span>
</div>
<div className="kv">
<span></span>
<span className="mono">
{positionSidesZh(
selectedGroup.perp_side,
selectedGroup.option_side,
)}
</span>
<span></span>
<span className="mono">{oo ? "期期对冲" : "永期对冲"}</span>
</div>
<div className="kv">
<span></span>
<span></span>
<span className="mono">
{selectedGroup.option_inst_id ||
optionInstFromFills(fills) ||
"—"}
{groupDirectionZh(selectedGroup)}
</span>
</div>
{oo ? (
<>
<div className="kv">
<span>Call合约</span>
<span className="mono">{callInst}</span>
</div>
<div className="kv">
<span>Put合约</span>
<span className="mono">{putInst}</span>
</div>
</>
) : (
<div className="kv">
<span></span>
<span className="mono">{callInst}</span>
</div>
)}
<div className="kv">
<span></span>
<span className="mono">
@@ -526,12 +585,15 @@ export default function TradesPage() {
)}
</span>
</div>
{selectedGroup.strike != null &&
!selectedGroup.expiry_settle ? (
{!selectedGroup.expiry_settle &&
(selectedGroup.strike != null ||
(oo && selectedGroup.strike2 != null)) ? (
<div className="kv">
<span></span>
<span className="mono">
{fmt(selectedGroup.strike, 0)}
{oo
? `Call ${fmt(selectedGroup.strike, 0)} / Put ${fmt(selectedGroup.strike2, 0)}`
: fmt(selectedGroup.strike, 0)}
</span>
</div>
) : null}
@@ -563,21 +625,42 @@ export default function TradesPage() {
</span>
</div>
<div className="kv">
<span></span>
<span>{oo ? "杠杆(Call/Put)" : "期权杠杆"}</span>
<span className="mono">
{selectedGroup.option_leverage != null
? `${fmt(selectedGroup.option_leverage, 0)}x`
: "—"}
</span>
</div>
<div className="kv">
<span></span>
<span className="mono">
{selectedGroup.initial_premium != null
? `${fmt(selectedGroup.initial_premium)} USDT`
: "—"}
{groupLeverageZh(selectedGroup)}
</span>
</div>
{oo ? (
<>
<div className="kv">
<span>Call权利金</span>
<span className="mono">
{fmt(selectedGroup.initial_premium)} USDT
</span>
</div>
<div className="kv">
<span>Put权利金</span>
<span className="mono">
{fmt(selectedGroup.initial_premium2)} USDT
</span>
</div>
<div className="kv">
<span></span>
<span className="mono">
{fmt(totalPrem)} USDT
</span>
</div>
</>
) : (
<div className="kv">
<span></span>
<span className="mono">
{selectedGroup.initial_premium != null
? `${fmt(selectedGroup.initial_premium)} USDT`
: "—"}
</span>
</div>
)}
<div className="kv">
<span></span>
<span className="mono">
@@ -591,9 +674,13 @@ export default function TradesPage() {
</div>
<p className="trade-detail-hint">
{selectedGroup.close_reason === "expiry"
? "到期结算:期权按「指数 vs 行权价」的内在价值入账(非盘口);永续仍按市价平。净盈亏 = 期权盈亏 + 永续盈亏 − 全部手续费。"
: "成交价为成交均价(未预先扣费);手续费单独列出。净盈亏 = 期权盈亏 + 永续盈亏 全部手续费。"}
{oo
? selectedGroup.close_reason === "expiry"
? "期期到期:Call/Put 均按「指数 vs 行权价」内在价值结算(非盘口)。净盈亏 = Call盈亏 + Put盈亏 全部手续费。"
: "期期对冲:双腿均为期权,无永续。成交价为成交均价;手续费单独列出。净盈亏 = Call盈亏 + Put盈亏 全部手续费。"
: selectedGroup.close_reason === "expiry"
? "到期结算:期权按「指数 vs 行权价」的内在价值入账(非盘口);永续仍按市价平。净盈亏 = 期权盈亏 + 永续盈亏 − 全部手续费。"
: "成交价为成交均价(未预先扣费);手续费单独列出。净盈亏 = 期权盈亏 + 永续盈亏 − 全部手续费。"}
</p>
{selectedGroup.expiry_settle ? (
@@ -607,26 +694,54 @@ export default function TradesPage() {
)}
</span>
</div>
<div className="kv">
<span></span>
<span className="mono">
{fmt(selectedGroup.expiry_settle.strike, 0)}
</span>
</div>
<div className="kv">
<span></span>
<span className="mono">
{fmt(selectedGroup.expiry_settle.intrinsic)}
{selectedGroup.expiry_settle.formula
? ` · ${selectedGroup.expiry_settle.formula}`
: ""}
</span>
</div>
{oo ? (
<>
<div className="kv">
<span>Call行权价 / </span>
<span className="mono">
{fmt(selectedGroup.expiry_settle.strike, 0)} /{" "}
{fmt(selectedGroup.expiry_settle.intrinsic)}
{selectedGroup.expiry_settle.formula
? ` · ${selectedGroup.expiry_settle.formula}`
: ""}
</span>
</div>
<div className="kv">
<span>Put行权价 / </span>
<span className="mono">
{fmt(selectedGroup.expiry_settle.strike2, 0)} /{" "}
{fmt(selectedGroup.expiry_settle.intrinsic2)}
{selectedGroup.expiry_settle.formula2
? ` · ${selectedGroup.expiry_settle.formula2}`
: ""}
</span>
</div>
</>
) : (
<>
<div className="kv">
<span></span>
<span className="mono">
{fmt(selectedGroup.expiry_settle.strike, 0)}
</span>
</div>
<div className="kv">
<span></span>
<span className="mono">
{fmt(selectedGroup.expiry_settle.intrinsic)}
{selectedGroup.expiry_settle.formula
? ` · ${selectedGroup.expiry_settle.formula}`
: ""}
</span>
</div>
</>
)}
</div>
) : null}
{fills.map((f) => {
const isOpt = f.leg === "option";
const isOpt =
f.leg === "option" || f.leg === "option2";
const pxLabel = isOpt ? "权利金" : "价";
const notional =
isOpt && f.action === "open"
@@ -640,6 +755,7 @@ export default function TradesPage() {
f.action,
f.side,
selectedGroup.close_reason,
oo,
)}
{f.inst_id ? (
<span className="meta"> · {f.inst_id}</span>
@@ -660,24 +776,51 @@ export default function TradesPage() {
{summary ? (
<div className="trade-pnl-block">
<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_perp ?? 0, 4)}
</span>
</div>
{oo ? (
<>
<div className="kv">
<span>Call盈亏</span>
<span
className={`mono ${pnlClass(summary.option_pnl)}`}
>
{fmt(summary.option_pnl)}
</span>
</div>
<div className="kv">
<span>Put盈亏</span>
<span
className={`mono ${pnlClass(summary.option2_pnl)}`}
>
{fmt(summary.option2_pnl)}
</span>
</div>
</>
) : (
<>
<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_perp ?? 0, 4)}
</span>
</div>
</>
)}
<div className="kv">
<span></span>
<span className="mono">
@@ -707,6 +850,9 @@ export default function TradesPage() {
</div>
</div>
) : null}
</>
);
})()}
</>
) : null}
</div>