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
+99 -28
View File
@@ -16,35 +16,62 @@ def _row(r: Any) -> dict:
return dict(r)
def _is_oo_group(g: dict) -> bool:
return (
str(g.get("hedge_mode") or "") == "option_option"
or bool(g.get("option2_inst_id"))
or str(g.get("bias") or "") == "option_option"
)
def _infer_settle_index(g: dict, fills: list) -> float | None:
settle_index = g.get("settle_index_px")
if settle_index is not None:
try:
v = float(settle_index)
if v > 0:
return v
except (TypeError, ValueError):
pass
strike = g.get("strike")
side = str(g.get("option_side") or "").lower()
if strike is None:
return None
for raw in fills:
f = dict(raw) if not isinstance(raw, dict) else raw
if str(f.get("leg")) != "option" or str(f.get("action")) != "close":
continue
if abs(float(f.get("slip") or 0)) > 1e-12:
continue
px = float(f.get("fill_px") or 0)
k = float(strike)
if side in ("call", "c"):
return k + px
if side in ("put", "p"):
return k - px
break
return None
def _intrinsic(side: str, settle_index: float, strike: float) -> float:
s = str(side or "").lower()
if s in ("call", "c"):
return max(settle_index - strike, 0.0)
if s in ("put", "p"):
return max(strike - settle_index, 0.0)
return 0.0
def _expiry_settle_info(g: dict, fills: list) -> dict | None:
"""到期结算口径:期权价 = 内在价值(指数 vs 行权价),非盘口。"""
if str(g.get("close_reason") or "") != "expiry":
return None
settle_index = _infer_settle_index(g, fills)
strike = g.get("strike")
side = str(g.get("option_side") or "").lower()
settle_index = g.get("settle_index_px")
if settle_index is None and strike is not None:
for raw in fills:
f = dict(raw) if not isinstance(raw, dict) else raw
if str(f.get("leg")) != "option" or str(f.get("action")) != "close":
continue
if abs(float(f.get("slip") or 0)) > 1e-12:
continue
px = float(f.get("fill_px") or 0)
k = float(strike)
if side in ("call", "c"):
settle_index = k + px
elif side in ("put", "p"):
settle_index = k - px
break
intrinsic = None
if settle_index is not None and strike is not None:
s = float(settle_index)
k = float(strike)
if side in ("call", "c"):
intrinsic = max(s - k, 0.0)
elif side in ("put", "p"):
intrinsic = max(k - s, 0.0)
intrinsic = _intrinsic(side, float(settle_index), float(strike))
formula = (
"Call: max(指数−行权价, 0)"
if side in ("call", "c")
@@ -52,13 +79,36 @@ def _expiry_settle_info(g: dict, fills: list) -> dict | None:
if side in ("put", "p")
else ""
)
return {
is_oo = _is_oo_group(g)
out: dict[str, Any] = {
"settle_index_px": float(settle_index) if settle_index is not None else None,
"strike": float(strike) if strike is not None else None,
"intrinsic": intrinsic,
"formula": formula,
"perp_note": "永续仍按市价平仓(非指数交割)",
"perp_note": (
"期期无永续腿;两腿均按内在价值结算"
if is_oo
else "永续仍按市价平仓(非指数交割)"
),
"is_oo": is_oo,
}
if is_oo:
strike2 = g.get("strike2")
side2 = str(g.get("option2_side") or "put").lower()
intrinsic2 = None
if settle_index is not None and strike2 is not None:
intrinsic2 = _intrinsic(side2, float(settle_index), float(strike2))
out["strike2"] = float(strike2) if strike2 is not None else None
out["intrinsic2"] = intrinsic2
out["formula2"] = (
"Put: max(行权价−指数, 0)"
if side2 in ("put", "p")
else "Call: max(指数−行权价, 0)"
if side2 in ("call", "c")
else ""
)
out["option2_side"] = side2
return out
def _close_index_px(g: dict, fills: list) -> float | None:
@@ -101,10 +151,10 @@ def _move_points(g: dict, fills: list) -> float | None:
return round(float(close_px) - e, 2)
def _option_entry_px(fills: list) -> float | None:
def _option_entry_px(fills: list, *, leg: str = "option") -> float | None:
for row in fills:
f = dict(row) if not isinstance(row, dict) else row
if str(f.get("leg") or "") != "option" or str(f.get("action") or "") != "open":
if str(f.get("leg") or "") != leg or str(f.get("action") or "") != "open":
continue
try:
v = float(f.get("fill_px") or 0)
@@ -116,7 +166,9 @@ def _option_entry_px(fills: list) -> float | None:
return None
def _option_leverage(g: dict, fills: list) -> float | None:
def _option_leverage_for_leg(
g: dict, fills: list, *, leg: str = "option"
) -> float | None:
"""开仓期权杠杆 = 开仓指数 ÷ 期权开仓均价(与选约门限口径一致)。"""
from ..strategy.selection import option_leverage
@@ -124,7 +176,7 @@ def _option_leverage(g: dict, fills: list) -> float | None:
entry = float(g.get("entry_index_px") or 0)
except (TypeError, ValueError):
return None
opt_px = _option_entry_px(fills)
opt_px = _option_entry_px(fills, leg=leg)
if entry <= 0 or opt_px is None:
return None
lev = option_leverage(entry, opt_px)
@@ -132,6 +184,8 @@ def _option_leverage(g: dict, fills: list) -> float | None:
def _enrich_group(g: dict, fills: list) -> dict:
is_oo = _is_oo_group(g)
g["is_oo"] = is_oo
summary = summarize_fills_pnl(fills)
# LIVE:优先 groups.realized_pnl(已按交易所回写,含资金费)
if str(g.get("exec_mode") or "").upper() == "LIVE" and g.get("realized_pnl") is not None:
@@ -140,11 +194,26 @@ def _enrich_group(g: dict, fills: list) -> dict:
if g.get("funding_usdt") is not None:
summary["funding_usdt"] = float(g["funding_usdt"])
summary["pnl_source"] = "live_exchange"
# 期期 SIM:若成交汇总缺腿但组上已有 realized_pnl,用组值兜底
elif (
is_oo
and g.get("realized_pnl") is not None
and (
summary.get("option_pnl") is None
or summary.get("option2_pnl") is None
)
):
summary = dict(summary)
summary["net_pnl"] = float(g["realized_pnl"])
summary["pnl_source"] = "group_realized"
g["pnl_summary"] = summary
if summary.get("net_pnl") is not None:
g["net_pnl"] = summary["net_pnl"]
elif g.get("realized_pnl") is not None:
g["net_pnl"] = float(g["realized_pnl"])
prem1 = float(g.get("initial_premium") or 0)
prem2 = float(g.get("initial_premium2") or 0) if is_oo else 0.0
g["total_initial_premium"] = prem1 + prem2 if is_oo else prem1
g.update(hold_timing(g, fills))
info = _expiry_settle_info(g, fills)
if info:
@@ -154,7 +223,9 @@ def _enrich_group(g: dict, fills: list) -> dict:
mp = _move_points(g, fills)
g["move_points"] = mp
g["close_index_px"] = _close_index_px(g, fills)
g["option_leverage"] = _option_leverage(g, fills)
g["option_leverage"] = _option_leverage_for_leg(g, fills, leg="option")
if is_oo:
g["option2_leverage"] = _option_leverage_for_leg(g, fills, leg="option2")
return g
+42 -28
View File
@@ -14,23 +14,35 @@ def _as_map(x: Any) -> dict[str, Any]:
return {}
def summarize_fills_pnl(fills: list[Any]) -> dict[str, float | None]:
"""
价差盈亏按 fill_px;手续费另扣。
净盈亏 = 期权盈亏 + 永续盈亏 − 全部手续费(开+平)。
允许只有永续已平、期权尚未结算的半组。
手续费拆:fees_perp / fees_option;滑点合计 slip_totalSIM 记账;LIVE 应为 0)。
"""
rows = [_as_map(x) for x in fills]
def _leg_option_pnl(rows: list[dict[str, Any]], leg: str) -> float | None:
opt_open = next(
(f for f in rows if f.get("leg") == "option" and f.get("action") == "open"),
(f for f in rows if f.get("leg") == leg and f.get("action") == "open"),
None,
)
opt_close = next(
(f for f in rows if f.get("leg") == "option" and f.get("action") == "close"),
(f for f in rows if f.get("leg") == leg and f.get("action") == "close"),
None,
)
if not opt_open or not opt_close:
return None
qty = float(opt_open.get("qty_eth") or opt_close.get("qty_eth") or 0)
return (float(opt_close["fill_px"]) - float(opt_open["fill_px"])) * qty
def summarize_fills_pnl(fills: list[Any]) -> dict[str, float | None]:
"""
价差盈亏按 fill_px;手续费另扣。
净盈亏 = 各腿盈亏之和 − 全部手续费(开+平)。
支持永期(option+perp)与期期(option+option2)。
手续费拆:fees_perp / fees_option(含 option2/ fees_option2
滑点合计 slip_totalSIM 记账;LIVE 应为 0)。
"""
rows = [_as_map(x) for x in fills]
option_pnl = _leg_option_pnl(rows, "option")
option2_pnl = _leg_option_pnl(rows, "option2")
perp_open = next(
(f for f in rows if f.get("leg") == "perp" and f.get("action") == "open"),
None,
@@ -40,11 +52,6 @@ def summarize_fills_pnl(fills: list[Any]) -> dict[str, float | None]:
None,
)
option_pnl: float | None = None
if opt_open and opt_close:
qty = float(opt_open.get("qty_eth") or opt_close.get("qty_eth") or 0)
option_pnl = (float(opt_close["fill_px"]) - float(opt_open["fill_px"])) * qty
perp_pnl: float | None = None
if perp_open and perp_close:
qty = float(perp_open.get("qty_eth") or perp_close.get("qty_eth") or 0)
@@ -60,27 +67,34 @@ def summarize_fills_pnl(fills: list[Any]) -> dict[str, float | None]:
float(f.get("fee") or 0) for f in rows if str(f.get("leg") or "") == "perp"
)
fees_option = sum(
float(f.get("fee") or 0) for f in rows if str(f.get("leg") or "") == "option"
float(f.get("fee") or 0)
for f in rows
if str(f.get("leg") or "") in ("option", "option2")
)
fees_option2 = sum(
float(f.get("fee") or 0) for f in rows if str(f.get("leg") or "") == "option2"
)
fees_total = fees_perp + fees_option
slip_total = sum(float(f.get("slip") or 0) for f in rows)
gross = None
net = None
if option_pnl is not None and perp_pnl is not None:
gross = option_pnl + perp_pnl
net = gross - fees_total
elif option_pnl is not None:
gross = option_pnl
net = option_pnl - fees_total
elif perp_pnl is not None:
gross = perp_pnl
net = perp_pnl - fees_total
parts: list[float] = []
if option_pnl is not None:
parts.append(option_pnl)
if option2_pnl is not None:
parts.append(option2_pnl)
if perp_pnl is not None:
parts.append(perp_pnl)
gross = sum(parts) if parts else None
net = (gross - fees_total) if gross is not None else None
return {
"option_pnl": option_pnl,
"option2_pnl": option2_pnl,
"perp_pnl": perp_pnl,
"fees_perp": fees_perp,
"fees_option": fees_option,
"fees_option2": fees_option2,
"fees_total": fees_total,
"slip_total": slip_total,
"gross_pnl": gross,
+67
View File
@@ -0,0 +1,67 @@
"""期期成交盈亏汇总:须计入 option2(Put)腿。"""
from __future__ import annotations
from app.sim.pnl import summarize_fills_pnl
def test_summarize_oo_both_legs() -> None:
fills = [
{
"leg": "option",
"action": "open",
"side": "long",
"fill_px": 12.6,
"qty_eth": 3.5,
"fee": 0.0221,
"slip": 0,
},
{
"leg": "option2",
"action": "open",
"side": "long",
"fill_px": 9.0,
"qty_eth": 5.0,
"fee": 0.0225,
"slip": 0,
},
{
"leg": "option",
"action": "close",
"side": "sell",
"fill_px": 0.0,
"qty_eth": 3.5,
"fee": 0.0,
"slip": 0,
},
{
"leg": "option2",
"action": "close",
"side": "sell",
"fill_px": 0.0,
"qty_eth": 5.0,
"fee": 0.0,
"slip": 0,
},
]
s = summarize_fills_pnl(fills)
assert s["option_pnl"] == -12.6 * 3.5
assert s["option2_pnl"] == -9.0 * 5.0
assert s["perp_pnl"] is None
assert abs(float(s["fees_option"] or 0) - 0.0446) < 1e-9
assert abs(float(s["gross_pnl"] or 0) - (-44.1 - 45.0)) < 1e-9
assert abs(float(s["net_pnl"] or 0) - (-89.1 - 0.0446)) < 1e-9
def test_summarize_perp_option_unchanged() -> None:
fills = [
{"leg": "option", "action": "open", "fill_px": 10, "qty_eth": 2, "fee": 0.1, "slip": 0},
{"leg": "perp", "action": "open", "side": "short", "fill_px": 100, "qty_eth": 1, "fee": 0.2, "slip": 0},
{"leg": "option", "action": "close", "fill_px": 12, "qty_eth": 2, "fee": 0.1, "slip": 0},
{"leg": "perp", "action": "close", "side": "short", "fill_px": 98, "qty_eth": 1, "fee": 0.2, "slip": 0},
]
s = summarize_fills_pnl(fills)
assert s["option_pnl"] == 4.0
assert s["perp_pnl"] == 2.0
assert s["option2_pnl"] is None
assert abs(float(s["net_pnl"] or 0) - (6.0 - 0.6)) < 1e-9
+14
View File
@@ -5,6 +5,20 @@
---
## 2026-08-11 — 期期交易记录展示与盈亏汇总
### 变更
1. `summarize_fills_pnl` 计入 `option2`(Put)盈亏与手续费,期期净盈亏不再漏腿。
2. 交易记录列表/详情:期期显示「期期·看涨+看跌」、Call/Put 合约与权利金、双腿结算与 Call/Put 盈亏;成交文案不再出现 `option2平仓sell`
3. 到期结算信息补充 Put 行权价/内在价值。
### 审计
截图 G-20260807-01:详情按永期模板只显 Call,初始权利金漏 Put,成交腿标签乱码,净盈亏汇总缺 Put。
---
## 2026-08-08 — 策略格式三页文档
### 变更
+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>