diff --git a/backend/app/live/live_pnl.py b/backend/app/live/live_pnl.py index d9d56ea..03db58e 100644 --- a/backend/app/live/live_pnl.py +++ b/backend/app/live/live_pnl.py @@ -37,8 +37,8 @@ def enrich_live_unrealized( ) -> dict[str, Any]: """ 在 Matcher.unrealized 结果上覆盖 LIVE 口径: - net = 永续交易所UPL + 期权净盈亏(本地) − 已付手续费 + 资金费(signed) - 不再扣预估平仓费。 + net = 永续交易所UPL + 期权净盈亏(本地) − 入场手续费×2 + 资金费(signed) + 离场手续费按入场手续费估算(开+平 ≈ 已付×2)。 """ if not base.get("has_position"): return base @@ -72,25 +72,9 @@ def enrich_live_unrealized( logger.warning("live unrealized exchange overlay failed: %s", e) option_upl = float(base.get("option_upl") or 0.0) # 期权净盈亏(本地) - # LIVE 达标口径:必须扣预估平仓手续费(与 SIM / 策略说明一致) - try: - from ..config import get_settings - - fr = float(get_settings().fee_rate or 0.0005) - except Exception: - fr = 0.0005 - notional_est = abs(float(base.get("perp_notional") or 0.0)) + abs( - float(base.get("option_notional") or 0.0) - ) - if notional_est <= 0: - # 兜底:用标记价粗算 - notional_est = abs(float(base.get("spot") or 0.0)) * ( - abs(float(base.get("perp_qty_eth") or 0.0)) - + abs(float(base.get("option_qty_eth") or 0.0)) - ) - est_close = max(0.0, notional_est * fr * 2.0) - # 资金费 signed:付出为负;再减估平仓费 - net_pnl = perp_upl + option_upl - fees_paid + funding - est_close + # 盯盘/达标:离场费 ≈ 入场费 → 合计扣 已付×2 + est_close = float(fees_paid) + net_pnl = perp_upl + option_upl - fees_paid * 2.0 + funding out = dict(base) out["perp_upl"] = perp_upl diff --git a/backend/app/sim/matcher.py b/backend/app/sim/matcher.py index ca94ca8..c475a5b 100644 --- a/backend/app/sim/matcher.py +++ b/backend/app/sim/matcher.py @@ -1055,9 +1055,7 @@ class Matcher: if opt_mark is not None: option_upl = float(opt_mark) * opt_qty - initial_premium - est_close_fees = est_perp_close_fee + est_opt_close_fee - # 净盈利:永续浮盈 + 期权浮盈 − 预估平仓手续费 - net_pnl = perp_upl + option_upl - est_close_fees + book_close_fees = est_perp_close_fee + est_opt_close_fee entry_idx = float(pos["entry_index_px"] or 0) move = abs(float(index_px) - entry_idx) if index_px is not None and entry_idx else 0.0 @@ -1081,6 +1079,14 @@ class Matcher: strike = float(g["strike"]) if g and g["strike"] is not None else None expiry_ymd = str(g["expiry_ymd"]) if g and g["expiry_ymd"] else None open_at_ms = int(g["open_at_ms"]) if g and g["open_at_ms"] else None + open_fees = abs(float(g["fees"] or 0)) if g else 0.0 + # 净盈利:浮盈 − 入场手续费×2(离场费按入场估算);无入场费时退回盘口估平仓费 + if open_fees > 1e-12: + est_close_fees = open_fees + net_pnl = perp_upl + option_upl - open_fees * 2.0 + else: + est_close_fees = book_close_fees + net_pnl = perp_upl + option_upl - est_close_fees expiry_ms = None if expiry_ymd and len(expiry_ymd) == 6: try: @@ -1120,6 +1126,7 @@ class Matcher: "expiry_ms": expiry_ms, "perp_upl": perp_upl, "option_upl": option_upl, + "fees_paid": open_fees, "est_close_fees": est_close_fees, "net_pnl": net_pnl, "index_px": index_px, diff --git a/backend/tests/test_live_pnl.py b/backend/tests/test_live_pnl.py index 52d5936..8614a75 100644 --- a/backend/tests/test_live_pnl.py +++ b/backend/tests/test_live_pnl.py @@ -45,7 +45,7 @@ class _FakeClient: return -1.5 -def test_enrich_live_unrealized_no_est_close_fee() -> None: +def test_enrich_live_unrealized_entry_fee_times_two() -> None: base = { "has_position": True, "group_id": "G1", @@ -73,7 +73,8 @@ def test_enrich_live_unrealized_no_est_close_fee() -> None: assert out["option_upl"] == 5.0 assert out["fees_paid"] == 0.5 assert out["funding_usdt"] == -1.5 - assert out["est_close_fees"] == 0.0 - # 8 + 5 - 0.5 + (-1.5) = 11 - assert abs(out["net_pnl"] - 11.0) < 1e-9 + # 离场费按入场估算 + assert out["est_close_fees"] == 0.5 + # 8 + 5 - 0.5*2 + (-1.5) = 10.5 + assert abs(out["net_pnl"] - 10.5) < 1e-9 assert out["pnl_source"] == "live_exchange"