From c2c69e580c34a0a4b7280a0b6b8d25f4e93d2ec9 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 13 Oct 2026 10:03:06 +0800 Subject: [PATCH] =?UTF-8?q?fix=E7=9C=9F=E7=9B=88=E4=BA=8F=E5=8F=AA?= =?UTF-8?q?=E8=AE=A4=E5=8D=96=E5=85=89=E6=94=B6=E5=9B=9E=E5=87=8F=E4=B9=B0?= =?UTF-8?q?=E5=B8=81=EF=BC=8C=E7=A6=81=E6=AD=A2=E6=9D=83=E5=88=A9=E9=87=91?= =?UTF-8?q?=E6=8C=87=E6=95=B0=E5=86=92=E5=85=85=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- docs/系统分析-期权主链条.md | 2 +- docs/规格-期权交易账户盈亏结算.md | 5 +- lib/options/options_account_pnl_lib.py | 103 +++++++++-- lib/options/options_review_lib.py | 71 ++++++-- lib/options/options_spot_bridge_lib.py | 5 +- .../templates/options_review_panel.html | 4 +- scripts/repair_options_dual_pnl.py | 53 +++--- tests/test_options_account_pnl.py | 43 +++++ tests/test_options_review_pnl_freeze.py | 170 +++++++++++------- 9 files changed, 338 insertions(+), 118 deletions(-) diff --git a/docs/系统分析-期权主链条.md b/docs/系统分析-期权主链条.md index 1e1e8f0..0749fbc 100644 --- a/docs/系统分析-期权主链条.md +++ b/docs/系统分析-期权主链条.md @@ -32,7 +32,7 @@ USDT(交易户) ▼ 卖光可卖标的币(禁止按开仓权利金封顶)→ 快照 trading_usdt_after ▼ -口径 A(复盘交易记录真盈亏): after − before,pnl_settled=1 +口径 A(复盘真盈亏): after − before ≡ 卖光收回 − 买币;禁止指数冒充 口径 B(期权历史/数据统计): 仅期权腿币盈亏(×卖回价折 U);不含现货 ``` diff --git a/docs/规格-期权交易账户盈亏结算.md b/docs/规格-期权交易账户盈亏结算.md index 7ff1d65..9504b45 100644 --- a/docs/规格-期权交易账户盈亏结算.md +++ b/docs/规格-期权交易账户盈亏结算.md @@ -35,9 +35,11 @@ ``` 本单真盈亏(U) = trading_usdt_after − trading_usdt_before + = 卖光标的币累计收回 USDT − 开仓买币花费 ``` -等价理解:平仓后卖光标的币收回的 USDT,相对开仓前(买币前)交易账户的变化。 +一次一仓且无中途划转时,账户差分与桥上 `usdt_recovered − budget_usdt` 等价。 +**禁止**用权利金×指数(期权腿 / 口径 B)冒充实盈亏。 - **消费者**:期权复盘「期权交易记录」盈亏列、复盘 KPI、账户冻结(`pnl_settled`) - **含**:多买缓冲、现货滑点、期权/现货摩擦(整笔钱进出) @@ -267,3 +269,4 @@ IDLE | 2026-10-13 | 修订:双口径 A/B;卖光再结算;初始权利金/盈利率;状态机;历史修复脚本 | | 2026-10-13 | 复盘「期权交易记录」盈亏与数据统计同口径 B;增加仅期权手续费展示 | | 2026-10-13 | 交易记录改回真盈亏 A;期权历史/统计仅期权腿 B,不含现货 | +| 2026-10-13 | 真盈亏明确为卖光收回−买币;复盘禁止指数折 U 冒充;桥可回退 | diff --git a/lib/options/options_account_pnl_lib.py b/lib/options/options_account_pnl_lib.py index 2514e34..e6caf34 100644 --- a/lib/options/options_account_pnl_lib.py +++ b/lib/options/options_account_pnl_lib.py @@ -190,9 +190,13 @@ def try_settle_after_sellback( block = sellable_coin_blocks_settle(ex, uly) if block: return 0 + try: + from lib.exchange.okx_options_lib import invalidate_options_balance_cache + + invalidate_options_balance_cache() + except Exception: + pass after = fetch_trading_usdt(pack, ex) - if after is None: - return 0 n = 0 if bridge_id is not None: n = settle_closed_trades_from_trading_account( @@ -232,21 +236,83 @@ def try_settle_after_sellback( pass +def resolve_true_pnl_usdt( + conn, + *, + trading_usdt_before: Any = None, + trading_usdt_after: Any = None, + bridge_id: Any = None, + realized_pnl_usdt: Any = None, + pnl_settled: Any = False, +) -> tuple[float | None, str]: + """真盈亏(口径 A)=卖光标的币收回 USDT − 开仓买币花费. + + 优先账户快照 after−before(与上式等价);其次已关桥的「卖回累计−买币预算」. + 绝不回退权利金×指数(那是口径 B / 期权腿). + 返回 (pnl_u, source): account | bridge | frozen | none + """ + before = _safe_float(trading_usdt_before) + after = _safe_float(trading_usdt_after) + if before is not None and after is not None: + return round(float(after) - float(before), 4), "account" + + bid = None + try: + if bridge_id is not None and str(bridge_id).strip() != "": + bid = int(bridge_id) + except (TypeError, ValueError): + bid = None + if bid is not None: + try: + from lib.options.options_spot_bridge_lib import ( + BRIDGE_CLOSED, + cash_pnl_usdt_from_bridge, + ensure_bridge_table, + ) + + ensure_bridge_table(conn) + brow = conn.execute( + "SELECT status, usdt_recovered FROM options_spot_bridge WHERE id=?", + (bid,), + ).fetchone() + if brow: + st = str(brow["status"] if hasattr(brow, "keys") else brow[0] or "") + recovered = _safe_float( + brow["usdt_recovered"] if hasattr(brow, "keys") else brow[1] + ) + if st == BRIDGE_CLOSED and recovered is not None: + bp = cash_pnl_usdt_from_bridge(conn, bid) + if bp is not None: + return bp, "bridge" + except Exception: + pass + + try: + settled = int(pnl_settled or 0) == 1 + except (TypeError, ValueError): + settled = bool(pnl_settled) + frozen = _safe_float(realized_pnl_usdt) + # 仅承认「有 before 且已账户结算」的旧冻结;无快照的指数折 U 不算真盈亏 + if settled and before is not None and frozen is not None: + return round(float(frozen), 4), "frozen" + return None, "none" + + def settle_closed_trades_from_trading_account( conn, *, - trading_usdt_after: float, + trading_usdt_after: float | None = None, bridge_id: int | None = None, trade_id: int | None = None, ) -> int: - """卖回后:对本桥/本单未结算已平仓位冻结 after−before. + """卖回后:对本桥/本单未结算已平仓位冻结真盈亏. + 优先 after−before;账户 after 缺失时用桥「卖回−买币」. 已 pnl_settled=1 的行永不改写. """ after = _safe_float(trading_usdt_after) - if after is None: - return 0 - after = round(float(after), 8) + if after is not None: + after = round(float(after), 8) clauses = ["status='closed'", "COALESCE(pnl_settled, 0)=0", "trading_usdt_before IS NOT NULL"] params: list[Any] = [] if trade_id is not None: @@ -259,7 +325,7 @@ def settle_closed_trades_from_trading_account( return 0 rows = conn.execute( f""" - SELECT id, trading_usdt_before, realized_pnl_usdt + SELECT id, trading_usdt_before, realized_pnl_usdt, bridge_id FROM options_trades WHERE {" AND ".join(clauses)} """, @@ -267,10 +333,27 @@ def settle_closed_trades_from_trading_account( ).fetchall() updated = 0 for row in rows: + tid = int(row["id"] if hasattr(row, "keys") else row[0]) before = _safe_float(row["trading_usdt_before"] if hasattr(row, "keys") else row[1]) if before is None: continue - pnl_u = round(float(after) - float(before), 4) + row_bid = row["bridge_id"] if hasattr(row, "keys") else row[3] + if row_bid is None and bridge_id is not None: + row_bid = bridge_id + after_use = after + if after_use is not None: + pnl_u = round(float(after_use) - float(before), 4) + else: + pnl_u, src = resolve_true_pnl_usdt( + conn, + trading_usdt_before=before, + trading_usdt_after=None, + bridge_id=row_bid, + pnl_settled=0, + ) + if pnl_u is None or src != "bridge": + continue + after_use = round(float(before) + float(pnl_u), 8) conn.execute( """ UPDATE options_trades @@ -279,7 +362,7 @@ def settle_closed_trades_from_trading_account( pnl_settled = 1 WHERE id = ? AND COALESCE(pnl_settled, 0)=0 """, - (after, pnl_u, int(row["id"] if hasattr(row, "keys") else row[0])), + (after_use, pnl_u, tid), ) updated += 1 return updated diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py index b8b236d..ac6236e 100644 --- a/lib/options/options_review_lib.py +++ b/lib/options/options_review_lib.py @@ -429,8 +429,7 @@ def sync_options_from_local_trades( ) -> dict[str, Any]: """从本地 options_trades 已平仓记录导入复盘快照(不访问交易所). - 盈亏优先用已冻结 realized_pnl_usdt(模拟盘现金口径 / 平仓时指数), - 禁止每次同步用实时指数重算,否则复盘数字会跟盘跳动. + 真盈亏=卖光收回−买币(after−before / 已关桥);禁止权利金×指数冒充. """ init_options_review_tables(conn) from lib.options.options_db import init_options_tables @@ -462,8 +461,8 @@ def sync_options_from_local_trades( eth_amount, 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, - trading_usdt_before, pnl_settled, initial_premium_usdt, - open_fee, close_fee, exchange_ord_id, close_ord_id + trading_usdt_before, trading_usdt_after, bridge_id, pnl_settled, + initial_premium_usdt, open_fee, close_fee, exchange_ord_id, close_ord_id FROM options_trades WHERE status = 'closed' ORDER BY id DESC @@ -560,8 +559,6 @@ def sync_options_from_local_trades( settled = int(r["pnl_settled"] or 0) == 1 except Exception: settled = False - # 有账户快照未结算:交易记录显示待结算(真盈亏=卖回后 after−before) - acct_pending = _safe_float(r["trading_usdt_before"]) is not None and not settled close_idx = _safe_float(r["close_index_px"]) open_idx = _safe_float(r["open_index_px"]) # 折算指数:仅用于初始权利金/期权手续费展示,不用于真盈亏 @@ -573,14 +570,60 @@ def sync_options_from_local_trades( else: idx = idx_cache.get(underly) prem = _safe_float(r["premium_paid"]) - # 交易记录真盈亏=口径 A(卖光币后账户差分);未结算不写假 U - if acct_pending: - pnl_u = None - elif frozen_usdt is not None and (settled or _safe_float(r["trading_usdt_before"]) is None): - pnl_u = round(float(frozen_usdt), 4) - else: - # 旧单无账户快照:不回退币×价冒充真盈亏 - pnl_u = None + # 真盈亏=卖光收回−买币(账户 after−before 或已关桥);禁止权利金×指数冒充 + from lib.options.options_account_pnl_lib import resolve_true_pnl_usdt + + pnl_u, pnl_src = resolve_true_pnl_usdt( + conn, + trading_usdt_before=r["trading_usdt_before"], + trading_usdt_after=r["trading_usdt_after"], + bridge_id=r["bridge_id"], + realized_pnl_usdt=frozen_usdt, + pnl_settled=settled, + ) + # 快照能算真盈亏但冻结值偏离:纠偏写回(仅纠正错账,不改口径) + before_f = _safe_float(r["trading_usdt_before"]) + after_f = _safe_float(r["trading_usdt_after"]) + if ( + pnl_src == "account" + and pnl_u is not None + and before_f is not None + and after_f is not None + and (not settled or frozen_usdt is None or abs(float(frozen_usdt) - float(pnl_u)) > 0.05) + ): + try: + conn.execute( + """ + UPDATE options_trades + SET realized_pnl_usdt = ?, pnl_settled = 1 + WHERE id = ? + """, + (pnl_u, trade_id), + ) + settled = True + except Exception: + pass + elif pnl_src == "bridge" and pnl_u is not None and not settled: + try: + after_heal = ( + round(float(before_f) + float(pnl_u), 8) if before_f is not None else None + ) + conn.execute( + """ + UPDATE options_trades + SET realized_pnl_usdt = ?, + trading_usdt_after = COALESCE(trading_usdt_after, ?), + pnl_settled = 1 + WHERE id = ? AND COALESCE(pnl_settled, 0)=0 + """, + (pnl_u, after_heal, trade_id), + ) + settled = True + except Exception: + pass + acct_pending = pnl_u is None and ( + before_f is not None or r["bridge_id"] is not None + ) # 初始权利金(U)=开仓指数×权利金币量;不含多买 stored_initial = _safe_float(r["initial_premium_usdt"]) open_idx_for_prem = open_idx if (open_idx is not None and open_idx > 0) else idx diff --git a/lib/options/options_spot_bridge_lib.py b/lib/options/options_spot_bridge_lib.py index 10162f2..045a35b 100644 --- a/lib/options/options_spot_bridge_lib.py +++ b/lib/options/options_spot_bridge_lib.py @@ -184,9 +184,10 @@ def add_bridge_usdt_recovered( def cash_pnl_usdt_from_bridge(conn: sqlite3.Connection, bridge_id: int | None) -> float | None: - """桥上现货净 USDT(卖回−买币预算).仅供对账,不得当作期权已实现盈亏. + """桥上真盈亏(U)=累计卖回 USDT − 开仓买币预算. - 买币预算含权利金本金与缓冲;用此值当盈利会把权利金本金算进去. + 与账户 after−before 等价(一次一仓、无中途划转时);可作口径 A 回退. + 不是期权腿币盈亏(口径 B). """ if bridge_id is None: return None diff --git a/lib/options/templates/options_review_panel.html b/lib/options/templates/options_review_panel.html index f59a6e2..8af16e3 100644 --- a/lib/options/templates/options_review_panel.html +++ b/lib/options/templates/options_review_panel.html @@ -229,7 +229,7 @@
期权交易记录
-

盈亏为卖光币后的账户真盈亏;点「复盘」填写表单。期权历史仅记期权腿币收益,不含现货。

+

真盈亏=卖光币收回 USDT − 开仓买币花费;点「复盘」填写表单。期权历史仅记期权腿币收益,不含现货。

@@ -239,7 +239,7 @@ 类型 标的/合约 初始权利金 - 真盈亏 + 真盈亏 盈利率 手续费 开仓时间 diff --git a/scripts/repair_options_dual_pnl.py b/scripts/repair_options_dual_pnl.py index 5b7f0a3..aac38ed 100644 --- a/scripts/repair_options_dual_pnl.py +++ b/scripts/repair_options_dual_pnl.py @@ -134,31 +134,44 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: and abs(settled_u - stats_u) / max(abs(stats_u), 1.0) <= 0.15 ) - if suspicious and not a_ok_near_b: - # 负冻结且无法对上 B:解除错误冻结 + # 桥真盈亏=卖回累计−买币预算(口径 A 回退;禁止用 B 冒充 A) + bridge_true_a = None + if bid is not None: + try: + b2 = conn.execute( + "SELECT status, budget_usdt, usdt_recovered FROM options_spot_bridge WHERE id=?", + (int(bid),), + ).fetchone() + if b2 and str(b2["status"] or "") == "closed": + spent = _safe_float(b2["budget_usdt"]) + recovered = _safe_float(b2["usdt_recovered"]) + if spent is not None and recovered is not None: + bridge_true_a = round(float(recovered) - float(spent), 4) + except Exception: + bridge_true_a = None + + if before is not None and after is not None and (not settled or suspicious): + action = "settle_from_snapshots" + new_settled_u = round(after - before, 4) + new_pnl_settled = 1 + elif bridge_true_a is not None and (not settled or suspicious): + action = "settle_from_bridge_cash" + new_settled_u = bridge_true_a + new_pnl_settled = 1 + elif suspicious and not a_ok_near_b: + # 负冻结且无快照/桥可重建:解除错误冻结 action = "unsettle_bad_account_freeze" new_pnl_settled = 0 new_settled_u = None elif ( - (not settled or suspicious) + a_ok_near_b + and settled_u is not None + and settled_u > 0 and bridge_hint == "sell_capped_at_premium" - and coin_pnl is not None - and coin_pnl > 0 - and stats_u is not None - and stats_u > 0 - and (settled_u is None or settled_u < 0 or a_ok_near_b) ): - # 桥记录不可信时:优先保留已接近 B 的正 A,否则用口径 B 回填复盘 - if a_ok_near_b: - action = "accept_recovered_account_A" - new_settled_u = round(float(settled_u), 4) - else: - action = "backfill_A_from_stats_B" - new_settled_u = round(float(stats_u), 4) - new_pnl_settled = 1 - elif before is not None and after is not None and not settled: - action = "settle_from_snapshots" - new_settled_u = round(after - before, 4) + # 仅当 A 已为正且接近 B 时保留,不再用 B 回填假 A + action = "accept_recovered_account_A" + new_settled_u = round(float(settled_u), 4) new_pnl_settled = 1 profit_rate = None @@ -206,8 +219,8 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: ) elif action in ( "settle_from_snapshots", + "settle_from_bridge_cash", "accept_recovered_account_A", - "backfill_A_from_stats_B", ) and new_settled_u is not None: conn.execute( """ diff --git a/tests/test_options_account_pnl.py b/tests/test_options_account_pnl.py index 9871837..2089137 100644 --- a/tests/test_options_account_pnl.py +++ b/tests/test_options_account_pnl.py @@ -149,6 +149,49 @@ class AccountPnlSettleTests(unittest.TestCase): self.assertEqual(int(out["total"]), 2) conn.close() + def test_resolve_true_pnl_prefers_account_then_bridge(self): + from lib.options.options_account_pnl_lib import resolve_true_pnl_usdt + from lib.options.options_db import init_options_tables + from lib.options.options_spot_bridge_lib import ensure_bridge_table + + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + init_options_tables(conn) + ensure_bridge_table(conn) + conn.execute( + """ + INSERT INTO options_spot_bridge( + id, underlying, status, budget_usdt, usdt_recovered + ) VALUES (3, 'ETH', 'closed', 100.0, 160.0) + """ + ) + conn.commit() + pnl, src = resolve_true_pnl_usdt( + conn, + trading_usdt_before=500.0, + trading_usdt_after=580.0, + bridge_id=3, + ) + self.assertEqual(src, "account") + self.assertAlmostEqual(float(pnl), 80.0, places=4) + pnl2, src2 = resolve_true_pnl_usdt( + conn, + trading_usdt_before=500.0, + trading_usdt_after=None, + bridge_id=3, + ) + self.assertEqual(src2, "bridge") + self.assertAlmostEqual(float(pnl2), 60.0, places=4) + pnl3, src3 = resolve_true_pnl_usdt( + conn, + trading_usdt_before=None, + realized_pnl_usdt=999.0, + pnl_settled=1, + ) + self.assertEqual(src3, "none") + self.assertIsNone(pnl3) + conn.close() + def test_settle_blocked_when_sellable_coin(self): from lib.options.options_account_pnl_lib import try_settle_after_sellback from lib.options.options_db import init_options_tables diff --git a/tests/test_options_review_pnl_freeze.py b/tests/test_options_review_pnl_freeze.py index 0945451..27f3b72 100644 --- a/tests/test_options_review_pnl_freeze.py +++ b/tests/test_options_review_pnl_freeze.py @@ -1,4 +1,4 @@ -"""复盘导入须使用冻结 U,不得用实时指数改写.""" +"""复盘真盈亏=卖光收回−买币;禁止权利金×指数冒充.""" from __future__ import annotations import sqlite3 @@ -6,7 +6,103 @@ import unittest class ReviewLocalSyncFreezeTests(unittest.TestCase): - def test_prefers_realized_pnl_usdt_over_live_index(self): + def test_account_after_minus_before_is_true_pnl(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, + trading_usdt_before, trading_usdt_after, pnl_settled, + created_at, closed_at + ) VALUES ( + 'BTC-USD-261008-86500-P', 'BTC', 'P', 1, 0.01, + 0.01, 0.02, 0.01, 'BTC', + 'closed', -51.0, 85000.0, + 1000.0, 2506.0, 1, + '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) + # 真盈亏=after−before=1506,不是错误冻结 -51,也不是指数折算 + self.assertAlmostEqual(float(row["realized_pnl_total"]), 1506.0, places=2) + healed = conn.execute( + "SELECT realized_pnl_usdt FROM options_trades WHERE id=1" + ).fetchone() + self.assertAlmostEqual(float(healed["realized_pnl_usdt"]), 1506.0, places=2) + conn.close() + + def test_bridge_cash_is_true_pnl_when_no_after(self): + from lib.options.options_db import init_options_tables + from lib.options.options_review_lib import sync_options_from_local_trades + from lib.options.options_spot_bridge_lib import ensure_bridge_table + 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_bridge_table(conn) + ensure_runtime_settings_table(conn) + runtime_set(conn, TRADING_MODE_KEY, MODE_SIM) + conn.execute( + """ + INSERT INTO options_spot_bridge( + id, underlying, status, budget_usdt, usdt_recovered, message + ) VALUES (7, 'BTC', 'closed', 2200.0, 3706.0, 'done') + """ + ) + 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, + trading_usdt_before, bridge_id, pnl_settled, + created_at, closed_at + ) VALUES ( + 'BTC-USD-261008-86500-P', 'BTC', 'P', 1, 0.01, + 0.01, 0.02, 0.01, 'BTC', + 'closed', 850.0, 85000.0, + 1000.0, 7, 0, + '2026-10-05 05:11:30', '2026-10-07 10:03:53' + ) + """ + ) + conn.commit() + sync_options_from_local_trades(conn, None) + row = conn.execute( + "SELECT realized_pnl_total FROM options_review_trades WHERE history_key=?", + ("local_opt:1",), + ).fetchone() + # 3706 − 2200 = 1506;不是 0.01*85000 + self.assertAlmostEqual(float(row["realized_pnl_total"]), 1506.0, places=2) + conn.close() + + def test_index_formula_not_shown_as_true_pnl(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 @@ -33,75 +129,13 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase): """ ) 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")) + sync_options_from_local_trades(conn, None) row = conn.execute( - "SELECT realized_pnl_total FROM options_review_trades WHERE history_key=?", + "SELECT realized_pnl_total, status_raw FROM options_review_trades WHERE history_key=?", ("local_opt:1",), ).fetchone() - self.assertIsNotNone(row) - # (0.02-0.01)*85000=850,不是 0.01*999999 - self.assertAlmostEqual(float(row["realized_pnl_total"]), 850.0, 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', 800.0, 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"] - ) - # (0.02-0.01)*80000=800; 实时指数变也不改 - self.assertAlmostEqual(v1, 800.0, places=2) - self.assertAlmostEqual(v2, v1, places=6) + # 无账户/桥快照:不得用指数折 U 冒充真盈亏 + self.assertIsNone(row["realized_pnl_total"]) conn.close()