"""LIVE 组净盈亏:交易所永续 UPL/资金费 + 本地期权净盈亏 − 真实手续费。""" from __future__ import annotations import logging import time from typing import Any, Protocol logger = logging.getLogger(__name__) class _FeeFundingClient(Protocol): def get_perp_upl_usdt(self, *args: Any, **kwargs: Any) -> float | None: ... def get_funding_usdt(self, *args: Any, **kwargs: Any) -> float: ... def get_closed_perp_pnl_usdt(self, *args: Any, **kwargs: Any) -> float | None: ... def group_paid_fees_usdt(db: Any, group_id: str) -> float: rows = db.fetchall( "SELECT fee FROM fills WHERE group_id=?", (group_id,), ) return sum(abs(float(r["fee"] or 0)) for r in rows) def enrich_live_unrealized( *, base: dict[str, Any], db: Any, client: Any, exchange: str, perp_inst_id: str, perp_side: str, open_at_ms: int | None, ) -> dict[str, Any]: """ 在 Matcher.unrealized 结果上覆盖 LIVE 口径: net = 永续交易所UPL + 期权净盈亏(本地) − 已付手续费 + 资金费(signed) 不再扣预估平仓费。 """ if not base.get("has_position"): return base group_id = str(base.get("group_id") or "") fees_paid = group_paid_fees_usdt(db, group_id) if group_id else 0.0 begin = int(open_at_ms or 0) funding = 0.0 perp_upl = float(base.get("perp_upl") or 0.0) ex = (exchange or "").lower() try: if ex == "binance": side = "LONG" if perp_side == "long" else "SHORT" upl = client.get_perp_upl_usdt(perp_inst_id, position_side=side) if upl is not None: perp_upl = float(upl) if begin > 0: funding = float( client.get_funding_usdt(perp_inst_id, begin_ms=begin) or 0.0 ) else: upl = client.get_perp_upl_usdt(perp_inst_id, pos_side=perp_side) if upl is not None: perp_upl = float(upl) if begin > 0: funding = float( client.get_funding_usdt(perp_inst_id, begin_ms=begin) or 0.0 ) except Exception as e: logger.warning("live unrealized exchange overlay failed: %s", e) option_upl = float(base.get("option_upl") or 0.0) # 期权净盈亏(本地) # 资金费 signed:付出为负,直接加总 net_pnl = perp_upl + option_upl - fees_paid + funding out = dict(base) out["perp_upl"] = perp_upl out["option_upl"] = option_upl out["fees_paid"] = fees_paid out["funding_usdt"] = funding out["est_close_fees"] = 0.0 # LIVE 不估平仓费 out["net_pnl"] = net_pnl out["pnl_source"] = "live_exchange" return out def reconcile_closed_group_pnl( *, db: Any, client: Any, exchange: str, group_id: str, perp_inst_id: str, open_at_ms: int | None, local_net: float | None, ) -> float: """ 平仓后回写:净盈亏优先用 交易所永续已实现 + 本地期权腿盈亏 − 手续费 + 资金费。 失败则退回 local_net。 """ from ..sim.pnl import summarize_fills_pnl fills = db.fetchall( "SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,) ) summary = summarize_fills_pnl(list(fills)) fees = float(summary.get("fees_total") or 0.0) option_pnl = summary.get("option_pnl") local_perp = summary.get("perp_pnl") begin = int(open_at_ms or 0) end = int(time.time() * 1000) funding = 0.0 exch_perp: float | None = None ex = (exchange or "").lower() try: if begin > 0: funding = float(client.get_funding_usdt(perp_inst_id, begin_ms=begin, end_ms=end) or 0.0) exch_perp = client.get_closed_perp_pnl_usdt( perp_inst_id, begin_ms=begin, end_ms=end ) except Exception as e: logger.warning("reconcile exchange pnl failed: %s", e) perp_pnl = float(exch_perp) if exch_perp is not None else ( float(local_perp) if local_perp is not None else 0.0 ) opt = float(option_pnl) if option_pnl is not None else 0.0 net = perp_pnl + opt - fees + funding if local_net is not None and exch_perp is None and abs(funding) < 1e-12: # 交易所永续已实现拉不到且无资金费 → 保持本地 net = float(local_net) with db._lock: try: db._conn.execute( "UPDATE groups SET realized_pnl=?, funding_usdt=?, fees=? WHERE group_id=?", (float(net), float(funding), float(fees), group_id), ) db._conn.commit() except Exception: # funding_usdt 列未迁移时降级 db._conn.execute( "UPDATE groups SET realized_pnl=?, fees=? WHERE group_id=?", (float(net), float(fees), group_id), ) db._conn.commit() return float(net)