diff --git a/docs/规格-期权交易账户盈亏结算.md b/docs/规格-期权交易账户盈亏结算.md index 6800943..106a0ce 100644 --- a/docs/规格-期权交易账户盈亏结算.md +++ b/docs/规格-期权交易账户盈亏结算.md @@ -38,9 +38,10 @@ − (权利金 × 开仓价 − 权利金 × 平仓卖回价) ``` -等价(无手续费时):`收回×卖回价 − 已付×开仓价`。 -含义:**计入权利金的 USDT 波动**;**忽略现货多买缓冲**。 -有开/平手续费时:开仓费按开仓价、平仓费按卖回价折 U 后扣除。 +- **币量**只用期权腿:`realized_pnl`(历史币盈亏,**已含**开/平手续费)、`premium_paid`(权利金);**不含**现货多买/多卖数量 +- **价格**只用开仓买币 VWAP、平仓卖回 VWAP;现货多余仓位只参与均价,**数量不进公式** +- 真盈亏**不再另扣**手续费(费已在历史币盈亏里);复盘「期权手续费」列仅展示 +- 无手续费时:`期权收回权利金×卖回价 − 权利金×开仓价`(收回=`premium_received`,不是账户全部现货) **固定量 vs 一次冻结价:** @@ -50,11 +51,11 @@ | `realized_pnl`(期权腿币盈亏) | 平仓后**固定**,不再改 | | 开仓价 | 买币完成时算一次 VWAP 写入 `open_index_px`,**冻住** | | 卖回价 | 平仓卖回全部完成时算一次 VWAP 写入 `close_index_px`,**冻住** | -| `realized_pnl_usdt` | 卖回完成后按上式算一次,`pnl_settled=1` 后**只读** | +| `realized_pnl_usdt` | 按上式计算;**刷新时用固定量+价重算**(可纠正旧错账) | - 卖回未完成 → 「待卖回」,不算真盈亏 - 缺价 → 「缺价未核」 -- 复盘刷新**不得**改已冻结的开仓价/卖回价/真盈亏/权利金币量/币盈亏 +- 复盘刷新:**不得**改权利金币量/币盈亏/已冻开仓价/卖回价;**要**按公式重算真盈亏 U - **消费者**:期权复盘「期权交易记录」盈亏列、复盘 KPI、`realized_pnl_usdt` 冻结 - **不含**:现货多买差分当盈亏 / 账户 after−before diff --git a/lib/options/options_pnl_settle_lib.py b/lib/options/options_pnl_settle_lib.py index 528ef47..082c4da 100644 --- a/lib/options/options_pnl_settle_lib.py +++ b/lib/options/options_pnl_settle_lib.py @@ -181,14 +181,15 @@ def true_pnl_usdt_premium_move( open_index_px: Any, sell_back_px: Any, ) -> float | None: - """复盘真盈亏(U):计入权利金 USDT 波动,忽略现货多买. + """复盘真盈亏(U):计入权利金 USDT 波动;现货多买数量不进公式. 真盈亏 = 期权历史盈亏(币)×平仓卖回价 − (权利金×开仓价 − 权利金×平仓卖回价) = 历史币盈亏×卖回价 − 权利金×(开仓价 − 卖回价) - 无手续费时等价于: 收回×卖回价 − 已付×开仓价. - 币盈亏须已含期权开/平手续费(与期权历史同口径). + 币量=期权腿(realized_pnl / premium_paid);价=买卖 VWAP. + 无手续费时 ≡ 期权收回权利金×卖回价 − 权利金×开仓价 + (收回≠账户卖掉的全部现货;多余仓只影响均价). """ pnl = _safe_float(coin_pnl) paid = _safe_float(premium_paid) @@ -274,12 +275,13 @@ def resolve_true_pnl_usdt_from_trade( 返回 (pnl_u, source): premium_move | stable | pending_sell | none """ - del fee_ccy # 费率折 U 已体现在历史币盈亏里;本函数不重扣 + del fee_ccy pc = str(premium_ccy or "").strip().upper() paid = _safe_float(premium_paid) recv = _safe_float(premium_received) ofee = abs(float(_safe_float(open_fee) or 0.0)) cfee = abs(float(_safe_float(close_fee) or 0.0)) + # 期权历史币盈亏已含开/平手续费;真盈亏不再另扣费 coin = _safe_float(realized_pnl) if coin is None and paid is not None and recv is not None: coin = round(float(recv) - float(paid) - ofee - cfee, 8) @@ -297,7 +299,7 @@ def resolve_true_pnl_usdt_from_trade( ) if gate == "pending_sell": return None, "pending_sell" - # 优先严格按用户式:历史币盈亏×卖回价 − 权利金×(开仓价−卖回价) + # 只用:历史币盈亏×卖回价 − 权利金×(开仓价−卖回价) usdt = true_pnl_usdt_premium_move( coin_pnl=coin, premium_paid=paid, @@ -306,28 +308,6 @@ def resolve_true_pnl_usdt_from_trade( ) if usdt is not None: return usdt, "premium_move" - - # 无历史币盈亏时回退收回/已付精确折 U - if ( - paid is not None - and recv is not None - and open_px is not None - and open_px > 0 - and close_px is not None - and close_px > 0 - ): - settled = settle_sim_coin_pnl_usdt( - premium_paid=paid, - premium_received=recv, - open_fee=ofee, - close_fee=cfee, - open_index_px=open_px, - sell_back_px=close_px, - premium_ccy=pc, - ) - usdt2 = _safe_float(settled.get("realized_pnl_usdt")) - if usdt2 is not None: - return usdt2, "premium_move" return None, "none" @@ -342,14 +322,13 @@ def settle_sim_coin_pnl_usdt( fee_ccy: str | None = None, premium_ccy: str = "ETH", ) -> dict[str, float | None]: - """币本位真盈亏(U):含权利金 USDT 波动,不含现货多买. + """由毛权利金生成净币盈亏,再按用户式折 U(费只扣进币盈亏一次). - = 历史币盈亏×卖回价 − (权利金×开仓价 − 权利金×卖回价) - = 收回币×卖回价 − 已付币×开仓指数 − 手续费U - - 不用买币预算当成本(预算含缓冲). + 币盈亏 = 收回 − 已付 − 开/平手续费 + 真盈亏U = 币盈亏×卖回价 − 权利金×(开仓价−卖回价) + 不再用「毛收回折U后再减手续费U」(避免与历史币盈亏口径两套算法). """ - del fee_ccy # 手续费按权利金币种折 U + del fee_ccy paid = float(_safe_float(premium_paid) or 0.0) recv = float(_safe_float(premium_received) or 0.0) ofee = abs(float(_safe_float(open_fee) or 0.0)) @@ -357,6 +336,11 @@ def settle_sim_coin_pnl_usdt( open_idx = _safe_float(open_index_px) sell_px = _safe_float(sell_back_px) net_coin = round(recv - paid - ofee - cfee, 8) + fee_u = None + if open_idx is not None and open_idx > 0 and sell_px is not None and sell_px > 0: + fee_u = round(ofee * float(open_idx) + cfee * float(sell_px), 4) + elif sell_px is not None and sell_px > 0 and (ofee > 0 or cfee > 0): + fee_u = round((ofee + cfee) * float(sell_px), 4) if open_idx is None or open_idx <= 0 or sell_px is None or sell_px <= 0: return { "realized_pnl": net_coin, @@ -366,13 +350,17 @@ def settle_sim_coin_pnl_usdt( "total_fee": round(ofee + cfee, 8), "initial_premium_usdt": None, "proceeds_usdt": None, + "fee_usdt": fee_u, + "premium_ccy": str(premium_ccy or "").strip().upper() or "ETH", } - # 权利金btc+盈利btc = 平仓收回币 - proceeds_u = float(recv) * float(sell_px) initial_u = float(paid) * float(open_idx) - # 开仓费按开仓指数、平仓费按卖回价折 U - fee_u = ofee * float(open_idx) + cfee * float(sell_px) - pnl_u = round(proceeds_u - initial_u - fee_u, 4) + proceeds_u = float(recv) * float(sell_px) + pnl_u = true_pnl_usdt_premium_move( + coin_pnl=net_coin, + premium_paid=paid, + open_index_px=open_idx, + sell_back_px=sell_px, + ) return { "realized_pnl": net_coin, "close_index_px": float(sell_px), @@ -381,7 +369,7 @@ def settle_sim_coin_pnl_usdt( "total_fee": round(ofee + cfee, 8), "initial_premium_usdt": round(initial_u, 4), "proceeds_usdt": round(proceeds_u, 4), - "fee_usdt": round(fee_u, 4), + "fee_usdt": fee_u, "premium_ccy": str(premium_ccy or "").strip().upper() or "ETH", } @@ -403,8 +391,8 @@ def settle_closed_pnl( 币数量净盈亏 = 收回 − 已付 − 开/平手续费(只扣一次). 币本位 U: - 收回×卖回价(index_px) − 已付×开仓指数 − 手续费U - (开仓指数缺失时回退为净币×平仓指数) + 期权收回权利金×卖回价 − 权利金×开仓价 − 手续费U + (开仓价缺失时回退为净币×卖回价;币量不含现货多余仓) USDC: 净额原样为 U. 注意:不要用 budget_usdt 当开仓成本——它含现货买入缓冲. diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py index 8fc6743..63fc833 100644 --- a/lib/options/options_review_lib.py +++ b/lib/options/options_review_lib.py @@ -555,11 +555,6 @@ def sync_options_from_local_trades( ccy = str(r["premium_ccy"] or "").strip().upper() if not ccy: ccy = premium_ccy_for_mode(margin_mode_from_inst_id(inst), underly) - frozen_usdt = _safe_float(r["realized_pnl_usdt"]) - try: - settled = int(r["pnl_settled"] or 0) == 1 - except Exception: - settled = False close_idx = _safe_float(r["close_index_px"]) open_idx = _safe_float(r["open_index_px"]) idx = None @@ -580,60 +575,67 @@ def sync_options_from_local_trades( close_fee=cfee, premium_ccy=ccy, ) - # 权利金币量/币盈亏固定;开仓价与卖回价各自冻一次后真盈亏只读 + # 权利金币量/币盈亏/已冻均价不变;刷新时按公式重算真盈亏U open_idx_for_prem = open_idx if (open_idx is not None and open_idx > 0) else idx sell_idx = close_idx if (close_idx is not None and close_idx > 0) else None - from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade + from lib.options.options_pnl_settle_lib import ( + resolve_spot_vwap_prices, + resolve_true_pnl_usdt_from_trade, + ) status_label = "已平" pnl_u = None - if settled and frozen_usdt is not None: - # 已冻结:只读展示,禁止同步反复改写 - pnl_u = round(float(frozen_usdt), 4) - else: - pnl_u, pnl_src = resolve_true_pnl_usdt_from_trade( - premium_paid=prem, - premium_received=r["premium_received"], - realized_pnl=r["realized_pnl"], - open_index_px=open_idx if (open_idx is not None and open_idx > 0) else None, - close_index_px=sell_idx, - open_fee=ofee, - close_fee=cfee, - premium_ccy=ccy, - fee_ccy=str(r["premium_ccy"] or ccy or ""), - conn=conn, - bridge_id=r["bridge_id"], - ) - if pnl_src == "pending_sell": - status_label = "待卖回" - pnl_u = None - elif pnl_u is not None and pnl_src not in ("none", "pending_sell"): - # 未冻结且可算:只写一次 - try: - from lib.options.options_pnl_settle_lib import resolve_spot_vwap_prices - + # 价已冻则只用库内价;缺价才问桥 VWAP(避免刷新改均价) + prices_frozen = ( + open_idx is not None + and open_idx > 0 + and sell_idx is not None + and sell_idx > 0 + ) + use_bridge = None if prices_frozen else r["bridge_id"] + pnl_u, pnl_src = resolve_true_pnl_usdt_from_trade( + premium_paid=prem, + premium_received=r["premium_received"], + realized_pnl=r["realized_pnl"], + open_index_px=open_idx if (open_idx is not None and open_idx > 0) else None, + close_index_px=sell_idx, + open_fee=ofee, + close_fee=cfee, + premium_ccy=ccy, + fee_ccy=str(r["premium_ccy"] or ccy or ""), + conn=conn if use_bridge is not None else None, + bridge_id=use_bridge, + ) + if pnl_src == "pending_sell": + status_label = "待卖回" + pnl_u = None + elif pnl_u is not None and pnl_src not in ("none", "pending_sell"): + try: + opx, cpx = open_idx, sell_idx + if not prices_frozen: opx, cpx, _g = resolve_spot_vwap_prices( conn, bridge_id=r["bridge_id"], open_index_px=open_idx, close_index_px=sell_idx, ) - conn.execute( - """ - UPDATE options_trades - SET realized_pnl_usdt = ?, - open_index_px = COALESCE(open_index_px, ?), - close_index_px = COALESCE(?, close_index_px), - pnl_settled = 1 - WHERE id = ? AND COALESCE(pnl_settled, 0)=0 - """, - (pnl_u, opx, cpx, trade_id), - ) - except Exception: - pass - else: - status_label = "缺价未核" - pnl_u = None + # 真盈亏U每次刷新按固定量+价重算;均价只补缺不覆盖 + conn.execute( + """ + UPDATE options_trades + SET realized_pnl_usdt = ?, + open_index_px = COALESCE(open_index_px, ?), + close_index_px = COALESCE(close_index_px, ?), + pnl_settled = 1 + WHERE id = ? + """, + (pnl_u, opx, cpx, trade_id), + ) + except Exception: + pass + else: + status_label = "缺价未核" + pnl_u = None # 初始权利金(U)=开仓指数×权利金币量;不含多买 stored_initial = _safe_float(r["initial_premium_usdt"]) prem_u = stored_initial @@ -641,6 +643,7 @@ def sync_options_from_local_trades( prem_u = _to_usdt(prem, ccy=ccy, idx=open_idx_for_prem) if ccy in ("ETH", "BTC") and prem_u is None: prem_u = prem + # 手续费列仅展示(已含在历史币盈亏内,真盈亏不再扣) fee_u = None if ccy in ("ETH", "BTC"): fee_parts = 0.0 diff --git a/lib/sim/broker_lib.py b/lib/sim/broker_lib.py index 0284037..76611a1 100644 --- a/lib/sim/broker_lib.py +++ b/lib/sim/broker_lib.py @@ -790,47 +790,88 @@ class SimBroker: prem_ccy = _premium_ccy_for_inst(inst_id) # 币本位到期兑付用币数量: 实值/指数 × 张数 × 乘数 + # 注意: option_expiry_pnl 的 intrinsic 是 U,减权利金时币本位不能直接用 if prem_ccy in ("ETH", "BTC") and float(spot) > 0: settle_recv = round( max(0.0, (intrinsic_u / float(spot)) * sheets * ct_mult), 8, ) + # 币盈亏 = 兑付币 − 仓位权利金币(与 options_trades.premium_paid 同口径) + pnl = round(float(settle_recv) - float(prem), 8) + else: + settle_recv = round(max(0.0, prem + pnl), 4) + pnl = round(float(pnl), 4) conn = self.get_db() try: conn.execute("DELETE FROM sim_option_positions WHERE inst_id=?", (inst_id,)) try: closed_at = _now() - open_ids = [ - int(r["id"]) - for r in conn.execute( - "SELECT id FROM options_trades WHERE inst_id=? AND status='open'", - (inst_id,), - ).fetchall() - ] - conn.execute( + open_rows = conn.execute( """ - UPDATE options_trades - SET status = 'closed', - close_quote = ?, - premium_received = ?, - realized_pnl = ?, - closed_at = COALESCE(closed_at, ?), - signal_note = CASE - WHEN signal_note IS NULL OR TRIM(signal_note) = '' - THEN '到期结算' - ELSE signal_note - END - WHERE inst_id = ? AND status = 'open' + SELECT id, premium_paid, open_fee, close_fee + FROM options_trades + WHERE inst_id=? AND status='open' """, - ( - round(intrinsic_u, 6), - settle_recv, - round(pnl, 4), - closed_at, - inst_id, - ), - ) + (inst_id,), + ).fetchall() + open_ids = [int(r["id"]) for r in open_rows] + total_paid = 0.0 + for r in open_rows: + try: + total_paid += float(r["premium_paid"] or 0) + except (TypeError, ValueError): + pass + n_open = len(open_rows) or 1 + for r in open_rows: + paid_i = 0.0 + try: + paid_i = float(r["premium_paid"] or 0) + except (TypeError, ValueError): + paid_i = 0.0 + ofee_i = 0.0 + try: + ofee_i = abs(float(r["open_fee"] or 0)) + except (TypeError, ValueError): + ofee_i = 0.0 + # 多笔开仓按权利金占比拆兑付;单笔拿全部 + if n_open == 1: + recv_i = float(settle_recv) + elif total_paid > 1e-12: + recv_i = float(settle_recv) * (paid_i / total_paid) + else: + recv_i = float(settle_recv) / float(n_open) + if prem_ccy in ("ETH", "BTC"): + recv_i = round(recv_i, 8) + pnl_i = round(recv_i - paid_i - ofee_i, 8) + else: + recv_i = round(recv_i, 4) + pnl_i = round(recv_i - paid_i - ofee_i, 4) + conn.execute( + """ + UPDATE options_trades + SET status = 'closed', + close_quote = ?, + premium_received = ?, + realized_pnl = ?, + close_index_px = COALESCE(close_index_px, ?), + closed_at = COALESCE(closed_at, ?), + signal_note = CASE + WHEN signal_note IS NULL OR TRIM(signal_note) = '' + THEN '到期结算' + ELSE signal_note + END + WHERE id = ? AND status = 'open' + """, + ( + round(intrinsic_u, 6), + recv_i, + pnl_i, + float(spot), + closed_at, + int(r["id"]), + ), + ) if open_ids: from lib.options.options_notify_lib import ( active_options_notify_cfg, diff --git a/tests/test_options_pnl_settle.py b/tests/test_options_pnl_settle.py index ef944b1..0afb9af 100644 --- a/tests/test_options_pnl_settle.py +++ b/tests/test_options_pnl_settle.py @@ -75,7 +75,7 @@ class SettleClosedPnlTests(unittest.TestCase): self.assertAlmostEqual(float(s["realized_pnl_usdt"]), expect, places=2) def test_sim_loss_remaining_minus_fees(self): - """亏损:剩余权利金U − 手续费U − 初始U(同一式).""" + """亏损:费先扣进币盈亏,再按用户式折 U(不另扣手续费U).""" s = settle_closed_pnl( premium_paid=0.01, premium_received=0.004, @@ -85,8 +85,30 @@ class SettleClosedPnlTests(unittest.TestCase): index_px=2500, open_index_px=3000, ) - # 0.004*2500 - 0.01*3000 - (0.0001*3000+0.0001*2500) = 10 - 30 - 0.55 = -20.55 - self.assertAlmostEqual(float(s["realized_pnl_usdt"]), -20.55, places=2) + # net_coin=-0.0062; U=-0.0062*2500 - 0.01*(3000-2500)=-20.5 + self.assertAlmostEqual(float(s["realized_pnl"]), -0.0062, places=8) + self.assertAlmostEqual(float(s["realized_pnl_usdt"]), -20.5, places=2) + + def test_resolve_loss_does_not_deduct_fee_again(self): + """历史币盈亏已含费;resolve 只跑用户式,不再用 open_fee/close_fee 扣第二遍.""" + from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade + + # coin 已是 recv-paid-fees=-0.0062 + pnl, src = resolve_true_pnl_usdt_from_trade( + premium_paid=0.01, + premium_received=0.004, + realized_pnl=-0.0062, + open_index_px=3000, + close_index_px=2500, + open_fee=0.0001, + close_fee=0.0001, + premium_ccy="ETH", + ) + self.assertEqual(src, "premium_move") + # -0.0062*2500 - 0.01*(3000-2500) = -15.5 - 5 = -20.5 + self.assertAlmostEqual(float(pnl), -20.5, places=2) + # 若再扣一遍费会更亏于 -20.5 + self.assertGreater(float(pnl), -20.55) def test_budget_ignored_for_usdt(self): # budget 不得抬高开仓成本 diff --git a/tests/test_options_review_pnl_freeze.py b/tests/test_options_review_pnl_freeze.py index 7371c51..3da1e40 100644 --- a/tests/test_options_review_pnl_freeze.py +++ b/tests/test_options_review_pnl_freeze.py @@ -6,8 +6,8 @@ import unittest class ReviewLocalSyncFreezeTests(unittest.TestCase): - def test_settled_true_pnl_not_rewritten_on_resync(self): - """已冻结真盈亏只读;同步不得因桥/指数再改写.""" + def test_refresh_recomputes_u_keeps_frozen_qty_px(self): + """刷新按公式重算U;权利金币/币盈亏/开仓卖回价不变.""" 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 @@ -29,7 +29,7 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase): ) VALUES ( 'BTC-USD-261008-86500-P', 'BTC', 'P', 1, 0.01, 0.01, 0.02, 0.01, 'BTC', - 0, 0, 'closed', 700.0, + 0, 0, 'closed', -51.0, 90000.0, 80000.0, 1, '2026-10-05 05:11:30', '2026-10-07 10:03:53' ) @@ -41,6 +41,7 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase): trade = conn.execute( "SELECT realized_pnl_usdt, open_index_px, close_index_px, pnl_settled, realized_pnl, premium_paid FROM options_trades WHERE id=1" ).fetchone() + # 0.01*80000 - 0.01*(90000-80000)=700;旧错账 -51 被刷新纠正 self.assertAlmostEqual(float(trade["realized_pnl_usdt"]), 700.0, places=2) self.assertAlmostEqual(float(trade["open_index_px"]), 90000.0, places=2) self.assertAlmostEqual(float(trade["close_index_px"]), 80000.0, places=2) diff --git a/tests/test_sim_expiry_coin_pnl.py b/tests/test_sim_expiry_coin_pnl.py new file mode 100644 index 0000000..4025d5c --- /dev/null +++ b/tests/test_sim_expiry_coin_pnl.py @@ -0,0 +1,94 @@ +"""模拟盘到期结算:币本位盈亏须用币,不能把 U 实值减权利金币.""" +from __future__ import annotations + +import sqlite3 +import unittest + +from lib.options.options_db import init_options_tables +from lib.sim.broker_lib import SimBroker +from lib.sim.db_lib import init_sim_tables + + +class SimExpiryCoinPnlTests(unittest.TestCase): + def test_coin_expiry_pnl_not_usd_minus_coin(self): + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + init_options_tables(conn) + init_sim_tables(conn) + # 251001 = 2025-10-01 08:00 UTC 已过期 + inst = "BTC-USD-251001-100000-P" + prem = 0.001 + sheets = 1.0 + ct_mult = 0.01 + spot = 90000.0 + conn.execute( + """ + INSERT INTO sim_option_positions( + inst_id, side, sheets, entry_px, ct_mult, premium_paid_usdc, updated_at + ) VALUES (?,?,?,?,?,?,?) + """, + (inst, "buy", sheets, 0.1, ct_mult, prem, "2025-09-01 10:00:00"), + ) + conn.execute( + """ + INSERT INTO options_trades( + inst_id, underlying, opt_type, strike, sheets, eth_amount, + premium_paid, status, premium_ccy, open_index_px, open_fee, + created_at + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + """, + ( + inst, + "BTC", + "P", + 100000, + int(sheets), + sheets * ct_mult, + prem, + "open", + "BTC", + 95000.0, + 0.0, + "2025-09-01 10:00:00", + ), + ) + conn.commit() + + class _Proxy: + def __getattr__(self, name): + return getattr(conn, name) + + def close(self): + return None + + broker = SimBroker(lambda: _Proxy()) # type: ignore[arg-type] + broker._index_px_for_option = lambda *_a, **_k: spot # type: ignore[method-assign] + # 跳过卖回(本测只核币盈亏) + import lib.options.options_spot_bridge_lib as bridge + + bridge.sell_residual_after_option_flat = lambda *_a, **_k: {"ok": True, "skipped": True} + + # 2025-10-02 UTC + now_ms = 1759363200000 + out = broker.settle_expired_option_positions(None, now_ms=now_ms) + self.assertEqual(len(out), 1) + + intrinsic_u = 100000.0 - spot # 10000 + settle_recv = (intrinsic_u / spot) * sheets * ct_mult + expected_pnl = settle_recv - prem + # 旧 bug: option_expiry_pnl = intrinsic_u*sheets*ct_mult - prem ≈ 99.999 + wrong_old = intrinsic_u * sheets * ct_mult - prem + self.assertGreater(abs(wrong_old - expected_pnl), 1.0) + + row = conn.execute( + "SELECT premium_received, realized_pnl, close_index_px, status FROM options_trades WHERE id=1" + ).fetchone() + self.assertEqual(str(row["status"]), "closed") + self.assertAlmostEqual(float(row["premium_received"]), settle_recv, places=8) + self.assertAlmostEqual(float(row["realized_pnl"]), expected_pnl, places=8) + self.assertAlmostEqual(float(row["close_index_px"]), spot, places=2) + self.assertLess(abs(float(row["realized_pnl"])), 1.0) # 币量级,绝不是 ~100 U + + +if __name__ == "__main__": + unittest.main()