Files
eth_hedge_sim/backend/app/live/live_pnl.py
T
dekun 9d3b36826c Include LIVE close-fee estimate in target PnL; rest after manual and emergency close.
Align docs so LIVE exit matches SIM fee buffer; manual full close now enters rest_seconds like auto/emergency.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 22:58:10 +08:00

169 lines
5.5 KiB
Python

"""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) # 期权净盈亏(本地)
# 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
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"] = est_close
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)