3491681c28
Co-authored-by: Cursor <cursoragent@cursor.com>
205 lines
7.1 KiB
Python
205 lines
7.1 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 + 期权净盈亏(本地) − 入场手续费×2 + 资金费(signed)
|
|
离场手续费按入场手续费估算(开+平 ≈ 已付×2)。
|
|
"""
|
|
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) # 期权净盈亏(本地)
|
|
option2_upl = float(base.get("option2_upl") or 0.0) # 期期 Put 腿
|
|
# 盯盘/达标:离场费已在 base.net_pnl(盘口可成交价)计入;这里只叠加资金费。
|
|
# 勿用交易所永续标记 UPL 覆盖净利,否则会虚高触发达标、成交后变亏。
|
|
book_net = base.get("net_pnl")
|
|
if book_net is not None:
|
|
net_pnl = float(book_net) + funding
|
|
else:
|
|
fees_est = float(fees_paid) * 2.0
|
|
net_pnl = perp_upl + option_upl + option2_upl - fees_est + funding
|
|
|
|
out = dict(base)
|
|
out["perp_upl"] = float(base.get("perp_upl") or perp_upl)
|
|
out["perp_upl_exchange"] = perp_upl
|
|
out["option_upl"] = option_upl
|
|
out["option2_upl"] = option2_upl
|
|
out["fees_paid"] = fees_paid
|
|
out["funding_usdt"] = funding
|
|
out["est_close_fees"] = float(base.get("est_close_fees") or fees_paid)
|
|
out["net_pnl"] = net_pnl
|
|
out["net_pnl_exchange"] = (
|
|
perp_upl + option_upl + option2_upl - float(fees_paid) * 2.0 + funding
|
|
)
|
|
out["pnl_source"] = "live_book_plus_funding"
|
|
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
|
|
|
|
# 期权平仓 fill 为 0 价镜像时:尝试用交易所交割账单补期权腿
|
|
try:
|
|
zero_opt_close = False
|
|
for fr in fills:
|
|
if str(fr["leg"] or "") in ("option", "option2") and str(
|
|
fr["action"] or ""
|
|
) == "close":
|
|
if abs(float(fr["notional"] or 0)) < 1e-12 and abs(
|
|
float(fr["fill_px"] or 0)
|
|
) < 1e-12:
|
|
zero_opt_close = True
|
|
break
|
|
if zero_opt_close:
|
|
from .option_settle import fetch_option_settlement
|
|
|
|
g = db.fetchone(
|
|
"SELECT option_inst_id, option2_inst_id FROM groups WHERE group_id=?",
|
|
(group_id,),
|
|
)
|
|
settle_cash = 0.0
|
|
for inst_key in ("option_inst_id", "option2_inst_id"):
|
|
inst = str((g[inst_key] if g else None) or "")
|
|
if not inst:
|
|
continue
|
|
st = fetch_option_settlement(
|
|
client,
|
|
exchange=ex,
|
|
option_inst_id=inst,
|
|
qty_eth=1.0,
|
|
begin_ms=begin,
|
|
end_ms=end,
|
|
)
|
|
if st.found:
|
|
settle_cash += float(st.cash)
|
|
if abs(settle_cash) > 1e-12:
|
|
# 用交割净现金替换本地 0 价期权盈亏近似:仍减 fees(交割费若已在 cash 内则可能双计,保守保留)
|
|
opt = float(settle_cash)
|
|
except Exception as e:
|
|
logger.warning("reconcile option settlement overlay failed: %s", e)
|
|
|
|
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)
|