6c49c7d51c
Co-authored-by: Cursor <cursoragent@cursor.com>
174 lines
6.2 KiB
Python
174 lines
6.2 KiB
Python
"""期权持仓展示(实例页 / 中控快照共用)."""
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from lib.options.options_db import init_options_tables, sum_open_premium_paid
|
||
from lib.options.options_history_lib import enrich_position_row_display
|
||
from lib.options.options_close_gate_lib import clear_close_gate, is_close_gate_passed, update_close_gate
|
||
from lib.options.options_pricing_lib import estimate_close_by_bids, intrinsic_px_per_unit
|
||
|
||
|
||
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 attach_close_preview(
|
||
cfg: dict[str, Any],
|
||
ex: Any,
|
||
row: dict[str, Any],
|
||
*,
|
||
sheets: int | None = None,
|
||
premium_paid: float | None = None,
|
||
) -> dict[str, Any]:
|
||
inst_id = str(row.get("inst_id") or row.get("instId") or "").strip()
|
||
if not inst_id:
|
||
return row
|
||
ct_mult = float(row.get("ct_mult") or 0.01)
|
||
target_sheets = int(sheets) if sheets is not None else int(abs(_safe_float(row.get("pos")) or 0))
|
||
paid = premium_paid if premium_paid is not None else _safe_float(row.get("premium_paid"))
|
||
book = cfg["fetch_option_book_depth"](ex, inst_id, 5)
|
||
row["bid_depth"] = book.get("bids") or []
|
||
row["ask_depth"] = book.get("asks") or []
|
||
mark_px = _safe_float(row.get("mark_px") or row.get("markPx"))
|
||
intrinsic = intrinsic_px_per_unit(
|
||
row.get("opt_type") or row.get("optType"),
|
||
_safe_float(row.get("strike") or row.get("stk")),
|
||
_safe_float(row.get("idx_px") or row.get("idxPx")),
|
||
)
|
||
# 与实盘一致:只按买一估算本轮可平
|
||
preview = estimate_close_by_bids(
|
||
row["bid_depth"],
|
||
target_sheets,
|
||
ct_mult=ct_mult,
|
||
premium_paid=paid,
|
||
mark_px=mark_px,
|
||
intrinsic_px=intrinsic,
|
||
max_levels=1,
|
||
)
|
||
premium_ccy = str(row.get("premium_ccy") or "USDC").strip().upper() or "USDC"
|
||
# 残档时不累计 2×门控;有效买一时刷新计时(仅自动平仓需要)
|
||
if preview.get("bid_invalid") or preview.get("auto_close_blocked"):
|
||
gate = update_close_gate(
|
||
inst_id, recycle_usdc=None, premium_paid=paid, premium_ccy=premium_ccy
|
||
)
|
||
preview["close_gate"] = gate
|
||
preview["close_gate_blocked"] = True
|
||
preview["close_gate_msg"] = preview.get("bid_invalid_reason") or gate.get("msg")
|
||
preview["manual_close_blocked"] = True
|
||
preview["liquidity_ok"] = False
|
||
else:
|
||
gate = update_close_gate(
|
||
inst_id,
|
||
recycle_usdc=_safe_float(preview.get("total_received")),
|
||
premium_paid=paid,
|
||
premium_ccy=premium_ccy,
|
||
)
|
||
passed = bool(gate.get("passed") or is_close_gate_passed(inst_id) or gate.get("ready"))
|
||
preview["close_gate"] = gate
|
||
preview["close_gate_blocked"] = not passed
|
||
preview["close_gate_msg"] = gate.get("msg")
|
||
preview["manual_close_blocked"] = False
|
||
preview["liquidity_ok"] = True
|
||
if not passed:
|
||
preview["auto_close_blocked"] = True
|
||
row["close_preview"] = preview
|
||
return row
|
||
|
||
|
||
def forget_close_gate_for_inst(inst_id: str) -> None:
|
||
clear_close_gate(inst_id)
|
||
|
||
|
||
def net_pnl_from_display_row(row: dict[str, Any]) -> float | None:
|
||
"""与持仓卡「净盈亏」同口径:买一可回收 − 权利金;残档买一则无净值."""
|
||
preview = row.get("close_preview") if isinstance(row.get("close_preview"), dict) else {}
|
||
if preview.get("bid_invalid"):
|
||
return None
|
||
net = preview.get("estimated_pnl")
|
||
if net is not None:
|
||
try:
|
||
return float(net)
|
||
except (TypeError, ValueError):
|
||
pass
|
||
# 仅当实际吃到买盘张数时,才用 total_received − 权利金(避免 bid 无效时 total_received=0 算出 −权利金假亏)
|
||
try:
|
||
covered = float(preview.get("covered_sheets") or 0)
|
||
except (TypeError, ValueError):
|
||
covered = 0.0
|
||
recv = _safe_float(preview.get("total_received"))
|
||
paid = _safe_float(row.get("premium_paid"))
|
||
if covered > 0 and recv is not None and paid is not None:
|
||
return round(recv - paid, 4)
|
||
return None
|
||
|
||
|
||
def display_pnl_from_option_row(row: dict[str, Any]) -> float | None:
|
||
"""展示用盈亏:优先买一净盈亏;残档/无买一时回退交易所标记浮盈 upl."""
|
||
net = net_pnl_from_display_row(row)
|
||
if net is not None:
|
||
return net
|
||
return _safe_float(row.get("upl"))
|
||
|
||
|
||
def sum_options_net_pnl_usdc(
|
||
cfg: dict[str, Any],
|
||
ex: Any,
|
||
raw_positions: list[dict[str, Any]] | None = None,
|
||
) -> float | None:
|
||
"""
|
||
期权浮盈合计(USDC),与顶栏实时盈亏/中控口径对齐为「净盈亏」:
|
||
各仓买一可回收 − 权利金之和;残档则回退该仓交易所 upl.
|
||
获取失败返回 None;无持仓返回 0.
|
||
"""
|
||
raw = raw_positions
|
||
if raw is None:
|
||
raw = cfg["fetch_option_positions"](ex)
|
||
if raw is None:
|
||
return None
|
||
if not raw:
|
||
return 0.0
|
||
positions = build_display_option_positions(cfg, ex, raw)
|
||
total = 0.0
|
||
found = False
|
||
for p in positions:
|
||
pnl = display_pnl_from_option_row(p)
|
||
if pnl is None:
|
||
continue
|
||
found = True
|
||
total += float(pnl)
|
||
return round(total, 4) if found else (0.0 if not positions else None)
|
||
|
||
|
||
def build_display_option_positions(
|
||
cfg: dict[str, Any],
|
||
ex: Any,
|
||
raw_positions: list[dict[str, Any]],
|
||
) -> list[dict[str, Any]]:
|
||
"""与实例 /api/options/positions 相同 enrichment + close_preview."""
|
||
meta_cache: dict[str, dict[str, Any] | None] = {}
|
||
rows: list[dict[str, Any]] = []
|
||
conn = cfg["get_db"]()
|
||
try:
|
||
init_options_tables(conn)
|
||
for p in raw_positions:
|
||
inst = str(p.get("instId") or "").strip()
|
||
premium_override = sum_open_premium_paid(conn, inst) if inst else None
|
||
row = enrich_position_row_display(
|
||
cfg,
|
||
ex,
|
||
p,
|
||
meta_cache=meta_cache,
|
||
premium_override=premium_override,
|
||
)
|
||
attach_close_preview(cfg, ex, row, premium_paid=_safe_float(row.get("premium_paid")))
|
||
rows.append(row)
|
||
finally:
|
||
conn.close()
|
||
return rows
|