Files
crypto_okx/lib/options/options_account_pnl_lib.py
T
dekun 9d77a430e5 fix期权复盘与数据统计双口径盈亏,并修复卖回封顶导致盈利单冻成亏损。
平仓卖光可卖币后再按账户差分结算复盘;数据统计改为历史币盈亏×卖回价;复盘展示初始权利金与盈利率,并附历史修复脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 09:10:13 +08:00

286 lines
8.8 KiB
Python

"""期权已实现盈亏:交易账户 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
after = fetch_trading_usdt(pack, ex)
if after is None:
return 0
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,
bridge_id: int | None = None,
trade_id: int | None = None,
) -> int:
"""卖回后:对本桥/本单未结算已平仓位冻结 after−before.
已 pnl_settled=1 的行永不改写.
"""
after = _safe_float(trading_usdt_after)
if after is None:
return 0
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
FROM options_trades
WHERE {" AND ".join(clauses)}
""",
tuple(params),
).fetchall()
updated = 0
for row in rows:
before = _safe_float(row["trading_usdt_before"] if hasattr(row, "keys") else row[1])
if before is None:
continue
pnl_u = round(float(after) - float(before), 4)
conn.execute(
"""
UPDATE options_trades
SET trading_usdt_after = ?,
realized_pnl_usdt = ?,
pnl_settled = 1
WHERE id = ? AND COALESCE(pnl_settled, 0)=0
""",
(after, pnl_u, int(row["id"] if hasattr(row, "keys") else row[0])),
)
updated += 1
return updated