From 839baea375076633faa060167307dbac0471bef9 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 13 Oct 2026 12:29:33 +0800 Subject: [PATCH] =?UTF-8?q?fix=E7=9C=9F=E7=9B=88=E4=BA=8F=E6=94=B6?= =?UTF-8?q?=E5=8F=A3=E4=B8=BA=E5=B8=81=E6=9C=AC=E4=BD=8D=C3=97=E5=8D=96?= =?UTF-8?q?=E5=9B=9E=E4=BB=B7=E5=87=8F=E5=88=9D=E5=A7=8B=E6=9D=83=E5=88=A9?= =?UTF-8?q?=E9=87=91U=EF=BC=8C=E7=A6=81=E6=8C=87=E6=95=B0=E5=86=92?= =?UTF-8?q?=E5=85=85=E5=B9=B3=E4=BB=93=E4=BB=B7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- lib/options/options_account_pnl_lib.py | 19 ++-- lib/options/options_close_exec_lib.py | 14 ++- lib/options/options_monitor_lib.py | 11 ++- lib/options/options_pnl_settle_lib.py | 88 +++++++++++-------- lib/options/options_review_lib.py | 23 +++-- ...epair_options_spot_vwap_from_sim_ledger.py | 18 ++-- tests/test_options_account_pnl.py | 4 +- tests/test_options_pnl_settle.py | 22 +++-- tests/test_options_review_pnl_freeze.py | 16 ++-- tests/test_options_spot_vwap.py | 39 +++++++- 10 files changed, 171 insertions(+), 83 deletions(-) diff --git a/lib/options/options_account_pnl_lib.py b/lib/options/options_account_pnl_lib.py index b2b0d2f..12ced19 100644 --- a/lib/options/options_account_pnl_lib.py +++ b/lib/options/options_account_pnl_lib.py @@ -243,9 +243,9 @@ def settle_closed_trades_from_trading_account( bridge_id: int | None = None, trade_id: int | None = None, ) -> int: - """卖回后:拍 after 快照,并冻结复盘真盈亏(权利金 USDT 波动口径). + """卖回后:拍 after 快照,并冻结复盘真盈亏. - realized_pnl_usdt = 历史币盈亏×卖回价 − (权利金×开仓价 − 权利金×卖回价); + realized_pnl_usdt = (权利金+币盈亏)×卖回VWAP − 初始权利金U; 不含现货多买.账户 after 仅审计.已 pnl_settled=1 的行永不改写. """ from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade @@ -269,7 +269,7 @@ def settle_closed_trades_from_trading_account( f""" SELECT id, trading_usdt_before, premium_paid, premium_received, realized_pnl, open_index_px, close_index_px, open_fee, close_fee, premium_ccy, fee_ccy, - bridge_id + bridge_id, initial_premium_usdt FROM options_trades WHERE {" AND ".join(clauses)} """, @@ -287,6 +287,9 @@ def settle_closed_trades_from_trading_account( row_bid = row["bridge_id"] if hasattr(row, "keys") else row[11] if row_bid is None and bridge_id is not None: row_bid = bridge_id + initial_u = ( + row["initial_premium_usdt"] if hasattr(row, "keys") else row[12] + ) open_px, close_px, _gate = resolve_spot_vwap_prices( conn, bridge_id=row_bid, @@ -299,6 +302,7 @@ def settle_closed_trades_from_trading_account( realized_pnl=row["realized_pnl"] if hasattr(row, "keys") else row[4], open_index_px=open_px, close_index_px=close_px, + initial_premium_usdt=initial_u, open_fee=row["open_fee"] if hasattr(row, "keys") else row[7], close_fee=row["close_fee"] if hasattr(row, "keys") else row[8], premium_ccy=row["premium_ccy"] if hasattr(row, "keys") else row[9], @@ -308,7 +312,11 @@ def settle_closed_trades_from_trading_account( ) if pnl_u is None or src in ("none", "pending_sell"): continue - # 卖回价一次冻结为卖回 VWAP;开仓价已在开仓时冻住,仅缺省时补 + # 卖回价冻为卖回 VWAP;开仓价/初始权利金已在开仓时冻住,仅缺省时补 + paid = _safe_float(row["premium_paid"] if hasattr(row, "keys") else row[2]) + heal_initial = _safe_float(initial_u) + if heal_initial is None and paid is not None and open_px is not None and open_px > 0: + heal_initial = round(float(paid) * float(open_px), 4) conn.execute( """ UPDATE options_trades @@ -316,10 +324,11 @@ def settle_closed_trades_from_trading_account( realized_pnl_usdt = ?, open_index_px = COALESCE(open_index_px, ?), close_index_px = COALESCE(?, close_index_px), + initial_premium_usdt = COALESCE(initial_premium_usdt, ?), pnl_settled = 1 WHERE id = ? AND COALESCE(pnl_settled, 0)=0 """, - (after_use, pnl_u, open_px, close_px, tid), + (after_use, pnl_u, open_px, close_px, heal_initial, tid), ) updated += 1 return updated diff --git a/lib/options/options_close_exec_lib.py b/lib/options/options_close_exec_lib.py index 1b1c60e..58b315a 100644 --- a/lib/options/options_close_exec_lib.py +++ b/lib/options/options_close_exec_lib.py @@ -505,20 +505,26 @@ 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=str(row["premium_ccy"] or row_ccy or premium_ccy), + premium_ccy=row_pc 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") - # 复盘真盈亏=权利金 USDT 波动口径;账户 after 另在卖回后拍快照 - pnl_usdt = settled.get("realized_pnl_usdt") + # 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") note_sql = "" # 占位: close_quote/recv/pnl/ord/closed_at/open_fee/close_fee/fee_ccy/close_idx/open_idx/pnl_u/[note]/id @@ -531,7 +537,7 @@ def close_option_by_bid1( row_ofee, row_cfee, str(row["premium_ccy"] or row_ccy or premium_ccy or ""), - settled.get("close_index_px"), + close_px_store, 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 271f174..e20a335 100644 --- a/lib/options/options_monitor_lib.py +++ b/lib/options/options_monitor_lib.py @@ -403,7 +403,14 @@ def sync_open_options_trades( fee_ccy=fee_ccy, ) realized_pnl = settled.get("realized_pnl") - pnl_usdt = settled.get("realized_pnl_usdt") + # 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") conn.execute( """ @@ -431,7 +438,7 @@ def sync_open_options_trades( realized_pnl, close_ord_id, closed_at, - settled.get("close_index_px"), + close_px_store, 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 082c4da..499193a 100644 --- a/lib/options/options_pnl_settle_lib.py +++ b/lib/options/options_pnl_settle_lib.py @@ -178,29 +178,32 @@ def true_pnl_usdt_premium_move( *, coin_pnl: Any, premium_paid: Any, - open_index_px: Any, sell_back_px: Any, + initial_premium_usdt: Any = None, + open_index_px: Any = None, ) -> float | None: - """复盘真盈亏(U):计入权利金 USDT 波动;现货多买数量不进公式. + """复盘真盈亏(U):币本位×卖回价 − 初始权利金U. - 真盈亏 = 期权历史盈亏(币)×平仓卖回价 - − (权利金×开仓价 − 权利金×平仓卖回价) - = 历史币盈亏×卖回价 − 权利金×(开仓价 − 卖回价) - - 币量=期权腿(realized_pnl / premium_paid);价=买卖 VWAP. - 无手续费时 ≡ 期权收回权利金×卖回价 − 权利金×开仓价 - (收回≠账户卖掉的全部现货;多余仓只影响均价). + 币本位 = 权利金 + 币盈亏 + 真盈亏U = 币本位 × 卖回VWAP − 初始权利金U + 初始权利金U 优先用开仓冻结值;缺失时才用 权利金×开仓价 补算. + 现货多买数量不进公式(只影响均价). """ pnl = _safe_float(coin_pnl) paid = _safe_float(premium_paid) - open_px = _safe_float(open_index_px) close_px = _safe_float(sell_back_px) if pnl is None or paid is None: return None - if open_px is None or open_px <= 0 or close_px is None or close_px <= 0: + if close_px is None or close_px <= 0: return None - prem_move = float(paid) * float(open_px) - float(paid) * float(close_px) - return round(float(pnl) * float(close_px) - prem_move, 4) + 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) def resolve_spot_vwap_prices( @@ -210,13 +213,18 @@ def resolve_spot_vwap_prices( open_index_px: Any = None, close_index_px: Any = None, ) -> tuple[float | None, float | None, str]: - """开仓价/卖回价:优先桥上多笔 VWAP,否则冻结指数. + """开仓价/卖回价解析. + + 开仓价:优先已冻结 open_index_px,缺失才补桥买 VWAP(不覆盖已冻值). + 卖回价:有桥时只认 close_sell VWAP;桥未关→pending_sell;已关无卖回→missing. + 无桥时 close_index_px 视为调用方传入的卖回价(测试/无现货桥). 返回 (open_px, close_px, gate): gate='' 可算; 'pending_sell' 卖回未完成; 'missing' 缺价 """ open_px = _safe_float(open_index_px) - close_px = _safe_float(close_index_px) + close_hint = _safe_float(close_index_px) + close_px: float | None = None bid = None try: if bridge_id is not None and str(bridge_id).strip() != "": @@ -233,26 +241,34 @@ def resolve_spot_vwap_prices( ) ensure_bridge_table(conn) - bv = bridge_buy_vwap(conn, bid) - if bv is not None and bv > 0: - open_px = bv row = conn.execute( "SELECT status FROM options_spot_bridge WHERE id=?", (bid,), ).fetchone() - st = str( - row["status"] if row and hasattr(row, "keys") else (row[0] if row else "") or "" - ) - # 桥未关闭(=卖回流程未结束):禁止用平仓指数冒充卖回 VWAP 提前结算 - 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 - # 已关桥但无卖回 VWAP:允许回退 close_index_px;两者都无则 missing + 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" except Exception: pass - if open_px is None or open_px <= 0 or close_px is None or close_px <= 0: + else: + close_px = close_hint + if close_px is None or close_px <= 0: return open_px, close_px, "missing" return open_px, close_px, "" @@ -264,6 +280,7 @@ def resolve_true_pnl_usdt_from_trade( realized_pnl: Any = None, open_index_px: Any = None, close_index_px: Any = None, + initial_premium_usdt: Any = None, open_fee: Any = 0, close_fee: Any = 0, premium_ccy: str | None = None, @@ -271,7 +288,7 @@ def resolve_true_pnl_usdt_from_trade( conn: Any = None, bridge_id: Any = None, ) -> tuple[float | None, str]: - """由成交字段算复盘真盈亏;开/卖回价优先多笔 VWAP. + """由成交字段算复盘真盈亏: (权利金+币盈亏)×卖回价 − 初始权利金U. 返回 (pnl_u, source): premium_move | stable | pending_sell | none """ @@ -299,12 +316,12 @@ 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, - open_index_px=open_px, 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" @@ -325,8 +342,7 @@ def settle_sim_coin_pnl_usdt( """由毛权利金生成净币盈亏,再按用户式折 U(费只扣进币盈亏一次). 币盈亏 = 收回 − 已付 − 开/平手续费 - 真盈亏U = 币盈亏×卖回价 − 权利金×(开仓价−卖回价) - 不再用「毛收回折U后再减手续费U」(避免与历史币盈亏口径两套算法). + 真盈亏U = (权利金+币盈亏)×卖回价 − 初始权利金U """ del fee_ccy paid = float(_safe_float(premium_paid) or 0.0) @@ -358,8 +374,8 @@ def settle_sim_coin_pnl_usdt( pnl_u = true_pnl_usdt_premium_move( coin_pnl=net_coin, premium_paid=paid, - open_index_px=open_idx, sell_back_px=sell_px, + initial_premium_usdt=initial_u, ) return { "realized_pnl": net_coin, @@ -391,7 +407,7 @@ def settle_closed_pnl( 币数量净盈亏 = 收回 − 已付 − 开/平手续费(只扣一次). 币本位 U: - 期权收回权利金×卖回价 − 权利金×开仓价 − 手续费U + (权利金+币盈亏)×卖回价 − 初始权利金U (开仓价缺失时回退为净币×卖回价;币量不含现货多余仓) USDC: 净额原样为 U. diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py index 21e7949..2de3b8b 100644 --- a/lib/options/options_review_lib.py +++ b/lib/options/options_review_lib.py @@ -575,7 +575,8 @@ def sync_options_from_local_trades( close_fee=cfee, premium_ccy=ccy, ) - # 权利金币量/币盈亏/已冻均价不变;刷新时按公式重算真盈亏U + # 币本位/初始权利金U 冻结不变;刷新只按卖回价重算真盈亏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 sell_idx = close_idx if (close_idx is not None and close_idx > 0) else None from lib.options.options_pnl_settle_lib import ( @@ -585,7 +586,7 @@ def sync_options_from_local_trades( status_label = "已平" pnl_u = None - # 始终问桥:买/卖回 VWAP 优先于库内指数占位价(防预算反推开仓价、平仓指数冒充卖回价) + # 有桥时卖回价只认 close_sell VWAP;开仓价不覆盖已冻值 opx, cpx, _gate = resolve_spot_vwap_prices( conn, bridge_id=r["bridge_id"], @@ -598,6 +599,7 @@ def sync_options_from_local_trades( realized_pnl=r["realized_pnl"], open_index_px=opx, close_index_px=cpx, + initial_premium_usdt=stored_initial, open_fee=ofee, close_fee=cfee, premium_ccy=ccy, @@ -610,30 +612,35 @@ def sync_options_from_local_trades( pnl_u = None elif pnl_u is not None and pnl_src not in ("none", "pending_sell"): try: - # VWAP 非空时覆盖指数占位;币量字段不改 + # 卖回 VWAP 写入 close;开仓价/初始权利金仅缺省时补 + heal_initial = stored_initial + if heal_initial is None and prem is not None and opx is not None and opx > 0: + heal_initial = round(float(prem) * float(opx), 4) conn.execute( """ UPDATE options_trades SET realized_pnl_usdt = ?, - open_index_px = COALESCE(?, open_index_px), + open_index_px = COALESCE(open_index_px, ?), close_index_px = COALESCE(?, close_index_px), + initial_premium_usdt = COALESCE(initial_premium_usdt, ?), pnl_settled = 1 WHERE id = ? """, - (pnl_u, opx, cpx, trade_id), + (pnl_u, opx, cpx, heal_initial, trade_id), ) - if opx is not None and opx > 0: + if (open_idx is None or open_idx <= 0) and opx is not None and opx > 0: open_idx = opx open_idx_for_prem = opx if cpx is not None and cpx > 0: sell_idx = cpx + if stored_initial is None and heal_initial is not None: + stored_initial = heal_initial except Exception: pass else: status_label = "缺价未核" pnl_u = None - # 初始权利金(U)=开仓指数×权利金币量;不含多买 - stored_initial = _safe_float(r["initial_premium_usdt"]) + # 初始权利金U:优先开仓冻结值 prem_u = stored_initial if prem_u is None: prem_u = _to_usdt(prem, ccy=ccy, idx=open_idx_for_prem) diff --git a/scripts/repair_options_spot_vwap_from_sim_ledger.py b/scripts/repair_options_spot_vwap_from_sim_ledger.py index 8f54ced..1e01278 100644 --- a/scripts/repair_options_spot_vwap_from_sim_ledger.py +++ b/scripts/repair_options_spot_vwap_from_sim_ledger.py @@ -155,7 +155,7 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: """ SELECT id, inst_id, underlying, premium_paid, premium_received, realized_pnl, open_index_px, close_index_px, realized_pnl_usdt, bridge_id, - created_at, closed_at, budget_usdt + created_at, closed_at, budget_usdt, initial_premium_usdt FROM options_trades WHERE status='closed' ORDER BY id @@ -181,11 +181,16 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: old_open = _safe_float(r["open_index_px"]) old_close = _safe_float(r["close_index_px"]) old_u = _safe_float(r["realized_pnl_usdt"]) + use_open = open_px or old_open + initial_u = _safe_float(r["initial_premium_usdt"]) + if initial_u is None and paid is not None and use_open is not None and use_open > 0: + initial_u = round(float(paid) * float(use_open), 4) new_u = true_pnl_usdt_premium_move( coin_pnl=coin, premium_paid=paid, - open_index_px=open_px or old_open, sell_back_px=close_px or old_close, + initial_premium_usdt=initial_u, + open_index_px=use_open, ) item = { "id": tid, @@ -231,11 +236,7 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: SET open_index_px = COALESCE(?, open_index_px), close_index_px = COALESCE(?, close_index_px), realized_pnl_usdt = COALESCE(?, realized_pnl_usdt), - initial_premium_usdt = CASE - WHEN ? IS NOT NULL AND premium_paid IS NOT NULL - THEN round(premium_paid * ?, 4) - ELSE initial_premium_usdt - END, + initial_premium_usdt = COALESCE(?, initial_premium_usdt), pnl_settled = 1 WHERE id = ? """, @@ -243,8 +244,7 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: open_px, close_px, new_u, - open_px, - open_px, + initial_u, tid, ), ) diff --git a/tests/test_options_account_pnl.py b/tests/test_options_account_pnl.py index 63a6781..d601128 100644 --- a/tests/test_options_account_pnl.py +++ b/tests/test_options_account_pnl.py @@ -158,14 +158,14 @@ class AccountPnlSettleTests(unittest.TestCase): true_pnl_usdt_premium_move, ) - # A = 0.01*80000 - (0.01*90000 - 0.01*80000) = 700 + # (0.01+0.01)*80000 - 0.01*90000 = 700 self.assertAlmostEqual( float( true_pnl_usdt_premium_move( coin_pnl=0.01, premium_paid=0.01, - open_index_px=90000, sell_back_px=80000, + initial_premium_usdt=900.0, ) ), 700.0, diff --git a/tests/test_options_pnl_settle.py b/tests/test_options_pnl_settle.py index 0afb9af..8315606 100644 --- a/tests/test_options_pnl_settle.py +++ b/tests/test_options_pnl_settle.py @@ -26,23 +26,32 @@ class FeeConvertTests(unittest.TestCase): class PremiumMoveFormulaTests(unittest.TestCase): - def test_user_formula_matches_proceeds_minus_open_cost(self): + def test_user_formula_matches_coin_basis_minus_initial(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, - open_index_px=open_px, sell_back_px=close_px, + initial_premium_usdt=initial, ) - # 历史盈亏×卖回 − (权利金×开 − 权利金×平) - expect = coin * close_px - (paid * open_px - paid * close_px) + # (权利金+盈亏)×卖回 − 初始权利金U + expect = (paid + coin) * close_px - initial self.assertAlmostEqual(float(a), expect, places=4) - # 无费时 ≡ 收回×平 − 已付×开 (recv=paid+coin) + # 无费时 ≡ 收回×平 − 已付×开 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) class SettleClosedPnlTests(unittest.TestCase): @@ -100,12 +109,13 @@ class SettleClosedPnlTests(unittest.TestCase): 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.0062*2500 - 0.01*(3000-2500) = -15.5 - 5 = -20.5 + # (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) diff --git a/tests/test_options_review_pnl_freeze.py b/tests/test_options_review_pnl_freeze.py index bec5adc..db64d6a 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 @@ -56,7 +56,7 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase): conn.close() def test_refresh_prefers_bridge_sell_vwap_over_wrong_index(self): - """库内卖回价若是指数占位,刷新须用桥上卖回 VWAP 纠正.""" + """库内卖回价若是指数占位,刷新须用桥上卖回 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 +86,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, pnl_settled, bridge_id, + open_index_px, close_index_px, initial_premium_usdt, 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, 1, ?, + 88086.0, 85400.0, 840.10, 1, ?, '2026-10-12 01:46:50', '2026-10-13 08:07:35' ) """, @@ -101,11 +101,13 @@ 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 FROM options_trades WHERE id=1" + "SELECT realized_pnl_usdt, open_index_px, close_index_px, initial_premium_usdt FROM options_trades WHERE id=1" ).fetchone() - self.assertAlmostEqual(float(trade["open_index_px"]), 84010.0, places=2) + # 已冻开仓价/初始权利金不覆盖;卖回价用桥 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) - # A = 0.01*81418 - 0.01*(84010-81418) = 814.18 - 25.92 = 788.26 + # (0.01+0.01)*81418 - 840.10 = 788.26 self.assertAlmostEqual(float(trade["realized_pnl_usdt"]), 788.26, places=2) conn.close() diff --git a/tests/test_options_spot_vwap.py b/tests/test_options_spot_vwap.py index df4129c..b7fd329 100644 --- a/tests/test_options_spot_vwap.py +++ b/tests/test_options_spot_vwap.py @@ -56,15 +56,15 @@ 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; coin_pnl=0.01 - # A = 0.01*2000 - (0.01*2000 - 0.01*2000) = 20 - # 若误用指数 90000/80000 会得到 700 + # buy_vwap=2000, sell_vwap=2000; 初始权利金按买 VWAP=20 + # U=(0.01+0.01)*2000 - 20 = 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=90000.0, + open_index_px=2000.0, close_index_px=80000.0, + initial_premium_usdt=20.0, premium_ccy="BTC", conn=conn, bridge_id=bid, @@ -120,6 +120,37 @@ class SpotVwapBridgeTests(unittest.TestCase): self.assertIsNone(pnl2) conn.close() + def test_closed_bridge_without_sell_vwap_is_missing(self): + """已关桥但无 close_sell 时不得用指数冒充卖回价.""" + from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade + from lib.options.options_spot_bridge_lib import ( + BRIDGE_CLOSED, + add_bridge_buy_fill, + ensure_bridge_table, + insert_bridge, + update_bridge, + ) + + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + ensure_bridge_table(conn) + bid = insert_bridge(conn, underlying="BTC", status="holding", budget_usdt=1000) + add_bridge_buy_fill(conn, bid, usdt_spent=1000.0, coin_bought=0.01) + update_bridge(conn, bid, status=BRIDGE_CLOSED, close=True) + pnl, src = resolve_true_pnl_usdt_from_trade( + premium_paid=0.01, + 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) + conn.close() + def test_buy_vwap_ignores_shrunk_coin_bought(self): """开仓后 coin_bought 被改成权利金量时,不得用 budget/缩水量当买价.""" from lib.options.options_spot_bridge_lib import (