"""期权已实现盈亏:交易账户 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 resolve_true_pnl_usdt( conn, *, trading_usdt_before: Any = None, trading_usdt_after: Any = None, bridge_id: Any = None, realized_pnl_usdt: Any = None, pnl_settled: Any = False, ) -> tuple[float | None, str]: """真盈亏(口径 A)=卖光标的币收回 USDT − 开仓买币花费. 优先账户快照 after−before(与上式等价);其次已关桥的「卖回累计−买币预算」. 绝不回退权利金×指数(那是口径 B / 期权腿). 返回 (pnl_u, source): account | bridge | frozen | none """ before = _safe_float(trading_usdt_before) after = _safe_float(trading_usdt_after) if before is not None and after is not None: return round(float(after) - float(before), 4), "account" bid = None try: if bridge_id is not None and str(bridge_id).strip() != "": bid = int(bridge_id) except (TypeError, ValueError): bid = None if bid is not None: try: from lib.options.options_spot_bridge_lib import ( BRIDGE_CLOSED, cash_pnl_usdt_from_bridge, ensure_bridge_table, ) ensure_bridge_table(conn) brow = conn.execute( "SELECT status, usdt_recovered FROM options_spot_bridge WHERE id=?", (bid,), ).fetchone() if brow: st = str(brow["status"] if hasattr(brow, "keys") else brow[0] or "") recovered = _safe_float( brow["usdt_recovered"] if hasattr(brow, "keys") else brow[1] ) if st == BRIDGE_CLOSED and recovered is not None: bp = cash_pnl_usdt_from_bridge(conn, bid) if bp is not None: return bp, "bridge" except Exception: pass try: settled = int(pnl_settled or 0) == 1 except (TypeError, ValueError): settled = bool(pnl_settled) frozen = _safe_float(realized_pnl_usdt) # 仅承认「有 before 且已账户结算」的旧冻结;无快照的指数折 U 不算真盈亏 if settled and before is not None and frozen is not None: return round(float(frozen), 4), "frozen" return None, "none" 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−before;账户 after 缺失时用桥「卖回−买币」. 已 pnl_settled=1 的行永不改写. """ after = _safe_float(trading_usdt_after) if after is not None: after = round(float(after), 8) clauses = ["status='closed'", "COALESCE(pnl_settled, 0)=0", "trading_usdt_before IS NOT NULL"] 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 rows = conn.execute( f""" SELECT id, trading_usdt_before, realized_pnl_usdt, bridge_id 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]) if before is None: continue row_bid = row["bridge_id"] if hasattr(row, "keys") else row[3] if row_bid is None and bridge_id is not None: row_bid = bridge_id after_use = after if after_use is not None: pnl_u = round(float(after_use) - float(before), 4) else: pnl_u, src = resolve_true_pnl_usdt( conn, trading_usdt_before=before, trading_usdt_after=None, bridge_id=row_bid, pnl_settled=0, ) if pnl_u is None or src != "bridge": continue after_use = round(float(before) + float(pnl_u), 8) conn.execute( """ UPDATE options_trades SET trading_usdt_after = ?, realized_pnl_usdt = ?, pnl_settled = 1 WHERE id = ? AND COALESCE(pnl_settled, 0)=0 """, (after_use, pnl_u, tid), ) updated += 1 return updated