diff --git a/lib/options/options_account_pnl_lib.py b/lib/options/options_account_pnl_lib.py index 12ced19..77f4237 100644 --- a/lib/options/options_account_pnl_lib.py +++ b/lib/options/options_account_pnl_lib.py @@ -245,7 +245,7 @@ def settle_closed_trades_from_trading_account( ) -> int: """卖回后:拍 after 快照,并冻结复盘真盈亏. - realized_pnl_usdt = (权利金+币盈亏)×卖回VWAP − 初始权利金U; + realized_pnl_usdt = 期权币盈亏×平仓价(卖回VWAP优先); 不含现货多买.账户 after 仅审计.已 pnl_settled=1 的行永不改写. """ from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade diff --git a/lib/options/options_close_exec_lib.py b/lib/options/options_close_exec_lib.py index 58b315a..7047500 100644 --- a/lib/options/options_close_exec_lib.py +++ b/lib/options/options_close_exec_lib.py @@ -505,26 +505,20 @@ def close_option_by_bid1( row_open_idx = float(bv) except Exception: pass - row_pc = str(row["premium_ccy"] or row_ccy or premium_ccy or "").strip().upper() settled = settle_closed_pnl( premium_paid=paid, premium_received=recv, open_fee=row_ofee, close_fee=row_cfee, - premium_ccy=row_pc or premium_ccy, + premium_ccy=str(row["premium_ccy"] or row_ccy or premium_ccy), index_px=close_idx, open_index_px=row_open_idx, budget_usdt=row_budget, fee_ccy=str(row["fee_ccy"] or row["premium_ccy"] or row_ccy or ""), ) pnl = settled.get("realized_pnl") - # BTC/ETH:币盈亏可落库;真盈亏U与平仓价等卖回VWAP,禁止用指数占位 - if row_pc in ("ETH", "BTC"): - pnl_usdt = None - close_px_store = None - else: - pnl_usdt = settled.get("realized_pnl_usdt") - close_px_store = settled.get("close_index_px") + # 真盈亏U=期权币盈亏×平仓价 + pnl_usdt = settled.get("realized_pnl_usdt") note_sql = "" # 占位: close_quote/recv/pnl/ord/closed_at/open_fee/close_fee/fee_ccy/close_idx/open_idx/pnl_u/[note]/id @@ -537,7 +531,7 @@ def close_option_by_bid1( row_ofee, row_cfee, str(row["premium_ccy"] or row_ccy or premium_ccy or ""), - close_px_store, + settled.get("close_index_px"), settled.get("open_index_px") or row_open_idx, pnl_usdt, ] diff --git a/lib/options/options_monitor_lib.py b/lib/options/options_monitor_lib.py index e20a335..271f174 100644 --- a/lib/options/options_monitor_lib.py +++ b/lib/options/options_monitor_lib.py @@ -403,14 +403,7 @@ def sync_open_options_trades( fee_ccy=fee_ccy, ) realized_pnl = settled.get("realized_pnl") - # BTC/ETH:真盈亏U与平仓价等卖回VWAP,禁止用指数占位 - pc = str(prem_ccy or "").strip().upper() - if pc in ("ETH", "BTC"): - pnl_usdt = None - close_px_store = None - else: - pnl_usdt = settled.get("realized_pnl_usdt") - close_px_store = settled.get("close_index_px") + pnl_usdt = settled.get("realized_pnl_usdt") conn.execute( """ @@ -438,7 +431,7 @@ def sync_open_options_trades( realized_pnl, close_ord_id, closed_at, - close_px_store, + settled.get("close_index_px"), settled.get("open_index_px"), pnl_usdt, prem_ccy, diff --git a/lib/options/options_pnl_settle_lib.py b/lib/options/options_pnl_settle_lib.py index 499193a..5bcf5b0 100644 --- a/lib/options/options_pnl_settle_lib.py +++ b/lib/options/options_pnl_settle_lib.py @@ -177,33 +177,22 @@ def fee_to_usdt( def true_pnl_usdt_premium_move( *, coin_pnl: Any, - premium_paid: Any, - sell_back_px: Any, + premium_paid: Any = None, + sell_back_px: Any = None, initial_premium_usdt: Any = None, open_index_px: Any = None, ) -> float | None: - """复盘真盈亏(U):币本位×卖回价 − 初始权利金U. + """复盘真盈亏(U)=期权历史币盈亏×平仓价. - 币本位 = 权利金 + 币盈亏 - 真盈亏U = 币本位 × 卖回VWAP − 初始权利金U - 初始权利金U 优先用开仓冻结值;缺失时才用 权利金×开仓价 补算. - 现货多买数量不进公式(只影响均价). + 平仓价优先卖回 VWAP,否则用冻结/传入的 close 价. + premium_paid / initial_premium_usdt / open_index_px 仅兼容旧调用,不参与计算. """ + del premium_paid, initial_premium_usdt, open_index_px pnl = _safe_float(coin_pnl) - paid = _safe_float(premium_paid) close_px = _safe_float(sell_back_px) - if pnl is None or paid is None: + if pnl is None or close_px is None or close_px <= 0: return None - if close_px is None or close_px <= 0: - return None - initial = _safe_float(initial_premium_usdt) - if initial is None: - open_px = _safe_float(open_index_px) - if open_px is None or open_px <= 0: - return None - initial = float(paid) * float(open_px) - coin_basis = float(paid) + float(pnl) - return round(coin_basis * float(close_px) - float(initial), 4) + return round(float(pnl) * float(close_px), 4) def resolve_spot_vwap_prices( @@ -213,18 +202,17 @@ def resolve_spot_vwap_prices( open_index_px: Any = None, close_index_px: Any = None, ) -> tuple[float | None, float | None, str]: - """开仓价/卖回价解析. + """开仓价/平仓价解析(期权盈亏折U用). - 开仓价:优先已冻结 open_index_px,缺失才补桥买 VWAP(不覆盖已冻值). - 卖回价:有桥时只认 close_sell VWAP;桥未关→pending_sell;已关无卖回→missing. - 无桥时 close_index_px 视为调用方传入的卖回价(测试/无现货桥). + 平仓价:桥上卖回 VWAP > 冻结 close_index_px > 开仓价. + 桥未关不再阻断折U(现货卖回与期权盈亏脱钩). 返回 (open_px, close_px, gate): - gate='' 可算; 'pending_sell' 卖回未完成; 'missing' 缺价 + gate='' 可算; 'missing' 缺价 """ open_px = _safe_float(open_index_px) close_hint = _safe_float(close_index_px) - close_px: float | None = None + close_px = close_hint bid = None try: if bridge_id is not None and str(bridge_id).strip() != "": @@ -234,40 +222,23 @@ def resolve_spot_vwap_prices( if conn is not None and bid is not None: try: from lib.options.options_spot_bridge_lib import ( - BRIDGE_CLOSED, bridge_buy_vwap, bridge_sell_vwap, ensure_bridge_table, ) ensure_bridge_table(conn) - row = conn.execute( - "SELECT status FROM options_spot_bridge WHERE id=?", - (bid,), - ).fetchone() - if row is None: - # 桥记录不存在:按无桥处理,close_index 视为调用方卖回价 - close_px = close_hint - else: - if open_px is None or open_px <= 0: - bv = bridge_buy_vwap(conn, bid) - if bv is not None and bv > 0: - open_px = bv - st = str( - row["status"] if hasattr(row, "keys") else (row[0] if row else "") or "" - ) - if st and st != BRIDGE_CLOSED: - return open_px, None, "pending_sell" - sv = bridge_sell_vwap(conn, bid) - if sv is not None and sv > 0: - close_px = sv - else: - # 有桥却无卖回成交:禁止用指数/旧 close_index 冒充 - return open_px, None, "missing" + if open_px is None or open_px <= 0: + bv = bridge_buy_vwap(conn, bid) + if bv is not None and bv > 0: + open_px = bv + sv = bridge_sell_vwap(conn, bid) + if sv is not None and sv > 0: + close_px = sv except Exception: pass - else: - close_px = close_hint + if close_px is None or close_px <= 0: + close_px = open_px if close_px is None or close_px <= 0: return open_px, close_px, "missing" return open_px, close_px, "" @@ -288,11 +259,11 @@ def resolve_true_pnl_usdt_from_trade( conn: Any = None, bridge_id: Any = None, ) -> tuple[float | None, str]: - """由成交字段算复盘真盈亏: (权利金+币盈亏)×卖回价 − 初始权利金U. + """由成交字段算复盘真盈亏: 期权币盈亏×平仓价. - 返回 (pnl_u, source): premium_move | stable | pending_sell | none + 返回 (pnl_u, source): premium_move | stable | none """ - del fee_ccy + del fee_ccy, initial_premium_usdt pc = str(premium_ccy or "").strip().upper() paid = _safe_float(premium_paid) recv = _safe_float(premium_received) @@ -308,20 +279,17 @@ def resolve_true_pnl_usdt_from_trade( return round(float(coin), 4), "stable" return None, "none" - open_px, close_px, gate = resolve_spot_vwap_prices( + _open_px, close_px, gate = resolve_spot_vwap_prices( conn, bridge_id=bridge_id, open_index_px=open_index_px, close_index_px=close_index_px, ) - if gate == "pending_sell": - return None, "pending_sell" + if gate == "missing": + return None, "none" usdt = true_pnl_usdt_premium_move( coin_pnl=coin, - premium_paid=paid, sell_back_px=close_px, - initial_premium_usdt=initial_premium_usdt, - open_index_px=open_px, ) if usdt is not None: return usdt, "premium_move" @@ -339,10 +307,10 @@ def settle_sim_coin_pnl_usdt( fee_ccy: str | None = None, premium_ccy: str = "ETH", ) -> dict[str, float | None]: - """由毛权利金生成净币盈亏,再按用户式折 U(费只扣进币盈亏一次). + """由毛权利金生成净币盈亏,再按期权盈亏折 U(费只扣进币盈亏一次). 币盈亏 = 收回 − 已付 − 开/平手续费 - 真盈亏U = (权利金+币盈亏)×卖回价 − 初始权利金U + 真盈亏U = 币盈亏 × 平仓价 """ del fee_ccy paid = float(_safe_float(premium_paid) or 0.0) @@ -357,33 +325,39 @@ def settle_sim_coin_pnl_usdt( 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: + if sell_px is None or sell_px <= 0: return { "realized_pnl": net_coin, - "close_index_px": float(sell_px) if sell_px is not None and sell_px > 0 else None, + "close_index_px": None, "open_index_px": float(open_idx) if open_idx is not None and open_idx > 0 else None, "realized_pnl_usdt": None, "total_fee": round(ofee + cfee, 8), - "initial_premium_usdt": None, + "initial_premium_usdt": ( + round(float(paid) * float(open_idx), 4) + if open_idx is not None and open_idx > 0 and paid > 0 + else None + ), "proceeds_usdt": None, "fee_usdt": fee_u, "premium_ccy": str(premium_ccy or "").strip().upper() or "ETH", } - initial_u = float(paid) * float(open_idx) + initial_u = ( + float(paid) * float(open_idx) + if open_idx is not None and open_idx > 0 + else None + ) proceeds_u = float(recv) * float(sell_px) pnl_u = true_pnl_usdt_premium_move( coin_pnl=net_coin, - premium_paid=paid, sell_back_px=sell_px, - initial_premium_usdt=initial_u, ) return { "realized_pnl": net_coin, "close_index_px": float(sell_px), - "open_index_px": float(open_idx), + "open_index_px": float(open_idx) if open_idx is not None and open_idx > 0 else None, "realized_pnl_usdt": pnl_u, "total_fee": round(ofee + cfee, 8), - "initial_premium_usdt": round(initial_u, 4), + "initial_premium_usdt": round(initial_u, 4) if initial_u is not None else None, "proceeds_usdt": round(proceeds_u, 4), "fee_usdt": fee_u, "premium_ccy": str(premium_ccy or "").strip().upper() or "ETH", @@ -406,9 +380,7 @@ def settle_closed_pnl( 由「毛权利金」结算. 币数量净盈亏 = 收回 − 已付 − 开/平手续费(只扣一次). - 币本位 U: - (权利金+币盈亏)×卖回价 − 初始权利金U - (开仓价缺失时回退为净币×卖回价;币量不含现货多余仓) + 币本位 U: 净币盈亏×平仓价(币量不含现货多余仓) USDC: 净额原样为 U. 注意:不要用 budget_usdt 当开仓成本——它含现货买入缓冲. @@ -428,8 +400,8 @@ def settle_closed_pnl( if pc in ("ETH", "BTC"): open_idx = _safe_float(open_index_px) sell_px = _safe_float(index_px) - if open_idx is not None and open_idx > 0 and sell_px is not None and sell_px > 0: - out = settle_sim_coin_pnl_usdt( + if sell_px is not None and sell_px > 0: + return settle_sim_coin_pnl_usdt( premium_paid=paid, premium_received=recv, open_fee=ofee, @@ -439,8 +411,6 @@ def settle_closed_pnl( fee_ccy=fee_ccy, premium_ccy=pc, ) - return out - # 无开仓指数:回退净币×平仓价 return freeze_pnl_to_usdt( net, premium_ccy=pc, diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py index 2de3b8b..fcfc8fb 100644 --- a/lib/options/options_review_lib.py +++ b/lib/options/options_review_lib.py @@ -607,10 +607,7 @@ def sync_options_from_local_trades( 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"): + if pnl_u is not None and pnl_src not in ("none", "pending_sell"): try: # 卖回 VWAP 写入 close;开仓价/初始权利金仅缺省时补 heal_initial = stored_initial diff --git a/lib/options/templates/options_review_panel.html b/lib/options/templates/options_review_panel.html index 8b45ac2..dfce26b 100644 --- a/lib/options/templates/options_review_panel.html +++ b/lib/options/templates/options_review_panel.html @@ -229,7 +229,7 @@
期权交易记录
-

真盈亏=历史币盈亏×卖回价 − 权利金×(开仓价−卖回价);开/卖回价为多笔 VWAP。卖回未完标「待卖回」,缺价标「缺价未核」。期权历史仅记币收益。

+

真盈亏=期权历史币盈亏×平仓价(有卖回 VWAP 优先,否则用冻结平仓价)。缺价标「缺价未核」。期权历史仅记币收益。

@@ -239,7 +239,7 @@ 类型 标的/合约 初始权利金 - 真盈亏 + 真盈亏 盈利率 手续费 开仓时间 diff --git a/scripts/repair_options_spot_vwap_from_sim_ledger.py b/scripts/repair_options_spot_vwap_from_sim_ledger.py index 1e01278..de544aa 100644 --- a/scripts/repair_options_spot_vwap_from_sim_ledger.py +++ b/scripts/repair_options_spot_vwap_from_sim_ledger.py @@ -187,10 +187,7 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: initial_u = round(float(paid) * float(use_open), 4) new_u = true_pnl_usdt_premium_move( coin_pnl=coin, - premium_paid=paid, sell_back_px=close_px or old_close, - initial_premium_usdt=initial_u, - open_index_px=use_open, ) item = { "id": tid, diff --git a/tests/test_options_account_pnl.py b/tests/test_options_account_pnl.py index d601128..eb85760 100644 --- a/tests/test_options_account_pnl.py +++ b/tests/test_options_account_pnl.py @@ -15,7 +15,7 @@ class AccountPnlSettleTests(unittest.TestCase): conn = sqlite3.connect(":memory:") conn.row_factory = sqlite3.Row init_options_tables(conn) - # coin_pnl=0.01, open=90000, close=80000 → A=700; 账户差分会是 947 但不采用 + # coin_pnl=0.01, close=80000 → U=800; 账户差分不采用 conn.execute( """ INSERT INTO options_trades( @@ -40,7 +40,7 @@ class AccountPnlSettleTests(unittest.TestCase): "SELECT trading_usdt_after, realized_pnl_usdt, pnl_settled FROM options_trades WHERE id=1" ).fetchone() self.assertAlmostEqual(float(row["trading_usdt_after"]), 1947.05, places=2) - self.assertAlmostEqual(float(row["realized_pnl_usdt"]), 700.0, places=2) + self.assertAlmostEqual(float(row["realized_pnl_usdt"]), 800.0, places=2) self.assertEqual(int(row["pnl_settled"]), 1) # 已冻结不再改 @@ -51,7 +51,7 @@ class AccountPnlSettleTests(unittest.TestCase): row2 = conn.execute( "SELECT realized_pnl_usdt FROM options_trades WHERE id=1" ).fetchone() - self.assertAlmostEqual(float(row2["realized_pnl_usdt"]), 700.0, places=2) + self.assertAlmostEqual(float(row2["realized_pnl_usdt"]), 800.0, places=2) conn.close() def test_stats_use_coin_times_sell_px_not_account(self): @@ -158,17 +158,15 @@ class AccountPnlSettleTests(unittest.TestCase): true_pnl_usdt_premium_move, ) - # (0.01+0.01)*80000 - 0.01*90000 = 700 + # 0.01*80000 = 800 self.assertAlmostEqual( float( true_pnl_usdt_premium_move( coin_pnl=0.01, - premium_paid=0.01, sell_back_px=80000, - initial_premium_usdt=900.0, ) ), - 700.0, + 800.0, places=4, ) pnl, src = resolve_true_pnl_usdt_from_trade( @@ -180,7 +178,7 @@ class AccountPnlSettleTests(unittest.TestCase): premium_ccy="BTC", ) self.assertEqual(src, "premium_move") - self.assertAlmostEqual(float(pnl), 700.0, places=4) + self.assertAlmostEqual(float(pnl), 800.0, places=4) def test_settle_blocked_when_sellable_coin(self): from lib.options.options_account_pnl_lib import try_settle_after_sellback diff --git a/tests/test_options_pnl_settle.py b/tests/test_options_pnl_settle.py index 8315606..439b44b 100644 --- a/tests/test_options_pnl_settle.py +++ b/tests/test_options_pnl_settle.py @@ -26,37 +26,18 @@ class FeeConvertTests(unittest.TestCase): class PremiumMoveFormulaTests(unittest.TestCase): - def test_user_formula_matches_coin_basis_minus_initial(self): + def test_option_pnl_times_close_px(self): coin = 0.01 - paid = 0.01 - open_px = 90000.0 close_px = 80000.0 - initial = paid * open_px a = true_pnl_usdt_premium_move( coin_pnl=coin, - premium_paid=paid, sell_back_px=close_px, - initial_premium_usdt=initial, ) - # (权利金+盈亏)×卖回 − 初始权利金U - expect = (paid + coin) * close_px - initial - self.assertAlmostEqual(float(a), expect, places=4) - # 无费时 ≡ 收回×平 − 已付×开 - recv = paid + coin - self.assertAlmostEqual(float(a), recv * close_px - paid * open_px, places=4) - # 缺初始时用开仓价补算,结果相同 - b = true_pnl_usdt_premium_move( - coin_pnl=coin, - premium_paid=paid, - sell_back_px=close_px, - open_index_px=open_px, - ) - self.assertAlmostEqual(float(b), float(a), places=4) + self.assertAlmostEqual(float(a), coin * close_px, places=4) class SettleClosedPnlTests(unittest.TestCase): def test_sim_net_times_close_index(self): - # 无开仓指数时回退净币×平仓价 s = settle_closed_pnl( premium_paid=0.01, premium_received=0.02, @@ -68,23 +49,23 @@ class SettleClosedPnlTests(unittest.TestCase): self.assertAlmostEqual(float(s["realized_pnl"]), 0.009, places=8) self.assertAlmostEqual(float(s["realized_pnl_usdt"]), 27.0, places=4) - def test_sim_proceeds_minus_open_premium_usdt(self): - """盈利=(收回币)×卖回价 − 已付币×开仓指数 − 手续费U.""" + def test_sim_coin_pnl_times_close(self): + """真盈亏=净币盈亏×平仓价.""" s = settle_closed_pnl( premium_paid=0.011005, premium_received=0.02270864, open_fee=0, close_fee=0, premium_ccy="BTC", - index_px=81000, # 卖回价 - open_index_px=86000, # 初始 U 权利金用开仓指数 + index_px=81000, + open_index_px=86000, ) - # 0.02270864*81000 - 0.011005*86000 - expect = 0.02270864 * 81000 - 0.011005 * 86000 + net = 0.02270864 - 0.011005 + expect = net * 81000 self.assertAlmostEqual(float(s["realized_pnl_usdt"]), expect, places=2) def test_sim_loss_remaining_minus_fees(self): - """亏损:费先扣进币盈亏,再按用户式折 U(不另扣手续费U).""" + """亏损:费先扣进币盈亏,再×平仓价.""" s = settle_closed_pnl( premium_paid=0.01, premium_received=0.004, @@ -94,34 +75,26 @@ class SettleClosedPnlTests(unittest.TestCase): index_px=2500, open_index_px=3000, ) - # 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) + self.assertAlmostEqual(float(s["realized_pnl_usdt"]), -15.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, - initial_premium_usdt=30.0, open_fee=0.0001, close_fee=0.0001, premium_ccy="ETH", ) self.assertEqual(src, "premium_move") - # (0.01-0.0062)*2500 - 30 = 9.5 - 30 = -20.5 - self.assertAlmostEqual(float(pnl), -20.5, places=2) - # 若再扣一遍费会更亏于 -20.5 - self.assertGreater(float(pnl), -20.55) + self.assertAlmostEqual(float(pnl), -15.5, places=2) def test_budget_ignored_for_usdt(self): - # budget 不得抬高开仓成本 s = settle_closed_pnl( premium_paid=0.01, premium_received=0.02, @@ -158,7 +131,6 @@ class SettleClosedPnlTests(unittest.TestCase): class SimPremiumPnlStatsTests(unittest.TestCase): def test_sync_sim_no_longer_overwrites_account_settled(self): - """账户结算后 sync 兼容入口不得改写.""" import sqlite3 from lib.options.options_db import init_options_tables @@ -235,13 +207,11 @@ class ExchangeFreezeStatsTests(unittest.TestCase): ] frozen = apply_exchange_pnl_freeze(conn, history, _Ex()) converted = history_pnl_to_usdt(frozen, _Ex()) - # 0.000138*100000=13.8; -0.00648*2500=-16.2; net=-2.4 self.assertAlmostEqual(float(converted[0]["realized_pnl"]), 13.8, places=4) self.assertAlmostEqual(float(converted[1]["realized_pnl"]), -16.2, places=4) stats = compute_options_stats_from_history(converted) self.assertAlmostEqual(float(stats["net_realized_pnl"]), -2.4, places=4) - # 指数再变也不改冻结 U class _Ex2: def public_get_market_ticker(self, params): return {"data": [{"last": "999999"}]} diff --git a/tests/test_options_review_pnl_freeze.py b/tests/test_options_review_pnl_freeze.py index db64d6a..fc00527 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 @@ -7,7 +7,7 @@ import unittest class ReviewLocalSyncFreezeTests(unittest.TestCase): def test_refresh_recomputes_u_keeps_frozen_qty_px(self): - """刷新按公式重算U;权利金币/币盈亏不变.""" + """刷新按币盈亏×平仓价重算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 @@ -41,22 +41,21 @@ 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) + # 0.01*80000=800;旧错账 -51 被刷新纠正 + self.assertAlmostEqual(float(trade["realized_pnl_usdt"]), 800.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) self.assertEqual(int(trade["pnl_settled"]), 1) self.assertAlmostEqual(float(trade["realized_pnl"]), 0.01, places=8) - self.assertAlmostEqual(float(trade["premium_paid"]), 0.01, places=8) row = conn.execute( "SELECT realized_pnl_total FROM options_review_trades WHERE history_key=?", ("local_opt:1",), ).fetchone() - self.assertAlmostEqual(float(row["realized_pnl_total"]), 700.0, places=2) + self.assertAlmostEqual(float(row["realized_pnl_total"]), 800.0, places=2) conn.close() def test_refresh_prefers_bridge_sell_vwap_over_wrong_index(self): - """库内卖回价若是指数占位,刷新须用桥上卖回 VWAP;初始权利金U冻结不改.""" + """有卖回 VWAP 时优先用它折U.""" 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 ( @@ -86,13 +85,13 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase): inst_id, underlying, opt_type, sheets, eth_amount, premium_paid, premium_received, realized_pnl, premium_ccy, open_fee, close_fee, status, realized_pnl_usdt, - open_index_px, close_index_px, initial_premium_usdt, pnl_settled, bridge_id, + open_index_px, close_index_px, pnl_settled, bridge_id, created_at, closed_at ) VALUES ( 'BTC-USD-261015-84000-P', 'BTC', 'P', 1, 0.01, 0.01, 0.02, 0.01, 'BTC', 0, 0, 'closed', 1544.0, - 88086.0, 85400.0, 840.10, 1, ?, + 88086.0, 85400.0, 1, ?, '2026-10-12 01:46:50', '2026-10-13 08:07:35' ) """, @@ -101,14 +100,11 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase): conn.commit() sync_options_from_local_trades(conn, None) trade = conn.execute( - "SELECT realized_pnl_usdt, open_index_px, close_index_px, initial_premium_usdt FROM options_trades WHERE id=1" + "SELECT realized_pnl_usdt, close_index_px FROM options_trades WHERE id=1" ).fetchone() - # 已冻开仓价/初始权利金不覆盖;卖回价用桥 VWAP - self.assertAlmostEqual(float(trade["open_index_px"]), 88086.0, places=2) - self.assertAlmostEqual(float(trade["initial_premium_usdt"]), 840.10, places=2) self.assertAlmostEqual(float(trade["close_index_px"]), 81418.0, places=2) - # (0.01+0.01)*81418 - 840.10 = 788.26 - self.assertAlmostEqual(float(trade["realized_pnl_usdt"]), 788.26, places=2) + # 0.01*81418 + self.assertAlmostEqual(float(trade["realized_pnl_usdt"]), 814.18, places=2) conn.close() def test_premium_usdt_move_formula(self): @@ -122,9 +118,6 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase): init_options_tables(conn) ensure_runtime_settings_table(conn) runtime_set(conn, TRADING_MODE_KEY, MODE_SIM) - # paid=0.01, recv=0.02, coin_pnl=0.01; open=90000, close=80000 - # A = 0.01*80000 - (0.01*90000 - 0.01*80000) = 800 - 100 = 700 - # 亦= recv*close - paid*open = 1600 - 900 = 700 conn.execute( """ INSERT INTO options_trades( @@ -151,12 +144,11 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase): "SELECT realized_pnl_total FROM options_review_trades WHERE history_key=?", ("local_opt:1",), ).fetchone() - self.assertAlmostEqual(float(row["realized_pnl_total"]), 700.0, places=2) - # 纠偏写回,不用账户差分 1506 + self.assertAlmostEqual(float(row["realized_pnl_total"]), 800.0, places=2) healed = conn.execute( "SELECT realized_pnl_usdt FROM options_trades WHERE id=1" ).fetchone() - self.assertAlmostEqual(float(healed["realized_pnl_usdt"]), 700.0, places=2) + self.assertAlmostEqual(float(healed["realized_pnl_usdt"]), 800.0, places=2) conn.close() def test_uncomputable_old_trade_marked(self): @@ -170,7 +162,6 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase): init_options_tables(conn) ensure_runtime_settings_table(conn) runtime_set(conn, TRADING_MODE_KEY, MODE_SIM) - # 未冻结且无开仓/卖回价 → 缺价未核(不瞎写) conn.execute( """ INSERT INTO options_trades( @@ -233,7 +224,6 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase): "SELECT realized_pnl_total FROM options_review_trades WHERE history_key=?", ("local_opt:1",), ).fetchone() - # 开平价相同:波动项为0 → 等于币×卖回价=850 self.assertAlmostEqual(float(row["realized_pnl_total"]), 850.0, places=2) conn.close() diff --git a/tests/test_options_spot_vwap.py b/tests/test_options_spot_vwap.py index b7fd329..8f97013 100644 --- a/tests/test_options_spot_vwap.py +++ b/tests/test_options_spot_vwap.py @@ -24,12 +24,10 @@ class SpotVwapBridgeTests(unittest.TestCase): bid = insert_bridge(conn, underlying="BTC", status="holding", budget_usdt=2200) add_bridge_buy_fill(conn, bid, usdt_spent=1000.0, coin_bought=0.5) add_bridge_buy_fill(conn, bid, usdt_spent=1200.0, coin_bought=0.6) - # 2200 / 1.1 = 2000 self.assertAlmostEqual(float(bridge_buy_vwap(conn, bid)), 2000.0, places=4) add_bridge_close_sell_fill(conn, bid, usdt_recovered=800.0, coin_sold=0.4) add_bridge_close_sell_fill(conn, bid, usdt_recovered=1200.0, coin_sold=0.6) - # 2000 / 1.0 = 2000 self.assertAlmostEqual(float(bridge_sell_vwap(conn, bid)), 2000.0, places=4) update_bridge(conn, bid, status=BRIDGE_CLOSED, close=True) self.assertAlmostEqual(float(bridge_sell_vwap(conn, bid)), 2000.0, places=4) @@ -56,15 +54,13 @@ class SpotVwapBridgeTests(unittest.TestCase): add_bridge_close_sell_fill(conn, bid, usdt_recovered=1600.0, coin_sold=0.8) add_bridge_close_sell_fill(conn, bid, usdt_recovered=400.0, coin_sold=0.2) update_bridge(conn, bid, status=BRIDGE_CLOSED, close=True) - # buy_vwap=2000, sell_vwap=2000; 初始权利金按买 VWAP=20 - # U=(0.01+0.01)*2000 - 20 = 20; 若误用指数 80000 会得到大数 + # sell_vwap=2000; U=0.01*2000=20; 不用指数 80000 pnl, src = resolve_true_pnl_usdt_from_trade( premium_paid=0.01, premium_received=0.02, realized_pnl=0.01, - open_index_px=2000.0, + open_index_px=90000.0, close_index_px=80000.0, - initial_premium_usdt=20.0, premium_ccy="BTC", conn=conn, bridge_id=bid, @@ -73,13 +69,12 @@ class SpotVwapBridgeTests(unittest.TestCase): self.assertAlmostEqual(float(pnl), 20.0, places=2) conn.close() - def test_pending_sell_blocks_true_pnl(self): + def test_pending_sell_still_allows_option_pnl(self): + """现货待卖回不阻断期权盈亏折U(用冻结平仓价).""" from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade from lib.options.options_spot_bridge_lib import ( - BRIDGE_HOLDING, BRIDGE_PENDING_SELL, add_bridge_buy_fill, - add_bridge_close_sell_fill, ensure_bridge_table, insert_bridge, update_bridge, @@ -90,7 +85,6 @@ class SpotVwapBridgeTests(unittest.TestCase): ensure_bridge_table(conn) bid = insert_bridge(conn, underlying="ETH", status="holding", budget_usdt=3000) add_bridge_buy_fill(conn, bid, usdt_spent=3000.0, coin_bought=1.0) - add_bridge_close_sell_fill(conn, bid, usdt_recovered=1500.0, coin_sold=0.5) update_bridge(conn, bid, status=BRIDGE_PENDING_SELL) pnl, src = resolve_true_pnl_usdt_from_trade( premium_paid=0.01, @@ -102,26 +96,12 @@ class SpotVwapBridgeTests(unittest.TestCase): conn=conn, bridge_id=bid, ) - self.assertEqual(src, "pending_sell") - self.assertIsNone(pnl) - # holding 未关桥同样不得用指数提前结算 - update_bridge(conn, bid, status=BRIDGE_HOLDING) - pnl2, src2 = resolve_true_pnl_usdt_from_trade( - premium_paid=0.01, - premium_received=0.02, - realized_pnl=0.01, - open_index_px=3000.0, - close_index_px=3000.0, - premium_ccy="ETH", - conn=conn, - bridge_id=bid, - ) - self.assertEqual(src2, "pending_sell") - self.assertIsNone(pnl2) + self.assertEqual(src, "premium_move") + self.assertAlmostEqual(float(pnl), 30.0, places=2) conn.close() - def test_closed_bridge_without_sell_vwap_is_missing(self): - """已关桥但无 close_sell 时不得用指数冒充卖回价.""" + def test_closed_bridge_without_sell_falls_back_close_index(self): + """已关桥无卖回 VWAP 时回退冻结平仓价.""" from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade from lib.options.options_spot_bridge_lib import ( BRIDGE_CLOSED, @@ -142,17 +122,15 @@ class SpotVwapBridgeTests(unittest.TestCase): realized_pnl=0.01, open_index_px=100000.0, close_index_px=90000.0, - initial_premium_usdt=1000.0, premium_ccy="BTC", conn=conn, bridge_id=bid, ) - self.assertEqual(src, "none") - self.assertIsNone(pnl) + self.assertEqual(src, "premium_move") + self.assertAlmostEqual(float(pnl), 900.0, places=2) conn.close() def test_buy_vwap_ignores_shrunk_coin_bought(self): - """开仓后 coin_bought 被改成权利金量时,不得用 budget/缩水量当买价.""" from lib.options.options_spot_bridge_lib import ( add_bridge_buy_fill, bridge_buy_vwap, @@ -166,7 +144,7 @@ class SpotVwapBridgeTests(unittest.TestCase): ensure_bridge_table(conn) bid = insert_bridge(conn, underlying="BTC", status="holding", budget_usdt=2200) add_bridge_buy_fill(conn, bid, usdt_spent=2200.0, coin_bought=1.1) - update_bridge(conn, bid, coin_bought=0.01) # 缩成权利金 + update_bridge(conn, bid, coin_bought=0.01) self.assertAlmostEqual(float(bridge_buy_vwap(conn, bid)), 2000.0, places=4) conn.close()