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 @@
1
期权交易记录
-
盈亏为卖光币后的账户真盈亏;点「复盘」填写表单。期权历史仅记期权腿币收益,不含现货。
+
真盈亏=卖光币收回 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()