"""已实现盈亏结算:扣手续费,并在平仓时按指数冻结 U 口径.""" from __future__ import annotations from typing import Any def _safe_float(v: Any) -> float | None: if v is None or v == "": return None try: return float(v) except (TypeError, ValueError): return None def fee_amount_abs(fee: Any) -> float: """OKX 扣费多为负;统一为正数「已付手续费」.""" f = _safe_float(fee) if f is None: return 0.0 return abs(float(f)) def fee_to_premium_ccy( fee_amt: Any, fee_ccy: Any, *, premium_ccy: str, index_px: float | None, ) -> float: """把手续费折成权利金币种数量(与 premium_paid/received 同单位).""" amt = fee_amount_abs(fee_amt) if amt <= 0: return 0.0 fc = str(fee_ccy or "").strip().upper() pc = str(premium_ccy or "").strip().upper() if not fc or not pc or fc == pc: return round(amt, 8) # 稳定币互兑近似 1:1 stables = {"USDT", "USDC", "USD"} if fc in stables and pc in stables: return round(amt, 8) idx = _safe_float(index_px) if idx is None or idx <= 0: return 0.0 if fc in stables and pc in ("ETH", "BTC"): return round(amt / float(idx), 8) if fc in ("ETH", "BTC") and pc in stables: return round(amt * float(idx), 8) if fc in ("ETH", "BTC") and pc in ("ETH", "BTC") and fc != pc: # 跨币种无可靠汇率时放弃折算,避免脏数据 return 0.0 return round(amt, 8) def extract_order_fee(order: dict[str, Any] | None) -> tuple[float, str]: """从下单/查单结果取手续费(原始币种,正数).""" if not isinstance(order, dict): return 0.0, "" raw = order.get("raw") if isinstance(order.get("raw"), dict) else {} info = order.get("info") if isinstance(order.get("info"), dict) else {} fee = order.get("fee") if fee is None: fee = raw.get("fee") if fee is None: fee = info.get("fee") ccy = ( order.get("fee_ccy") or raw.get("feeCcy") or raw.get("fee_ccy") or info.get("fee_ccy") or info.get("feeCcy") or "" ) return fee_amount_abs(fee), str(ccy or "").strip().upper() def sum_option_order_fees_in_premium( ex: Any, *, inst_id: str, ord_ids: list[str] | str | None, premium_ccy: str, index_px: float | None, fetch_order_fn: Any = None, ) -> float: """按订单号汇总手续费并折成权利金单位.""" if isinstance(ord_ids, str): ids = [p.strip() for p in ord_ids.split(",") if p.strip()] else: ids = [str(x).strip() for x in (ord_ids or []) if str(x).strip()] if not ids: return 0.0 if fetch_order_fn is None: from lib.exchange.okx_options_lib import fetch_option_order fetch_order_fn = fetch_option_order total = 0.0 seen: set[str] = set() for oid in ids: if oid in seen: continue seen.add(oid) try: info = fetch_order_fn(ex, inst_id=inst_id, ord_id=oid) except Exception: continue if not isinstance(info, dict) or not info.get("ok"): continue fee_amt, fee_ccy = extract_order_fee(info) total += fee_to_premium_ccy( fee_amt, fee_ccy, premium_ccy=premium_ccy, index_px=index_px ) return round(total, 8) def freeze_pnl_to_usdt( net_pnl: float, *, premium_ccy: str, index_px: float | None, total_fee: float = 0.0, ) -> dict[str, float | None]: """把已是「净额」的权利金盈亏冻结为 U(不再扣费).""" net = round(float(net_pnl), 8) pc = str(premium_ccy or "").strip().upper() idx = _safe_float(index_px) fee_total = abs(float(total_fee or 0.0)) if pc in ("ETH", "BTC"): if idx is None or idx <= 0: return { "realized_pnl": net, "close_index_px": None, "realized_pnl_usdt": None, "total_fee": round(fee_total, 8), } return { "realized_pnl": net, "close_index_px": float(idx), "realized_pnl_usdt": round(net * float(idx), 4), "total_fee": round(fee_total, 8), } return { "realized_pnl": net, "close_index_px": float(idx) if idx is not None and idx > 0 else None, "realized_pnl_usdt": round(net, 4), "total_fee": round(fee_total, 8), } def fee_to_usdt( fee_amt: Any, fee_ccy: Any, *, index_px: float | None, ) -> float: """手续费折 USDT.""" amt = fee_amount_abs(fee_amt) if amt <= 0: return 0.0 fc = str(fee_ccy or "").strip().upper() if fc in ("USDT", "USDC", "USD", ""): # 空币种时按稳定币近似,避免把小数额 ETH 费当成整 U if fc == "" and amt < 0.05: idx = _safe_float(index_px) if idx is not None and idx > 100: return round(amt * float(idx), 4) return round(amt, 4) if fc in ("ETH", "BTC"): idx = _safe_float(index_px) if idx is None or idx <= 0: return 0.0 return round(amt * float(idx), 4) return round(amt, 4) def true_pnl_usdt_premium_move( *, coin_pnl: Any, premium_paid: Any, open_index_px: Any, sell_back_px: Any, ) -> float | None: """复盘真盈亏(U):计入权利金 USDT 波动,忽略现货多买. 真盈亏 = 期权历史盈亏(币)×平仓卖回价 − (权利金×开仓价 − 权利金×平仓卖回价) = 历史币盈亏×卖回价 − 权利金×(开仓价 − 卖回价) 无手续费时等价于: 收回×卖回价 − 已付×开仓价. 币盈亏须已含期权开/平手续费(与期权历史同口径). """ pnl = _safe_float(coin_pnl) paid = _safe_float(premium_paid) open_px = _safe_float(open_index_px) close_px = _safe_float(sell_back_px) if pnl is None or paid is None: return None if open_px is None or open_px <= 0 or close_px is None or close_px <= 0: return None prem_move = float(paid) * float(open_px) - float(paid) * float(close_px) return round(float(pnl) * float(close_px) - prem_move, 4) def resolve_true_pnl_usdt_from_trade( *, premium_paid: Any = None, premium_received: Any = None, realized_pnl: Any = None, open_index_px: Any = None, close_index_px: Any = None, open_fee: Any = 0, close_fee: Any = 0, premium_ccy: str | None = None, fee_ccy: str | None = None, ) -> tuple[float | None, str]: """由成交字段算复盘真盈亏;不含现货多买/账户差分. 返回 (pnl_u, source): premium_move | stable | none """ del fee_ccy # 费率折 U 已体现在历史币盈亏里;本函数不重扣 pc = str(premium_ccy or "").strip().upper() paid = _safe_float(premium_paid) recv = _safe_float(premium_received) ofee = abs(float(_safe_float(open_fee) or 0.0)) cfee = abs(float(_safe_float(close_fee) or 0.0)) coin = _safe_float(realized_pnl) if coin is None and paid is not None and recv is not None: coin = round(float(recv) - float(paid) - ofee - cfee, 8) if pc not in ("ETH", "BTC"): if coin is not None: return round(float(coin), 4), "stable" return None, "none" open_px = _safe_float(open_index_px) close_px = _safe_float(close_index_px) # 优先严格按用户式:历史币盈亏×卖回价 − 权利金×(开仓价−卖回价) usdt = true_pnl_usdt_premium_move( coin_pnl=coin, premium_paid=paid, open_index_px=open_px, sell_back_px=close_px, ) if usdt is not None: return usdt, "premium_move" # 无历史币盈亏时回退收回/已付精确折 U if ( paid is not None and recv is not None and open_px is not None and open_px > 0 and close_px is not None and close_px > 0 ): settled = settle_sim_coin_pnl_usdt( premium_paid=paid, premium_received=recv, open_fee=ofee, close_fee=cfee, open_index_px=open_px, sell_back_px=close_px, premium_ccy=pc, ) usdt2 = _safe_float(settled.get("realized_pnl_usdt")) if usdt2 is not None: return usdt2, "premium_move" return None, "none" def settle_sim_coin_pnl_usdt( *, premium_paid: Any, premium_received: Any, open_fee: Any = 0, close_fee: Any = 0, open_index_px: float | None, sell_back_px: float | None, fee_ccy: str | None = None, premium_ccy: str = "ETH", ) -> dict[str, float | None]: """币本位真盈亏(U):含权利金 USDT 波动,不含现货多买. = 历史币盈亏×卖回价 − (权利金×开仓价 − 权利金×卖回价) = 收回币×卖回价 − 已付币×开仓指数 − 手续费U 不用买币预算当成本(预算含缓冲). """ del fee_ccy # 手续费按权利金币种折 U paid = float(_safe_float(premium_paid) or 0.0) recv = float(_safe_float(premium_received) or 0.0) ofee = abs(float(_safe_float(open_fee) or 0.0)) cfee = abs(float(_safe_float(close_fee) or 0.0)) open_idx = _safe_float(open_index_px) sell_px = _safe_float(sell_back_px) net_coin = round(recv - paid - ofee - cfee, 8) if open_idx is None or open_idx <= 0 or sell_px is None or sell_px <= 0: return { "realized_pnl": net_coin, "close_index_px": float(sell_px) if sell_px is not None and sell_px > 0 else None, "open_index_px": float(open_idx) if open_idx is not None and open_idx > 0 else None, "realized_pnl_usdt": None, "total_fee": round(ofee + cfee, 8), "initial_premium_usdt": None, "proceeds_usdt": None, } # 权利金btc+盈利btc = 平仓收回币 proceeds_u = float(recv) * float(sell_px) initial_u = float(paid) * float(open_idx) # 开仓费按开仓指数、平仓费按卖回价折 U fee_u = ofee * float(open_idx) + cfee * float(sell_px) pnl_u = round(proceeds_u - initial_u - fee_u, 4) return { "realized_pnl": net_coin, "close_index_px": float(sell_px), "open_index_px": float(open_idx), "realized_pnl_usdt": pnl_u, "total_fee": round(ofee + cfee, 8), "initial_premium_usdt": round(initial_u, 4), "proceeds_usdt": round(proceeds_u, 4), "fee_usdt": round(fee_u, 4), "premium_ccy": str(premium_ccy or "").strip().upper() or "ETH", } def settle_closed_pnl( *, premium_paid: Any, premium_received: Any, open_fee: Any = 0, close_fee: Any = 0, premium_ccy: str, index_px: float | None, open_index_px: float | None = None, budget_usdt: float | None = None, fee_ccy: str | None = None, ) -> dict[str, float | None]: """ 由「毛权利金」结算. 币数量净盈亏 = 收回 − 已付 − 开/平手续费(只扣一次). 币本位 U: 收回×卖回价(index_px) − 已付×开仓指数 − 手续费U (开仓指数缺失时回退为净币×平仓指数) USDC: 净额原样为 U. 注意:不要用 budget_usdt 当开仓成本——它含现货买入缓冲. 不要对 OKX realizedPnl 再调此函数扣费. """ del budget_usdt # 买币预算不是权利金成本 paid = float(_safe_float(premium_paid) or 0.0) recv = float(_safe_float(premium_received) or 0.0) ofee = float(_safe_float(open_fee) or 0.0) cfee = float(_safe_float(close_fee) or 0.0) if ofee < 0: ofee = abs(ofee) if cfee < 0: cfee = abs(cfee) net = round(recv - paid - ofee - cfee, 8) pc = str(premium_ccy or "").strip().upper() if pc in ("ETH", "BTC"): open_idx = _safe_float(open_index_px) sell_px = _safe_float(index_px) if open_idx is not None and open_idx > 0 and sell_px is not None and sell_px > 0: out = settle_sim_coin_pnl_usdt( premium_paid=paid, premium_received=recv, open_fee=ofee, close_fee=cfee, open_index_px=open_idx, sell_back_px=sell_px, fee_ccy=fee_ccy, premium_ccy=pc, ) return out # 无开仓指数:回退净币×平仓价 return freeze_pnl_to_usdt( net, premium_ccy=pc, index_px=index_px, total_fee=ofee + cfee, ) out = freeze_pnl_to_usdt( net, premium_ccy=premium_ccy, index_px=index_px, total_fee=ofee + cfee, ) return out def settle_exchange_realized_pnl( *, realized_pnl: Any, premium_ccy: str, index_px: float | None, fee_hint: Any = None, ) -> dict[str, float | None]: """ 冻结交易所 positions-history 的 realizedPnl. 该字段已含手续费,此处只折 U,不再减 fee. """ net = _safe_float(realized_pnl) if net is None: return { "realized_pnl": None, "close_index_px": None, "realized_pnl_usdt": None, "total_fee": fee_amount_abs(fee_hint), } return freeze_pnl_to_usdt( float(net), premium_ccy=premium_ccy, index_px=index_px, total_fee=fee_amount_abs(fee_hint), ) def resolve_close_index_px( ex: Any, *, inst_id: str = "", underlying: str = "", hint: Any = None, ) -> float | None: """平仓冻结用指数:优先调用方传入,否则拉标的近似价.""" h = _safe_float(hint) if h is not None and h > 0: return float(h) underly = (underlying or "").strip().upper() if not underly and inst_id: underly = str(inst_id).split("-")[0].strip().upper() or "ETH" if not underly: underly = "ETH" try: from lib.options.options_stats_lib import _underlying_index_usdt return _underlying_index_usdt(ex, underly) except Exception: return None