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:
+37
-9
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user