"""期权已实现盈亏:交易账户 USDT 快照差分(一次一仓). 规格见 docs/规格-期权交易账户盈亏结算.md """ 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 fetch_trading_usdt(cfg: dict[str, Any] | None, ex: Any) -> float | None: """读取交易账户 USDT(可用优先).""" fetch = (cfg or {}).get("fetch_options_balances") if isinstance(cfg, dict) else None if not callable(fetch): from lib.exchange.okx_options_lib import fetch_options_balances fetch = fetch_options_balances try: bal = fetch(ex, force=True) or {} except TypeError: try: bal = fetch(ex) or {} except Exception: return None except Exception: return None v = _safe_float(bal.get("trading_usdt_avail")) if v is None: v = _safe_float(bal.get("trading_usdt")) return round(float(v), 8) if v is not None else None def sellable_coin_blocks_open_msg(ex: Any, underlying: str) -> str | None: """开仓前有可卖标的币则拒开.""" from lib.options.options_spot_bridge_lib import ( fetch_trading_coin_available, is_sellable_spot_coin, ) uly = (underlying or "").strip().upper() if uly not in ("ETH", "BTC"): return None try: avail = fetch_trading_coin_available(ex, uly) except Exception: return None if avail is None: return None if is_sellable_spot_coin(ex, uly, avail): return f"交易账户仍有可卖 {uly}({float(avail):g}),请先卖回 USDT 后再开仓" return None def transfer_blocked_by_position_msg(conn, ex: Any = None) -> str | None: """持仓中或待卖回时禁止划转.""" from lib.options.options_spot_bridge_lib import has_unfinished_bridge try: if has_unfinished_bridge(conn): return "期权持仓或待卖回期间禁止划转,请先平仓并卖回 USDT" except Exception: pass try: row = conn.execute( "SELECT COUNT(*) AS n FROM options_trades WHERE status='open'" ).fetchone() n = int(row["n"] if hasattr(row, "keys") else row[0]) if n > 0: return "期权持仓中禁止划转" except Exception: pass if ex is not None: try: from lib.exchange.okx_options_lib import fetch_option_positions for p in fetch_option_positions(ex) or []: try: if abs(float(p.get("pos") or 0)) > 1e-12: return "存在未平期权持仓,禁止划转" except (TypeError, ValueError): continue except Exception: pass return None def sellable_coin_blocks_settle( ex: Any, underlying: str | None, ) -> str | None: """仍有可卖标的币则禁止账户差分结算(规格:卖光再冻结).""" from lib.options.options_spot_bridge_lib import ( fetch_trading_coin_available, is_sellable_spot_coin, ) uly = (underlying or "").strip().upper() if uly not in ("ETH", "BTC"): return None try: avail = fetch_trading_coin_available(ex, uly) except Exception: return None if avail is None: return None if is_sellable_spot_coin(ex, uly, avail): return f"仍有可卖 {uly}({float(avail):g}),暂不结算账户盈亏" return None def _underlying_for_settle( conn, *, bridge_id: int | None = None, trade_id: int | None = None, ) -> str | None: try: if trade_id is not None: row = conn.execute( "SELECT underlying FROM options_trades WHERE id=?", (int(trade_id),), ).fetchone() if row: return str(row["underlying"] if hasattr(row, "keys") else row[0] or "").strip().upper() or None if bridge_id is not None: row = conn.execute( """ SELECT underlying FROM options_trades WHERE bridge_id=? AND status='closed' ORDER BY id DESC LIMIT 1 """, (int(bridge_id),), ).fetchone() if row: return str(row["underlying"] if hasattr(row, "keys") else row[0] or "").strip().upper() or None brow = conn.execute( "SELECT underlying FROM options_spot_bridge WHERE id=?", (int(bridge_id),), ).fetchone() if brow: return str(brow["underlying"] if hasattr(brow, "keys") else brow[0] or "").strip().upper() or None except Exception: return None return None def try_settle_after_sellback( get_db: Any, ex: Any, *, bridge_id: int | None = None, cfg: dict[str, Any] | None = None, underlying: str | None = None, sell_complete: bool | None = None, ) -> int: """卖回路径通用结算:读交易账户 after 并冻结未结算单. 门禁:sell_complete 显式 False 时不结算;仍有可卖标的币时不结算. """ if sell_complete is False: return 0 pack = cfg if isinstance(cfg, dict) else {"fetch_options_balances": None} import sqlite3 # Windows 上 sqlite3.Connection 的 callable() 可能为 True,不能据此判断 get_db if isinstance(get_db, sqlite3.Connection): conn = get_db close_conn = False elif callable(get_db): conn = get_db() close_conn = True else: conn = get_db close_conn = False try: from lib.options.options_db import init_options_tables init_options_tables(conn) uly = (underlying or "").strip().upper() or None if not uly: uly = _underlying_for_settle(conn, bridge_id=bridge_id) block = sellable_coin_blocks_settle(ex, uly) if block: return 0 try: from lib.exchange.okx_options_lib import invalidate_options_balance_cache invalidate_options_balance_cache() except Exception: pass after = fetch_trading_usdt(pack, ex) n = 0 if bridge_id is not None: n = settle_closed_trades_from_trading_account( conn, trading_usdt_after=after, bridge_id=int(bridge_id) ) if n < 1: row = conn.execute( """ SELECT id FROM options_trades WHERE status='closed' AND COALESCE(pnl_settled,0)=0 AND trading_usdt_before IS NOT NULL ORDER BY id DESC LIMIT 1 """ ).fetchone() if row: tid = int(row["id"] if hasattr(row, "keys") else row[0]) if not uly: uly = _underlying_for_settle(conn, trade_id=tid) block2 = sellable_coin_blocks_settle(ex, uly) if block2: return 0 n = settle_closed_trades_from_trading_account( conn, trading_usdt_after=after, trade_id=tid, ) try: conn.commit() except Exception: pass return n finally: if close_conn: try: conn.close() except Exception: pass def settle_closed_trades_from_trading_account( conn, *, trading_usdt_after: float | None = None, bridge_id: int | None = None, trade_id: int | None = None, ) -> int: """卖回后:拍 after 快照,并冻结复盘真盈亏. realized_pnl_usdt = (权利金+币盈亏)×卖回VWAP − 初始权利金U; 不含现货多买.账户 after 仅审计.已 pnl_settled=1 的行永不改写. """ from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade after = _safe_float(trading_usdt_after) if after is not None: after = round(float(after), 8) clauses = ["status='closed'", "COALESCE(pnl_settled, 0)=0"] params: list[Any] = [] if trade_id is not None: clauses.append("id=?") params.append(int(trade_id)) elif bridge_id is not None: clauses.append("bridge_id=?") params.append(int(bridge_id)) else: return 0 from lib.options.options_pnl_settle_lib import resolve_spot_vwap_prices rows = conn.execute( f""" SELECT id, trading_usdt_before, premium_paid, premium_received, realized_pnl, open_index_px, close_index_px, open_fee, close_fee, premium_ccy, fee_ccy, bridge_id, initial_premium_usdt FROM options_trades WHERE {" AND ".join(clauses)} """, tuple(params), ).fetchall() updated = 0 for row in rows: tid = int(row["id"] if hasattr(row, "keys") else row[0]) before = _safe_float( row["trading_usdt_before"] if hasattr(row, "keys") else row[1] ) after_use = after if after_use is None and before is not None: after_use = None row_bid = row["bridge_id"] if hasattr(row, "keys") else row[11] if row_bid is None and bridge_id is not None: row_bid = bridge_id initial_u = ( row["initial_premium_usdt"] if hasattr(row, "keys") else row[12] ) open_px, close_px, _gate = resolve_spot_vwap_prices( conn, bridge_id=row_bid, open_index_px=row["open_index_px"] if hasattr(row, "keys") else row[5], close_index_px=row["close_index_px"] if hasattr(row, "keys") else row[6], ) pnl_u, src = resolve_true_pnl_usdt_from_trade( premium_paid=row["premium_paid"] if hasattr(row, "keys") else row[2], premium_received=row["premium_received"] if hasattr(row, "keys") else row[3], realized_pnl=row["realized_pnl"] if hasattr(row, "keys") else row[4], open_index_px=open_px, close_index_px=close_px, initial_premium_usdt=initial_u, open_fee=row["open_fee"] if hasattr(row, "keys") else row[7], close_fee=row["close_fee"] if hasattr(row, "keys") else row[8], premium_ccy=row["premium_ccy"] if hasattr(row, "keys") else row[9], fee_ccy=row["fee_ccy"] if hasattr(row, "keys") else row[10], conn=conn, bridge_id=row_bid, ) if pnl_u is None or src in ("none", "pending_sell"): continue # 卖回价冻为卖回 VWAP;开仓价/初始权利金已在开仓时冻住,仅缺省时补 paid = _safe_float(row["premium_paid"] if hasattr(row, "keys") else row[2]) heal_initial = _safe_float(initial_u) if heal_initial is None and paid is not None and open_px is not None and open_px > 0: heal_initial = round(float(paid) * float(open_px), 4) conn.execute( """ UPDATE options_trades SET trading_usdt_after = COALESCE(?, trading_usdt_after), realized_pnl_usdt = ?, open_index_px = COALESCE(open_index_px, ?), close_index_px = COALESCE(?, close_index_px), initial_premium_usdt = COALESCE(initial_premium_usdt, ?), pnl_settled = 1 WHERE id = ? AND COALESCE(pnl_settled, 0)=0 """, (after_use, pnl_u, open_px, close_px, heal_initial, tid), ) updated += 1 return updated