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

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

865 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""期权本地交易统计(胜率 / 盈亏 / 持仓时长)."""
from __future__ import annotations
from datetime import datetime
from typing import Any
from lib.instance.instance_embed_context_lib import profit_loss_ratio_from_averages
from lib.options.options_db import init_options_tables
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 _underlying_index_usdt(ex: Any, underly: str) -> float | None:
"""取标的 USDT 近似指数(币本位已平盈亏折 U).优先公开 ticker,避免私钥失败."""
u = (underly or "ETH").strip().upper() or "ETH"
pubs: list[Any] = []
try:
from lib.sim.hooks import _APP_MODULE, _sim_public_exchange
pub = _sim_public_exchange(ex) if _APP_MODULE is not None else None
if pub is not None:
pubs.append(pub)
except Exception:
pass
if ex is not None and ex not in pubs:
pubs.append(ex)
from lib.exchange.okx_options_lib import fetch_index_price
for pub in pubs:
try:
if hasattr(pub, "public_get_market_ticker"):
rows = (pub.public_get_market_ticker({"instId": f"{u}-USDT"}) or {}).get("data") or []
if rows:
last = _safe_float(rows[0].get("last") or rows[0].get("lastPx"))
if last is not None and last > 0:
return float(last)
except Exception:
pass
try:
px = fetch_index_price(pub, f"{u}-USD")
if px is not None and float(px) > 0:
return float(px)
except Exception:
pass
try:
t = pub.fetch_ticker(f"{u}/USDT") or {}
last = _safe_float(t.get("last") or t.get("close"))
if last is not None and last > 0:
return float(last)
except Exception:
continue
return None
def history_pnl_to_usdt(
history: list[dict[str, Any]],
ex: Any = None,
*,
prefer_frozen_usdt: bool = True,
) -> list[dict[str, Any]]:
"""折 U.
prefer_frozen_usdt=True:优先行内 realized_pnl_usdt(实盘 exchange freeze / 旧兼容).
prefer_frozen_usdt=False:口径 B — 币盈亏 × 平仓卖回价(close_index_px),忽略账户差分 A.
"""
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
idx_cache: dict[str, float | None] = {}
out: list[dict[str, Any]] = []
for row in history:
r = dict(row)
if r.get("status") == "open":
out.append(r)
continue
frozen = _safe_float(r.get("realized_pnl_usdt"))
if prefer_frozen_usdt and frozen is not None:
r["realized_pnl"] = round(float(frozen), 4)
out.append(r)
continue
pnl = _safe_float(r.get("realized_pnl"))
if pnl is None:
out.append(r)
continue
inst = str(r.get("inst_id") or "")
underly = str(r.get("underlying") or (inst.split("-")[0] if inst else "ETH") or "ETH")
ccy = str(r.get("premium_ccy") or "").strip().upper()
if not ccy:
ccy = premium_ccy_for_mode(margin_mode_from_inst_id(inst), underly)
if ccy in ("ETH", "BTC"):
idx = _safe_float(r.get("close_index_px"))
if idx is None or idx <= 0:
if underly not in idx_cache:
idx_cache[underly] = _underlying_index_usdt(ex, underly)
idx = idx_cache.get(underly)
if idx is None or idx <= 0:
r["realized_pnl"] = None
else:
r["realized_pnl"] = round(float(pnl) * float(idx), 4)
else:
r["realized_pnl"] = round(float(pnl), 4)
out.append(r)
return out
def history_pnl_to_usdt_stats(history: list[dict[str, Any]], ex: Any = None) -> list[dict[str, Any]]:
"""数据统计口径 B:期权历史币盈亏 × 平仓卖回价."""
return history_pnl_to_usdt(history, ex, prefer_frozen_usdt=False)
def _parse_ts(raw: Any) -> datetime | None:
if raw is None or raw == "":
return None
s = str(raw).strip().replace(" ", "T", 1)
try:
return datetime.fromisoformat(s)
except (TypeError, ValueError):
return None
def _hold_seconds(created_at: Any, closed_at: Any) -> float | None:
start = _parse_ts(created_at)
end = _parse_ts(closed_at)
if start is None or end is None:
return None
sec = (end - start).total_seconds()
return sec if sec >= 0 else None
def _avg_seconds(values: list[float]) -> float | None:
if not values:
return None
return round(sum(values) / len(values), 1)
def compute_options_stats_from_history(history: list[dict[str, Any]]) -> dict[str, Any]:
"""基于期权历史列表(交易所)计算统计."""
wins: list[float] = []
losses: list[float] = []
win_holds: list[float] = []
loss_holds: list[float] = []
all_holds: list[float] = []
open_holds: list[float] = []
now = datetime.now()
for row in history:
if row.get("status") == "open":
start = _parse_ts(row.get("created_at"))
if start is not None:
sec = (now - start).total_seconds()
if sec >= 0:
open_holds.append(sec)
continue
pnl_raw = row.get("realized_pnl")
if pnl_raw is None:
continue
try:
pnl = float(pnl_raw)
except (TypeError, ValueError):
continue
hold = _hold_seconds(row.get("created_at"), row.get("closed_at"))
if hold is not None:
all_holds.append(hold)
if pnl > 0:
wins.append(pnl)
if hold is not None:
win_holds.append(hold)
elif pnl < 0:
losses.append(pnl)
if hold is not None:
loss_holds.append(hold)
total_closed = len(wins) + len(losses)
win_rate = round(len(wins) / total_closed * 100, 2) if total_closed else 0
avg_win = sum(wins) / len(wins) if wins else None
avg_loss = sum(losses) / len(losses) if losses else None
total_profit = round(sum(wins), 4) if wins else 0.0
total_loss = round(abs(sum(losses)), 4) if losses else 0.0
net_realized = round(sum(wins) + sum(losses), 4)
return {
"total_closed": total_closed,
"win_count": len(wins),
"loss_count": len(losses),
"win_rate": win_rate,
"profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss),
"avg_win": round(avg_win, 4) if avg_win is not None else None,
"avg_loss": round(abs(avg_loss), 4) if avg_loss is not None else None,
"total_profit": total_profit,
"total_loss": total_loss,
"net_realized_pnl": net_realized,
"avg_hold_sec": _avg_seconds(all_holds),
"avg_win_hold_sec": _avg_seconds(win_holds),
"avg_loss_hold_sec": _avg_seconds(loss_holds),
"open_count": len(open_holds),
"avg_open_hold_sec": _avg_seconds(open_holds),
}
_STATS_WINDOWS = ("today", "7d", "30d", "all")
def normalize_stats_window(raw: Any) -> str:
w = str(raw or "7d").strip().lower()
if w in ("1d", "day", "今日"):
return "today"
if w in ("7", "week"):
return "7d"
if w in ("30", "month"):
return "30d"
if w in ("0", "全部", "total"):
return "all"
return w if w in _STATS_WINDOWS else "7d"
def stats_window_bounds(window: str, *, now: datetime | None = None) -> tuple[datetime | None, datetime | None]:
"""返回闭区间起止(本地/业务时区近似用本地 now).all → (None, None)."""
w = normalize_stats_window(window)
end = now or datetime.now()
if w == "all":
return None, None
if w == "today":
start = end.replace(hour=0, minute=0, second=0, microsecond=0)
return start, end
days = 7 if w == "7d" else 30
from datetime import timedelta
start = end - timedelta(days=days)
return start, end
def filter_history_for_stats_window(
history: list[dict[str, Any]],
window: str,
*,
now: datetime | None = None,
) -> list[dict[str, Any]]:
"""窗口内已平仓 + 当前持仓(open)."""
start, end = stats_window_bounds(window, now=now)
out: list[dict[str, Any]] = []
for row in history or []:
if row.get("status") == "open":
out.append(row)
continue
if start is None:
out.append(row)
continue
ts = _parse_ts(row.get("closed_at")) or _parse_ts(row.get("created_at"))
if ts is None:
continue
if ts < start:
continue
if end is not None and ts > end:
continue
out.append(row)
return out
def compute_options_stats_for_window(
history: list[dict[str, Any]],
window: str,
*,
now: datetime | None = None,
) -> dict[str, Any]:
w = normalize_stats_window(window)
filtered = filter_history_for_stats_window(history, w, now=now)
stats = compute_options_stats_from_history(filtered)
start, end = stats_window_bounds(w, now=now)
stats["window"] = w
stats["window_start"] = start.strftime("%Y-%m-%d %H:%M:%S") if start else None
stats["window_end"] = end.strftime("%Y-%m-%d %H:%M:%S") if end else None
return stats
def _row_get(row: Any, key: str, default: Any = None) -> Any:
try:
if hasattr(row, "keys") and key in row.keys():
return row[key]
except Exception:
pass
if isinstance(row, dict):
return row.get(key, default)
return default
def _closed_trade_dicts(closed_rows) -> list[dict[str, Any]]:
out: list[dict[str, Any]] = []
for row in closed_rows or []:
out.append(
{
"status": "closed",
"realized_pnl": _row_get(row, "realized_pnl"),
# 口径 B 不读账户 realized_pnl_usdt;保留键位兼容旧调用
"realized_pnl_usdt": None,
"close_index_px": _row_get(row, "close_index_px"),
"created_at": _row_get(row, "created_at"),
"closed_at": _row_get(row, "closed_at"),
"inst_id": _row_get(row, "inst_id") or "",
"underlying": _row_get(row, "underlying") or "",
"premium_ccy": _row_get(row, "premium_ccy") or "",
}
)
return out
# 口径 B:凡有币盈亏的已平均计入;U 由 history_pnl_to_usdt_stats 按卖回价折算
_CLOSED_STATS_SELECT = """
SELECT realized_pnl, close_index_px,
created_at, closed_at, inst_id, underlying, premium_ccy
FROM options_trades
WHERE status = 'closed'
AND realized_pnl IS NOT NULL
"""
def freeze_missing_realized_pnl_usdt(conn, ex: Any = None) -> int:
"""旧已平仓缺冻结 U 时按 close_index/当前指数写一次,之后不再跟盘变.
注意:已有 realized_pnl 视为净额直接冻结,不再二次扣 open_fee/close_fee
(避免对 OKX realizedPnl 或已扣费本地净额重复减费).
"""
init_options_tables(conn)
rows = conn.execute(
"""
SELECT id, realized_pnl, realized_pnl_usdt, close_index_px,
inst_id, underlying, premium_ccy
FROM options_trades
WHERE status = 'closed'
AND realized_pnl_usdt IS NULL
AND realized_pnl IS NOT NULL
AND trading_usdt_before IS NULL
ORDER BY id DESC
LIMIT 300
"""
).fetchall()
if not rows:
return 0
from lib.options.options_pnl_settle_lib import (
freeze_pnl_to_usdt,
resolve_close_index_px,
)
updated = 0
for row in rows:
ccy = str(_row_get(row, "premium_ccy") or "").strip().upper()
idx = _safe_float(_row_get(row, "close_index_px"))
if idx is None or idx <= 0:
idx = resolve_close_index_px(
ex,
inst_id=str(_row_get(row, "inst_id") or ""),
underlying=str(_row_get(row, "underlying") or ""),
)
pnl = _safe_float(_row_get(row, "realized_pnl"))
if pnl is None:
continue
settled = freeze_pnl_to_usdt(pnl, premium_ccy=ccy or "USDC", index_px=idx)
usdt = settled.get("realized_pnl_usdt")
close_idx = settled.get("close_index_px")
if usdt is None:
continue
conn.execute(
"""
UPDATE options_trades
SET realized_pnl_usdt = ?,
close_index_px = COALESCE(close_index_px, ?)
WHERE id = ? AND realized_pnl_usdt IS NULL
""",
(float(usdt), close_idx, int(row["id"])),
)
updated += 1
return updated
def recompute_closed_realized_pnl_usdt(conn, ex: Any = None) -> int:
"""强制按「平仓收回×平仓指数 − 开仓成本U − 手续费」重算已平冻结 U.
旧逻辑用 (收回−已付)×平仓指数,等于把开仓成本也按平仓价重估,ETH 上涨时会偏亏,
导致合计盈亏与余额对不上.有 budget_usdt/open_index_px 时优先用真实开仓成本.
"""
init_options_tables(conn)
rows = conn.execute(
"""
SELECT id, realized_pnl, realized_pnl_usdt, close_index_px, open_index_px,
inst_id, underlying, premium_ccy, premium_paid, premium_received,
open_fee, close_fee, fee_ccy, budget_usdt, close_quote, eth_amount,
open_quote
FROM options_trades
WHERE status = 'closed'
AND premium_paid IS NOT NULL
ORDER BY id DESC
LIMIT 300
"""
).fetchall()
if not rows:
return 0
from lib.options.options_pnl_settle_lib import resolve_close_index_px, settle_closed_pnl
from lib.options.options_pricing_lib import total_premium
updated = 0
for row in rows:
paid = _safe_float(_row_get(row, "premium_paid"))
recv = _safe_float(_row_get(row, "premium_received"))
if recv is None:
cq = _safe_float(_row_get(row, "close_quote"))
eth = _safe_float(_row_get(row, "eth_amount"))
if cq is not None and eth is not None and eth > 0:
recv = float(total_premium(cq, eth))
if paid is None or recv is None:
continue
ofee = abs(_safe_float(_row_get(row, "open_fee")) or 0.0)
cfee = abs(_safe_float(_row_get(row, "close_fee")) or 0.0)
ccy = str(_row_get(row, "premium_ccy") or "").strip().upper() or "ETH"
close_idx = _safe_float(_row_get(row, "close_index_px"))
open_idx = _safe_float(_row_get(row, "open_index_px"))
budget = _safe_float(_row_get(row, "budget_usdt"))
if close_idx is None or close_idx <= 0:
close_idx = resolve_close_index_px(
ex,
inst_id=str(_row_get(row, "inst_id") or ""),
underlying=str(_row_get(row, "underlying") or ""),
)
if (open_idx is None or open_idx <= 0) and budget and paid > 1e-12:
open_idx = float(budget) / float(paid)
settled = settle_closed_pnl(
premium_paid=paid,
premium_received=recv,
open_fee=ofee,
close_fee=cfee,
premium_ccy=ccy,
index_px=close_idx,
open_index_px=open_idx,
budget_usdt=budget,
fee_ccy=str(_row_get(row, "fee_ccy") or ccy),
)
new_usdt = settled.get("realized_pnl_usdt")
if new_usdt is None:
continue
old_usdt = _safe_float(_row_get(row, "realized_pnl_usdt"))
new_pnl = settled.get("realized_pnl")
# 有变化才写,避免无意义刷库
if (
old_usdt is not None
and abs(float(old_usdt) - float(new_usdt)) < 1e-6
and _safe_float(_row_get(row, "realized_pnl")) is not None
and new_pnl is not None
and abs(float(_row_get(row, "realized_pnl")) - float(new_pnl)) < 1e-10
):
continue
conn.execute(
"""
UPDATE options_trades
SET realized_pnl = COALESCE(?, realized_pnl),
close_index_px = COALESCE(?, close_index_px),
open_index_px = COALESCE(open_index_px, ?),
realized_pnl_usdt = ?
WHERE id = ?
""",
(
new_pnl,
settled.get("close_index_px"),
settled.get("open_index_px"),
float(new_usdt),
int(row["id"]),
),
)
updated += 1
return updated
def repair_double_counted_fee_pnl(conn, ex: Any = None) -> int:
"""兼容旧名:改为强制按开仓成本重算冻结 U."""
return recompute_closed_realized_pnl_usdt(conn, ex)
def _sim_net_usdt_converts_for_trade(
conn,
*,
underlying: str,
start_ts: str,
end_ts: str,
) -> float | None:
"""从 sim_ledger 汇总该标的现货兑换的 USDT 净变动(买币为负、卖回为正)."""
coin = (underlying or "").strip().upper()
if coin not in ("ETH", "BTC") or not start_ts or not end_ts:
return None
try:
rows = conn.execute(
"""
SELECT amount, note FROM sim_ledger_entries
WHERE kind = 'convert'
AND UPPER(ccy) = 'USDT'
AND account = 'trading'
AND ts >= ?
AND ts <= ?
""",
(str(start_ts), str(end_ts)),
).fetchall()
except Exception:
return None
total = 0.0
n = 0
for row in rows or []:
note = str(_row_get(row, "note") or "").upper()
amt = _safe_float(_row_get(row, "amount"))
if amt is None:
continue
related = (
f"{coin}/USDT" in note
or f"{coin}-USDT" in note
or note.strip() == f"FROM {coin}"
)
if not related:
continue
total += float(amt)
n += 1
if n < 1:
return None
return round(total, 4)
def sync_sim_cash_realized_pnl_usdt(conn, ex: Any = None) -> int:
"""兼容入口:已按交易账户结算(pnl_settled=1)的永不改写.
有 trading_usdt_before 的待结算单留给卖回结算;
仅无账户快照的旧单才回退权利金×指数冻结.
"""
del ex
init_options_tables(conn)
# 账户快照路径由 sell_residual / maybe_sell 结算,此处不覆盖
return 0
def load_local_options_stats_history(conn, ex: Any = None) -> list[dict[str, Any]]:
"""本地已平(模拟盘口径) + 当前 open."""
init_options_tables(conn)
try:
# 权利金净盈亏冻结 U(纠正旧版把买币预算差额当盈利的数据)
sync_sim_cash_realized_pnl_usdt(conn, ex)
except Exception:
pass
try:
freeze_missing_realized_pnl_usdt(conn, ex)
except Exception:
pass
closed_rows = conn.execute(_CLOSED_STATS_SELECT).fetchall()
open_rows = conn.execute(
"SELECT created_at FROM options_trades WHERE status = 'open'"
).fetchall()
history = _closed_trade_dicts(closed_rows)
for row in open_rows:
history.append({"status": "open", "created_at": _row_get(row, "created_at")})
return history
def _load_exchange_pnl_freeze_map(conn) -> dict[str, dict[str, Any]]:
init_options_tables(conn)
out: dict[str, dict[str, Any]] = {}
try:
rows = conn.execute(
"""
SELECT history_key, realized_pnl_usdt, close_index_px, realized_pnl
FROM options_exchange_pnl_freeze
"""
).fetchall()
except Exception:
return out
for row in rows or []:
key = str(_row_get(row, "history_key") or "").strip()
if not key:
continue
out[key] = {
"realized_pnl_usdt": _row_get(row, "realized_pnl_usdt"),
"close_index_px": _row_get(row, "close_index_px"),
"realized_pnl": _row_get(row, "realized_pnl"),
}
return out
def apply_exchange_pnl_freeze(
conn,
history: list[dict[str, Any]],
ex: Any = None,
) -> list[dict[str, Any]]:
"""写冻结表.行内已有 close_index_px / _freeze_index_hint 时不打网.
若仍缺指数且传了 ex,会在持连接时补拉(仅兼容旧单测).生产请用
load_live_exchange_stats_history(先网后短事务).
"""
freeze_map = _load_exchange_pnl_freeze_map(conn)
enriched: list[dict[str, Any]] = []
need_idx: dict[str, str] = {}
for row in history or []:
r = dict(row)
if r.get("status") == "open":
enriched.append(r)
continue
key = str(r.get("history_key") or "").strip()
pnl = _safe_float(r.get("realized_pnl"))
if (
key
and pnl is not None
and not (
key in freeze_map
and _safe_float(freeze_map[key].get("realized_pnl_usdt")) is not None
)
and _safe_float(r.get("close_index_px")) is None
and _safe_float(r.get("_freeze_index_hint")) is None
):
underly = str(r.get("underlying") or "").strip().upper()
inst = str(r.get("inst_id") or "")
if not underly and inst:
underly = inst.split("-")[0].strip().upper()
if underly:
need_idx[underly] = inst
enriched.append(r)
if need_idx and ex is not None:
from lib.options.options_pnl_settle_lib import resolve_close_index_px
idx_cache: dict[str, float | None] = {}
for underly, inst in need_idx.items():
idx_cache[underly] = resolve_close_index_px(
ex, inst_id=inst, underlying=underly
)
for r in enriched:
if r.get("status") == "open":
continue
underly = str(r.get("underlying") or "").strip().upper()
inst = str(r.get("inst_id") or "")
if not underly and inst:
underly = inst.split("-")[0].strip().upper()
if underly in idx_cache and _safe_float(r.get("close_index_px")) is None:
r["_freeze_index_hint"] = idx_cache.get(underly)
return _apply_exchange_pnl_freeze_with_hints(conn, enriched, freeze_map)
def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
"""模拟盘数据统计:口径 B(历史币盈亏×卖回价)."""
conn = get_db()
try:
history = load_local_options_stats_history(conn, ex)
try:
conn.commit()
except Exception:
pass
return compute_options_stats_from_history(history_pnl_to_usdt_stats(history, ex))
finally:
conn.close()
def load_live_exchange_stats_history(cfg: dict[str, Any], ex: Any) -> list[dict[str, Any]]:
"""实盘统计用历史:先拉交易所,再无连接时取指数,最后短事务写冻结.
禁止在持有 DB 连接时打行情/历史网,否则会拖死持仓与中控轮询.
"""
from lib.options.options_history_lib import load_options_history
from lib.options.options_pnl_settle_lib import resolve_close_index_px
history = load_options_history(ex, cfg) or []
get_db = cfg["get_db"]
conn = get_db()
try:
freeze_map = _load_exchange_pnl_freeze_map(conn)
try:
conn.commit()
except Exception:
pass
finally:
conn.close()
need_idx: dict[str, str] = {}
for row in history:
if row.get("status") == "open":
continue
key = str(row.get("history_key") or "").strip()
if not key or _safe_float(row.get("realized_pnl")) is None:
continue
if key in freeze_map and _safe_float(freeze_map[key].get("realized_pnl_usdt")) is not None:
continue
underly = str(row.get("underlying") or "").strip().upper()
inst = str(row.get("inst_id") or "")
if not underly and inst:
underly = inst.split("-")[0].strip().upper()
if underly and _safe_float(row.get("close_index_px")) is None:
need_idx[underly] = inst
idx_cache: dict[str, float | None] = {}
for underly, inst in need_idx.items():
idx_cache[underly] = resolve_close_index_px(ex, inst_id=inst, underlying=underly)
enriched: list[dict[str, Any]] = []
for row in history:
r = dict(row)
if r.get("status") != "open":
underly = str(r.get("underlying") or "").strip().upper()
inst = str(r.get("inst_id") or "")
if not underly and inst:
underly = inst.split("-")[0].strip().upper()
key = str(r.get("history_key") or "").strip()
if underly in idx_cache and _safe_float(r.get("close_index_px")) is None:
if not (
key in freeze_map
and _safe_float(freeze_map[key].get("realized_pnl_usdt")) is not None
):
r["_freeze_index_hint"] = idx_cache.get(underly)
enriched.append(r)
conn = get_db()
try:
out = _apply_exchange_pnl_freeze_with_hints(conn, enriched, freeze_map)
try:
conn.commit()
except Exception:
pass
return out
finally:
conn.close()
def compute_options_stats_live_exchange(cfg: dict[str, Any], ex: Any) -> dict[str, Any]:
"""实盘:与「期权历史」同一数据源;口径 B=币盈亏×卖回价(freeze)."""
history = load_live_exchange_stats_history(cfg, ex)
return compute_options_stats_from_history(
history_pnl_to_usdt(history, ex, prefer_frozen_usdt=True)
)
def _apply_exchange_pnl_freeze_with_hints(
conn,
history: list[dict[str, Any]],
freeze_map: dict[str, dict[str, Any]] | None = None,
) -> list[dict[str, Any]]:
"""短事务写冻结;指数只用来自行内 _freeze_index_hint / 已有 close_index_px."""
from lib.options.options_pnl_settle_lib import settle_exchange_realized_pnl
from lib.options.options_time_lib import options_app_now_str
init_options_tables(conn)
fmap = freeze_map if freeze_map is not None else _load_exchange_pnl_freeze_map(conn)
out: list[dict[str, Any]] = []
for row in history or []:
r = dict(row)
hint = r.pop("_freeze_index_hint", None)
if r.get("status") == "open":
out.append(r)
continue
key = str(r.get("history_key") or "").strip()
pnl = _safe_float(r.get("realized_pnl"))
if not key or pnl is None:
out.append(r)
continue
frozen = fmap.get(key)
if frozen is not None and _safe_float(frozen.get("realized_pnl_usdt")) is not None:
r["realized_pnl_usdt"] = frozen.get("realized_pnl_usdt")
r["close_index_px"] = frozen.get("close_index_px")
out.append(r)
continue
ccy = str(r.get("premium_ccy") or "").strip().upper()
idx = _safe_float(r.get("close_index_px")) or _safe_float(hint)
settled = settle_exchange_realized_pnl(
realized_pnl=pnl,
premium_ccy=ccy or "ETH",
index_px=idx,
)
usdt = settled.get("realized_pnl_usdt")
close_idx = settled.get("close_index_px")
if usdt is not None:
try:
conn.execute(
"""
INSERT INTO options_exchange_pnl_freeze(
history_key, realized_pnl, realized_pnl_usdt, close_index_px,
premium_ccy, updated_at
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(history_key) DO UPDATE SET
realized_pnl = excluded.realized_pnl,
realized_pnl_usdt = excluded.realized_pnl_usdt,
close_index_px = excluded.close_index_px,
premium_ccy = excluded.premium_ccy,
updated_at = excluded.updated_at
""",
(
key,
float(pnl),
float(usdt),
close_idx,
ccy or "",
options_app_now_str(),
),
)
except Exception:
pass
r["realized_pnl_usdt"] = float(usdt)
r["close_index_px"] = close_idx
out.append(r)
return out
def compute_options_stats_auto(cfg: dict[str, Any], ex: Any = None) -> dict[str, Any]:
"""模拟盘走本地结算;实盘走交易所期权历史原生盈亏."""
get_db = cfg.get("get_db")
try:
from lib.sim.mode_lib import is_sim_mode
if callable(get_db) and is_sim_mode(get_db):
return compute_options_stats(get_db, ex)
except Exception:
pass
if ex is None:
ex = cfg.get("exchange_options")
return compute_options_stats_live_exchange(cfg, ex)
def _stats_from_option_trade_rows(closed_rows, open_rows, *, ex: Any = None) -> dict[str, Any]:
"""兼容旧调用;内部统一折 U 后走 compute_options_stats_from_history."""
history = _closed_trade_dicts(closed_rows)
for row in open_rows or []:
history.append({"status": "open", "created_at": _row_get(row, "created_at")})
return compute_options_stats_from_history(history_pnl_to_usdt_stats(history, ex))
def header_options_stats_for_window(
conn,
list_window: dict[str, Any],
app_tz,
ex: Any = None,
) -> dict[str, Any]:
"""顶栏总交易/胜率/盈亏比:口径 B(历史币盈亏×卖回价)."""
from lib.common.history_window_lib import utc_window_to_bj_sql_strings
init_options_tables(conn)
start_bj, end_bj = utc_window_to_bj_sql_strings(
list_window["start_utc"], list_window["end_utc"], app_tz
)
closed_rows = conn.execute(
"""
SELECT realized_pnl, close_index_px,
created_at, closed_at, inst_id, underlying, premium_ccy
FROM options_trades
WHERE status = 'closed'
AND realized_pnl IS NOT NULL
AND COALESCE(closed_at, created_at) >= ?
AND COALESCE(closed_at, created_at) <= ?
""",
(start_bj, end_bj),
).fetchall()
open_rows = conn.execute(
"SELECT created_at FROM options_trades WHERE status = 'open'"
).fetchall()
history = _closed_trade_dicts(closed_rows)
for row in open_rows:
history.append({"status": "open", "created_at": _row_get(row, "created_at")})
stats = compute_options_stats_from_history(history_pnl_to_usdt_stats(history, ex))
return {
"total": int(stats.get("total_closed") or 0),
"rate": float(stats.get("win_rate") or 0),
"profit_loss_ratio": stats.get("profit_loss_ratio"),
}