"""已实现盈亏结算:扣手续费,并在平仓时按指数冻结 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, sell_back_px: Any, initial_premium_usdt: Any = None, open_index_px: Any = None, ) -> float | None: """复盘真盈亏(U):币本位×卖回价 − 初始权利金U. 币本位 = 权利金 + 币盈亏 真盈亏U = 币本位 × 卖回VWAP − 初始权利金U 初始权利金U 优先用开仓冻结值;缺失时才用 权利金×开仓价 补算. 现货多买数量不进公式(只影响均价). """ pnl = _safe_float(coin_pnl) paid = _safe_float(premium_paid) close_px = _safe_float(sell_back_px) if pnl is None or paid is None: return None if close_px is None or close_px <= 0: return None initial = _safe_float(initial_premium_usdt) if initial is None: open_px = _safe_float(open_index_px) if open_px is None or open_px <= 0: return None initial = float(paid) * float(open_px) coin_basis = float(paid) + float(pnl) return round(coin_basis * float(close_px) - float(initial), 4) def resolve_spot_vwap_prices( conn: Any = None, *, bridge_id: Any = None, open_index_px: Any = None, close_index_px: Any = None, ) -> tuple[float | None, float | None, str]: """开仓价/卖回价解析. 开仓价:优先已冻结 open_index_px,缺失才补桥买 VWAP(不覆盖已冻值). 卖回价:有桥时只认 close_sell VWAP;桥未关→pending_sell;已关无卖回→missing. 无桥时 close_index_px 视为调用方传入的卖回价(测试/无现货桥). 返回 (open_px, close_px, gate): gate='' 可算; 'pending_sell' 卖回未完成; 'missing' 缺价 """ open_px = _safe_float(open_index_px) close_hint = _safe_float(close_index_px) close_px: float | None = None bid = None try: if bridge_id is not None and str(bridge_id).strip() != "": bid = int(bridge_id) except (TypeError, ValueError): bid = None if conn is not None and bid is not None: try: from lib.options.options_spot_bridge_lib import ( BRIDGE_CLOSED, bridge_buy_vwap, bridge_sell_vwap, ensure_bridge_table, ) ensure_bridge_table(conn) row = conn.execute( "SELECT status FROM options_spot_bridge WHERE id=?", (bid,), ).fetchone() if row is None: # 桥记录不存在:按无桥处理,close_index 视为调用方卖回价 close_px = close_hint else: if open_px is None or open_px <= 0: bv = bridge_buy_vwap(conn, bid) if bv is not None and bv > 0: open_px = bv st = str( row["status"] if hasattr(row, "keys") else (row[0] if row else "") or "" ) if st and st != BRIDGE_CLOSED: return open_px, None, "pending_sell" sv = bridge_sell_vwap(conn, bid) if sv is not None and sv > 0: close_px = sv else: # 有桥却无卖回成交:禁止用指数/旧 close_index 冒充 return open_px, None, "missing" except Exception: pass else: close_px = close_hint if close_px is None or close_px <= 0: return open_px, close_px, "missing" return open_px, close_px, "" 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, initial_premium_usdt: Any = None, open_fee: Any = 0, close_fee: Any = 0, premium_ccy: str | None = None, fee_ccy: str | None = None, conn: Any = None, bridge_id: Any = None, ) -> tuple[float | None, str]: """由成交字段算复盘真盈亏: (权利金+币盈亏)×卖回价 − 初始权利金U. 返回 (pnl_u, source): premium_move | stable | pending_sell | none """ del fee_ccy 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, close_px, gate = resolve_spot_vwap_prices( conn, bridge_id=bridge_id, open_index_px=open_index_px, close_index_px=close_index_px, ) if gate == "pending_sell": return None, "pending_sell" usdt = true_pnl_usdt_premium_move( coin_pnl=coin, premium_paid=paid, sell_back_px=close_px, initial_premium_usdt=initial_premium_usdt, open_index_px=open_px, ) if usdt is not None: return usdt, "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(费只扣进币盈亏一次). 币盈亏 = 收回 − 已付 − 开/平手续费 真盈亏U = (权利金+币盈亏)×卖回价 − 初始权利金U """ del fee_ccy 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) fee_u = None if open_idx is not None and open_idx > 0 and sell_px is not None and sell_px > 0: fee_u = round(ofee * float(open_idx) + cfee * float(sell_px), 4) elif sell_px is not None and sell_px > 0 and (ofee > 0 or cfee > 0): fee_u = round((ofee + cfee) * float(sell_px), 4) 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, "fee_usdt": fee_u, "premium_ccy": str(premium_ccy or "").strip().upper() or "ETH", } initial_u = float(paid) * float(open_idx) proceeds_u = float(recv) * float(sell_px) pnl_u = true_pnl_usdt_premium_move( coin_pnl=net_coin, premium_paid=paid, sell_back_px=sell_px, initial_premium_usdt=initial_u, ) 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": fee_u, "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: (权利金+币盈亏)×卖回价 − 初始权利金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