实盘数据统计改用期权历史交易所盈亏;模拟盘仍走本地结算。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-08 23:20:16 +08:00
parent d05629c362
commit 7943cd2167
6 changed files with 222 additions and 139 deletions
+13
View File
@@ -95,6 +95,19 @@ def init_options_tables(conn: sqlite3.Connection) -> None:
ON options_target_monitors(status)
"""
)
# 实盘统计:按期权历史 history_key 冻结交易所盈亏折 U,避免跟实时指数漂移
conn.execute(
"""
CREATE TABLE IF NOT EXISTS options_exchange_pnl_freeze (
history_key TEXT PRIMARY KEY,
realized_pnl REAL,
realized_pnl_usdt REAL,
close_index_px REAL,
premium_ccy TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
for ddl in (
"ALTER TABLE options_trades ADD COLUMN wechat_open_sent INTEGER DEFAULT 0",
"ALTER TABLE options_trades ADD COLUMN wechat_close_sent INTEGER DEFAULT 0",
+12 -57
View File
@@ -188,13 +188,14 @@ def settle_closed_pnl(
) -> dict[str, float | None]:
"""
由「毛权利金」结算.
币数量净盈亏 = 收回 − 已付 − 开/平手续费.
冻结 U(币本位):
平仓收回×平仓指数 − 开仓成本U − 手续费U
开仓成本优先 budget_usdt,否则 已付×开仓指数(缺省才用平仓指数).
这样不会把开仓成本也按平仓价重估,避免和余额对不上.
币数量净盈亏 = 收回 − 已付 − 开/平手续费(只扣一次).
冻结 U(币本位) = 净盈亏 × 平仓指数.
注意:不要用 budget_usdt 当开仓成本——它含现货买入缓冲,多余币会卖回,
拿全额预算当成本会把亏损算大. open_index/budget 参数保留兼容,不参与 U 计算.
不要对 OKX realizedPnl 再调此函数扣费.
"""
del open_index_px, budget_usdt, fee_ccy # 兼容旧调用,不参与结算
paid = float(_safe_float(premium_paid) or 0.0)
recv = float(_safe_float(premium_received) or 0.0)
ofee = float(_safe_float(open_fee) or 0.0)
@@ -204,59 +205,13 @@ def settle_closed_pnl(
if cfee < 0:
cfee = abs(cfee)
net = round(recv - paid - ofee - cfee, 8)
pc = str(premium_ccy or "").strip().upper()
close_idx = _safe_float(index_px)
open_idx = _safe_float(open_index_px)
budget = _safe_float(budget_usdt)
fc = str(fee_ccy or pc or "").strip().upper()
if pc not in ("ETH", "BTC"):
return freeze_pnl_to_usdt(
net,
premium_ccy=premium_ccy,
index_px=close_idx,
total_fee=ofee + cfee,
)
if close_idx is None or close_idx <= 0:
return {
"realized_pnl": net,
"close_index_px": None,
"open_index_px": float(open_idx) if open_idx and open_idx > 0 else None,
"realized_pnl_usdt": None,
"total_fee": round(ofee + cfee, 8),
}
# 开仓成本 U:预算(真实花出去的 USDT) > 已付×开仓指数 > 已付×平仓指数
if budget is not None and budget > 0:
entry_u = float(budget)
if open_idx is None or open_idx <= 0:
open_idx = (entry_u / paid) if paid > 1e-12 else close_idx
elif open_idx is not None and open_idx > 0:
entry_u = paid * float(open_idx)
else:
entry_u = paid * float(close_idx)
open_idx = close_idx
exit_u = recv * float(close_idx)
# 开仓费按开仓指数、平仓费按平仓指数折 U
fee_u = fee_to_usdt(ofee, fc or pc, index_px=open_idx) + fee_to_usdt(
cfee, fc or pc, index_px=close_idx
out = freeze_pnl_to_usdt(
net,
premium_ccy=premium_ccy,
index_px=index_px,
total_fee=ofee + cfee,
)
# 若开仓成本已是 budget(含买币),ETH 开仓费已体现在币变少/卖回变少,不再重复扣 ETH 开仓费
if budget is not None and budget > 0 and (fc or pc) in ("ETH", "BTC"):
fee_u = fee_to_usdt(cfee, fc or pc, index_px=close_idx)
if str(fee_ccy or "").strip().upper() in ("USDT", "USDC", "USD"):
fee_u += fee_to_usdt(ofee, fee_ccy, index_px=open_idx)
usdt = round(exit_u - entry_u - fee_u, 4)
return {
"realized_pnl": net,
"close_index_px": float(close_idx),
"open_index_px": float(open_idx) if open_idx and open_idx > 0 else None,
"realized_pnl_usdt": usdt,
"total_fee": round(ofee + cfee, 8),
}
return out
def settle_exchange_realized_pnl(
+3 -3
View File
@@ -1280,13 +1280,13 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
return jsonify({"ok": False, "msg": err})
from lib.options.options_margin_mode_lib import is_coin_margin_mode
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
from lib.options.options_stats_lib import compute_options_stats
from lib.options.options_stats_lib import compute_options_stats_auto
raw_live = cfg["fetch_option_positions"](ex)
if raw_live is None:
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
# 已平仓走本地库:扣手续费 + 平仓时冻结 U,不跟实时指数漂移
stats = compute_options_stats(cfg["get_db"], ex)
# 实盘:期权历史交易所原生盈亏;模拟盘:本地结算
stats = compute_options_stats_auto(cfg, ex)
open_float = sum_options_net_pnl_usdc(cfg, ex, raw_live)
net_realized = _safe_float(stats.get("net_realized_pnl")) or 0.0
total_pnl = None
+112 -6
View File
@@ -466,12 +466,8 @@ def repair_double_counted_fee_pnl(conn, ex: Any = None) -> int:
def load_local_options_stats_history(conn, ex: Any = None) -> list[dict[str, Any]]:
"""本地已平(含冻结 U/手续费净额) + 当前 open,供统计/中控窗口复用."""
"""本地已平(模拟盘口径) + 当前 open."""
init_options_tables(conn)
try:
recompute_closed_realized_pnl_usdt(conn, ex)
except Exception:
pass
try:
freeze_missing_realized_pnl_usdt(conn, ex)
except Exception:
@@ -486,7 +482,86 @@ def load_local_options_stats_history(conn, ex: Any = None) -> list[dict[str, Any
return history
def apply_exchange_pnl_freeze(
conn,
history: list[dict[str, Any]],
ex: Any = None,
) -> list[dict[str, Any]]:
"""实盘:以期权历史里的交易所 realized_pnl 为准,首次折 U 后按 history_key 冻结."""
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)
out: list[dict[str, Any]] = []
for row in history or []:
r = dict(row)
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 = conn.execute(
"""
SELECT realized_pnl_usdt, close_index_px, realized_pnl
FROM options_exchange_pnl_freeze WHERE history_key = ?
""",
(key,),
).fetchone()
if frozen is not None and _safe_float(_row_get(frozen, "realized_pnl_usdt")) is not None:
r["realized_pnl_usdt"] = _row_get(frozen, "realized_pnl_usdt")
r["close_index_px"] = _row_get(frozen, "close_index_px")
# 币数量仍以当前交易所行为准(防历史修订);U 用冻结值
out.append(r)
continue
ccy = str(r.get("premium_ccy") or "").strip().upper()
underly = str(r.get("underlying") or "").strip()
inst = str(r.get("inst_id") or "")
if not underly and inst:
underly = inst.split("-")[0]
from lib.options.options_pnl_settle_lib import resolve_close_index_px
idx = resolve_close_index_px(ex, inst_id=inst, underlying=underly)
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:
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(),
),
)
r["realized_pnl_usdt"] = float(usdt)
r["close_index_px"] = close_idx
out.append(r)
return out
def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
"""模拟盘:本地交易结算.实盘请用 compute_options_stats_live_exchange."""
conn = get_db()
try:
history = load_local_options_stats_history(conn, ex)
@@ -494,12 +569,43 @@ def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
conn.commit()
except Exception:
pass
# 优先冻结 U;缺省再按指数折(与 /api/options/stats 一致)
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
finally:
conn.close()
def compute_options_stats_live_exchange(cfg: dict[str, Any], ex: Any) -> dict[str, Any]:
"""实盘:与「期权历史」同一数据源(交易所 positions-history),盈亏以交易所为准."""
from lib.options.options_history_lib import load_options_history
history = load_options_history(ex, cfg)
conn = cfg["get_db"]()
try:
history = apply_exchange_pnl_freeze(conn, history, ex)
try:
conn.commit()
except Exception:
pass
finally:
conn.close()
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
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)