fix模拟盘复盘盈亏跟盘跳动:优先用冻结现金 U,不再每次用实时指数重算。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 (
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user