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:
+99
-28
@@ -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
@@ -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_total(SIM 记账;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_total(SIM 记账;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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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