fix模拟盘复盘盈亏跟盘跳动:优先用冻结现金 U,不再每次用实时指数重算。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-08 23:43:46 +08:00
parent 3f621835a9
commit a16dd4f9cc
2 changed files with 169 additions and 8 deletions
+61 -8
View File
@@ -284,17 +284,38 @@ def sync_options_from_local_trades(
) -> dict[str, Any]: ) -> dict[str, Any]:
"""从本地 options_trades 已平仓记录导入复盘快照(不访问交易所). """从本地 options_trades 已平仓记录导入复盘快照(不访问交易所).
币本位权利金/盈亏按指数折算成 USDT(U) 写入,复盘页统一按 U 展示. 盈亏优先用已冻结 realized_pnl_usdt(模拟盘现金口径 / 平仓时指数),
禁止每次同步用实时指数重算,否则复盘数字会跟盘跳动.
""" """
init_options_review_tables(conn) init_options_review_tables(conn)
from lib.options.options_db import init_options_tables from lib.options.options_db import init_options_tables
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
init_options_tables(conn) init_options_tables(conn)
# 模拟盘先把现金盈亏写进 realized_pnl_usdt,复盘与持仓统计同一口径.
# 不可 is_sim_mode(lambda: conn): with_db 会 close 掉调用方连接.
try:
from lib.instance.runtime_settings_lib import ensure_runtime_settings_table, runtime_get
from lib.sim.mode_lib import MODE_SIM, default_trading_mode
ensure_runtime_settings_table(conn)
mode = runtime_get(conn, "trading.mode")
mode_s = str(mode or "").strip().lower() or default_trading_mode()
if mode_s == MODE_SIM:
from lib.options.options_stats_lib import sync_sim_cash_realized_pnl_usdt
sync_sim_cash_realized_pnl_usdt(conn, ex)
try:
conn.commit()
except Exception:
pass
except Exception:
pass
rows = conn.execute( rows = conn.execute(
""" """
SELECT id, inst_id, underlying, opt_type, strike, exp_time, sheets, SELECT id, inst_id, underlying, opt_type, strike, exp_time, sheets,
open_quote, close_quote, premium_paid, premium_received, realized_pnl, open_quote, close_quote, premium_paid, premium_received, realized_pnl,
realized_pnl_usdt, close_index_px, open_index_px, budget_usdt,
premium_ccy, created_at, closed_at, signal_note, status premium_ccy, created_at, closed_at, signal_note, status
FROM options_trades FROM options_trades
WHERE status = 'closed' WHERE status = 'closed'
@@ -325,9 +346,9 @@ def sync_options_from_local_trades(
for pub in pubs: for pub in pubs:
try: try:
if hasattr(pub, "public_get_market_ticker"): if hasattr(pub, "public_get_market_ticker"):
rows = (pub.public_get_market_ticker({"instId": inst}) or {}).get("data") or [] tick_rows = (pub.public_get_market_ticker({"instId": inst}) or {}).get("data") or []
if rows: if tick_rows:
last = rows[0].get("last") or rows[0].get("lastPx") last = tick_rows[0].get("last") or tick_rows[0].get("lastPx")
if last is not None and float(last) > 0: if last is not None and float(last) > 0:
return float(last) return float(last)
except Exception: except Exception:
@@ -363,6 +384,12 @@ def sync_options_from_local_trades(
idx_cache: dict[str, float | None] = {} idx_cache: dict[str, float | None] = {}
need_idx: set[str] = set() need_idx: set[str] = set()
for r in rows: for r in rows:
# 已有冻结 U 或平仓指数时不再打实时行情
if _safe_float(r["realized_pnl_usdt"]) is not None:
continue
frozen_idx = _safe_float(r["close_index_px"]) or _safe_float(r["open_index_px"])
if frozen_idx is not None and frozen_idx > 0:
continue
inst0 = str(r["inst_id"] or "") inst0 = str(r["inst_id"] or "")
underly0 = str(r["underlying"] or (inst0.split("-")[0] if inst0 else "ETH") or "ETH") underly0 = str(r["underlying"] or (inst0.split("-")[0] if inst0 else "ETH") or "ETH")
ccy0 = str(r["premium_ccy"] or "").strip().upper() ccy0 = str(r["premium_ccy"] or "").strip().upper()
@@ -381,7 +408,17 @@ def sync_options_from_local_trades(
ccy = str(r["premium_ccy"] or "").strip().upper() ccy = str(r["premium_ccy"] or "").strip().upper()
if not ccy: if not ccy:
ccy = premium_ccy_for_mode(margin_mode_from_inst_id(inst), underly) ccy = premium_ccy_for_mode(margin_mode_from_inst_id(inst), underly)
idx = idx_cache.get(underly) frozen_usdt = _safe_float(r["realized_pnl_usdt"])
close_idx = _safe_float(r["close_index_px"])
open_idx = _safe_float(r["open_index_px"])
# 折算指数:平仓冻结 > 开仓 > 实时(仅无冻结时兜底)
idx = None
if close_idx is not None and close_idx > 0:
idx = close_idx
elif open_idx is not None and open_idx > 0:
idx = open_idx
else:
idx = idx_cache.get(underly)
pnl = _safe_float(r["realized_pnl"]) pnl = _safe_float(r["realized_pnl"])
if pnl is None: if pnl is None:
paid0 = _safe_float(r["premium_paid"]) paid0 = _safe_float(r["premium_paid"])
@@ -389,10 +426,15 @@ def sync_options_from_local_trades(
if paid0 is not None and recv0 is not None: if paid0 is not None and recv0 is not None:
pnl = recv0 - paid0 pnl = recv0 - paid0
prem = _safe_float(r["premium_paid"]) prem = _safe_float(r["premium_paid"])
pnl_u = _to_usdt(pnl, ccy=ccy, idx=idx) if frozen_usdt is not None:
pnl_u = round(float(frozen_usdt), 4)
else:
pnl_u = _to_usdt(pnl, ccy=ccy, idx=idx)
# 权利金用平仓/开仓冻结指数折算,不用实时价
prem_u = _to_usdt(prem, ccy=ccy, idx=idx) prem_u = _to_usdt(prem, ccy=ccy, idx=idx)
if ccy in ("ETH", "BTC") and idx is None: if ccy in ("ETH", "BTC") and pnl_u is None:
pnl_u = pnl pnl_u = pnl
if ccy in ("ETH", "BTC") and prem_u is None:
prem_u = prem prem_u = prem
opened_at = r["created_at"] opened_at = r["created_at"]
closed_at = r["closed_at"] closed_at = r["closed_at"]
@@ -664,7 +706,18 @@ def ensure_local_review_synced(
conn.commit() conn.commit()
except Exception: except Exception:
pass pass
if backfill_exchange_pnl and ex is not None: # 模拟盘用现金冻结,勿再用交易所历史+实时指数覆盖
sim_mode = False
try:
from lib.instance.runtime_settings_lib import ensure_runtime_settings_table, runtime_get
from lib.sim.mode_lib import MODE_SIM, default_trading_mode
ensure_runtime_settings_table(conn)
mode = runtime_get(conn, "trading.mode")
sim_mode = (str(mode or "").strip().lower() or default_trading_mode()) == MODE_SIM
except Exception:
sim_mode = False
if backfill_exchange_pnl and ex is not None and not sim_mode:
try: try:
from lib.exchange.okx_options_lib import fetch_all_option_positions_history from lib.exchange.okx_options_lib import fetch_all_option_positions_history
from lib.options.options_monitor_lib import ( from lib.options.options_monitor_lib import (
+108
View File
@@ -0,0 +1,108 @@
"""复盘导入须使用冻结 U,不得用实时指数改写."""
from __future__ import annotations
import sqlite3
import unittest
class ReviewLocalSyncFreezeTests(unittest.TestCase):
def test_prefers_realized_pnl_usdt_over_live_index(self):
from lib.options.options_db import init_options_tables
from lib.options.options_review_lib import sync_options_from_local_trades
from lib.sim.mode_lib import MODE_SIM, TRADING_MODE_KEY
from lib.instance.runtime_settings_lib import ensure_runtime_settings_table, runtime_set
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
init_options_tables(conn)
ensure_runtime_settings_table(conn)
runtime_set(conn, TRADING_MODE_KEY, MODE_SIM)
conn.execute(
"""
INSERT INTO options_trades(
inst_id, underlying, opt_type, sheets, eth_amount,
premium_paid, premium_received, realized_pnl, premium_ccy,
status, realized_pnl_usdt, close_index_px,
created_at, closed_at
) VALUES (
'BTC-USD-261008-86500-P', 'BTC', 'P', 1, 0.01,
0.01, 0.02, 0.01172, 'BTC',
'closed', 1947.05, 85000.0,
'2026-10-05 05:11:30', '2026-10-07 10:03:53'
)
"""
)
conn.commit()
class _Ex:
def public_get_market_ticker(self, params):
return {"data": [{"last": "999999"}]}
out = sync_options_from_local_trades(conn, _Ex())
self.assertTrue(out.get("ok"))
row = conn.execute(
"SELECT realized_pnl_total FROM options_review_trades WHERE history_key=?",
("local_opt:1",),
).fetchone()
self.assertIsNotNone(row)
# 必须是冻结现金 1947.05,不是 0.01172*999999
self.assertAlmostEqual(float(row["realized_pnl_total"]), 1947.05, places=2)
conn.close()
def test_second_sync_stable_when_index_moves(self):
from lib.options.options_db import init_options_tables
from lib.options.options_review_lib import sync_options_from_local_trades
from lib.sim.mode_lib import MODE_SIM, TRADING_MODE_KEY
from lib.instance.runtime_settings_lib import ensure_runtime_settings_table, runtime_set
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
init_options_tables(conn)
ensure_runtime_settings_table(conn)
runtime_set(conn, TRADING_MODE_KEY, MODE_SIM)
conn.execute(
"""
INSERT INTO options_trades(
inst_id, underlying, opt_type, sheets, eth_amount,
premium_paid, premium_received, realized_pnl, premium_ccy,
status, realized_pnl_usdt, close_index_px,
created_at, closed_at
) VALUES (
'BTC-USD-261008-86500-P', 'BTC', 'P', 1, 0.01,
0.01, 0.02, 0.01, 'BTC',
'closed', 952.96, 80000.0,
'2026-10-05 05:11:30', '2026-10-07 10:03:53'
)
"""
)
conn.commit()
class _Ex1:
def public_get_market_ticker(self, params):
return {"data": [{"last": "70000"}]}
class _Ex2:
def public_get_market_ticker(self, params):
return {"data": [{"last": "120000"}]}
sync_options_from_local_trades(conn, _Ex1())
v1 = float(
conn.execute(
"SELECT realized_pnl_total FROM options_review_trades WHERE history_key=?",
("local_opt:1",),
).fetchone()["realized_pnl_total"]
)
sync_options_from_local_trades(conn, _Ex2())
v2 = float(
conn.execute(
"SELECT realized_pnl_total FROM options_review_trades WHERE history_key=?",
("local_opt:1",),
).fetchone()["realized_pnl_total"]
)
self.assertAlmostEqual(v1, 952.96, places=2)
self.assertAlmostEqual(v2, v1, places=6)
conn.close()
if __name__ == "__main__":
unittest.main()