c2c69e580c
Co-authored-by: Cursor <cursoragent@cursor.com>
369 lines
12 KiB
Python
369 lines
12 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
|
||
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
|