diff --git a/docs/系统分析-期权主链条.md b/docs/系统分析-期权主链条.md index 0749fbc..8e39712 100644 --- a/docs/系统分析-期权主链条.md +++ b/docs/系统分析-期权主链条.md @@ -32,8 +32,8 @@ USDT(交易户) ▼ 卖光可卖标的币(禁止按开仓权利金封顶)→ 快照 trading_usdt_after ▼ -口径 A(复盘真盈亏): after − before ≡ 卖光收回 − 买币;禁止指数冒充 -口径 B(期权历史/数据统计): 仅期权腿币盈亏(×卖回价折 U);不含现货 +口径 A(复盘真盈亏): 币盈亏×卖回价 − 权利金×(开仓价−卖回价);含权利金U波动,忽略多买 +口径 B(期权历史/数据统计): 仅期权腿币盈亏(×卖回价折 U);不含现货/权利金U波动修正 ``` ### 0.3 启动装配 diff --git a/docs/规格-期权交易账户盈亏结算.md b/docs/规格-期权交易账户盈亏结算.md index 9504b45..1f96910 100644 --- a/docs/规格-期权交易账户盈亏结算.md +++ b/docs/规格-期权交易账户盈亏结算.md @@ -31,20 +31,20 @@ ## 2. 双口径定义 -### 2.1 口径 A — 真盈亏(交易账户 / 复盘交易记录) +### 2.1 口径 A — 真盈亏(复盘交易记录) ``` -本单真盈亏(U) = trading_usdt_after − trading_usdt_before - = 卖光标的币累计收回 USDT − 开仓买币花费 +真盈亏(U) = 期权历史盈亏(币) × 平仓卖回价 + − (权利金 × 开仓价 − 权利金 × 平仓卖回价) ``` -一次一仓且无中途划转时,账户差分与桥上 `usdt_recovered − budget_usdt` 等价。 -**禁止**用权利金×指数(期权腿 / 口径 B)冒充实盈亏。 +等价(无手续费时):`收回×卖回价 − 已付×开仓价`。 +含义:**计入权利金的 USDT 波动**;**忽略现货多买缓冲**。 +有开/平手续费时:开仓费按开仓价、平仓费按卖回价折 U 后扣除。 -- **消费者**:期权复盘「期权交易记录」盈亏列、复盘 KPI、账户冻结(`pnl_settled`) -- **含**:多买缓冲、现货滑点、期权/现货摩擦(整笔钱进出) -- **门禁**:全部期权仓位已平 **且** 标的币无可卖量后才拍 `after` 并冻结 -- 未结算:列表显示「待结算」,不写币×价假 U +- **消费者**:期权复盘「期权交易记录」盈亏列、复盘 KPI、`realized_pnl_usdt` 冻结 +- **不含**:现货多买 / 账户 after−before / 桥 `usdt_recovered−budget` +- 账户 `trading_usdt_before/after` 仅审计用,**不**作复盘真盈亏 ### 2.2 口径 B — 期权历史 / 数据统计(仅期权腿) @@ -54,8 +54,8 @@ ``` - **消费者**:期权面板「期权历史」(币)、数据统计 / 顶栏窗口统计(折 U) -- **不含**现货买币/卖回;**不**用现货贵贱改写权利金腿 -- 与复盘真盈亏 A **独立**,禁止混用 +- **不含**现货;**不**做「开仓价 vs 卖回价」权利金 USDT 波动修正(那是口径 A) +- 与复盘真盈亏 A **独立**:`A = B − 权利金×(开仓价−卖回价)` ### 2.3 初始权利金、盈利率、期权手续费 @@ -270,3 +270,4 @@ IDLE | 2026-10-13 | 复盘「期权交易记录」盈亏与数据统计同口径 B;增加仅期权手续费展示 | | 2026-10-13 | 交易记录改回真盈亏 A;期权历史/统计仅期权腿 B,不含现货 | | 2026-10-13 | 真盈亏明确为卖光收回−买币;复盘禁止指数折 U 冒充;桥可回退 | +| 2026-10-13 | 真盈亏改回权利金 USDT 波动式:币盈亏×卖回价−权利金×(开−平);忽略多买 | diff --git a/lib/options/options_account_pnl_lib.py b/lib/options/options_account_pnl_lib.py index e6caf34..a8026a3 100644 --- a/lib/options/options_account_pnl_lib.py +++ b/lib/options/options_account_pnl_lib.py @@ -236,68 +236,6 @@ 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, *, @@ -305,15 +243,17 @@ def settle_closed_trades_from_trading_account( bridge_id: int | None = None, trade_id: int | None = None, ) -> int: - """卖回后:对本桥/本单未结算已平仓位冻结真盈亏. + """卖回后:拍 after 快照,并冻结复盘真盈亏(权利金 USDT 波动口径). - 优先 after−before;账户 after 缺失时用桥「卖回−买币」. - 已 pnl_settled=1 的行永不改写. + realized_pnl_usdt = 历史币盈亏×卖回价 − (权利金×开仓价 − 权利金×卖回价); + 不含现货多买.账户 after 仅审计.已 pnl_settled=1 的行永不改写. """ + from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade + after = _safe_float(trading_usdt_after) if after is not None: after = round(float(after), 8) - clauses = ["status='closed'", "COALESCE(pnl_settled, 0)=0", "trading_usdt_before IS NOT NULL"] + clauses = ["status='closed'", "COALESCE(pnl_settled, 0)=0"] params: list[Any] = [] if trade_id is not None: clauses.append("id=?") @@ -325,7 +265,8 @@ def settle_closed_trades_from_trading_account( return 0 rows = conn.execute( f""" - SELECT id, trading_usdt_before, realized_pnl_usdt, bridge_id + 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 FROM options_trades WHERE {" AND ".join(clauses)} """, @@ -334,30 +275,30 @@ def settle_closed_trades_from_trading_account( 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 - 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 + before = _safe_float( + row["trading_usdt_before"] if hasattr(row, "keys") else row[1] + ) 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) + if after_use is None and before is not None: + # 无 after 读数时不编造;仍可用权利金公式冻结 + after_use = None + pnl_u, src = resolve_true_pnl_usdt_from_trade( + premium_paid=row["premium_paid"] if hasattr(row, "keys") else row[2], + premium_received=row["premium_received"] if hasattr(row, "keys") else row[3], + realized_pnl=row["realized_pnl"] if hasattr(row, "keys") else row[4], + open_index_px=row["open_index_px"] if hasattr(row, "keys") else row[5], + close_index_px=row["close_index_px"] if hasattr(row, "keys") else row[6], + 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], + fee_ccy=row["fee_ccy"] if hasattr(row, "keys") else row[10], + ) + if pnl_u is None or src == "none": + continue conn.execute( """ UPDATE options_trades - SET trading_usdt_after = ?, + SET trading_usdt_after = COALESCE(?, trading_usdt_after), realized_pnl_usdt = ?, pnl_settled = 1 WHERE id = ? AND COALESCE(pnl_settled, 0)=0 diff --git a/lib/options/options_close_exec_lib.py b/lib/options/options_close_exec_lib.py index b97b1f2..2f87a27 100644 --- a/lib/options/options_close_exec_lib.py +++ b/lib/options/options_close_exec_lib.py @@ -510,9 +510,8 @@ def close_option_by_bid1( fee_ccy=str(row["fee_ccy"] or row["premium_ccy"] or row_ccy or ""), ) pnl = settled.get("realized_pnl") - # 有交易账户 before 快照:U 盈亏等卖回后结算,平仓不写 realized_pnl_usdt - has_acct_snap = _safe_float(row["trading_usdt_before"]) is not None - pnl_usdt = None if has_acct_snap else settled.get("realized_pnl_usdt") + # 复盘真盈亏=权利金 USDT 波动口径;账户 after 另在卖回后拍快照 + 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 diff --git a/lib/options/options_pnl_settle_lib.py b/lib/options/options_pnl_settle_lib.py index f6a12b8..edda931 100644 --- a/lib/options/options_pnl_settle_lib.py +++ b/lib/options/options_pnl_settle_lib.py @@ -174,6 +174,101 @@ def fee_to_usdt( return round(amt, 4) +def true_pnl_usdt_premium_move( + *, + coin_pnl: Any, + premium_paid: Any, + open_index_px: Any, + sell_back_px: Any, +) -> float | None: + """复盘真盈亏(U):计入权利金 USDT 波动,忽略现货多买. + + 真盈亏 = 期权历史盈亏(币)×平仓卖回价 + − (权利金×开仓价 − 权利金×平仓卖回价) + = 历史币盈亏×卖回价 − 权利金×(开仓价 − 卖回价) + + 无手续费时等价于: 收回×卖回价 − 已付×开仓价. + 币盈亏须已含期权开/平手续费(与期权历史同口径). + """ + 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: + return None + prem_move = float(paid) * float(open_px) - float(paid) * float(close_px) + return round(float(pnl) * float(close_px) - prem_move, 4) + + +def resolve_true_pnl_usdt_from_trade( + *, + premium_paid: Any = None, + premium_received: Any = None, + realized_pnl: Any = None, + open_index_px: Any = None, + close_index_px: Any = None, + open_fee: Any = 0, + close_fee: Any = 0, + premium_ccy: str | None = None, + fee_ccy: str | None = None, +) -> tuple[float | None, str]: + """由成交字段算复盘真盈亏;不含现货多买/账户差分. + + 返回 (pnl_u, source): premium_move | stable | none + """ + del fee_ccy # 费率折 U 已体现在历史币盈亏里;本函数不重扣 + 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) + + if pc not in ("ETH", "BTC"): + if coin is not None: + return round(float(coin), 4), "stable" + return None, "none" + + open_px = _safe_float(open_index_px) + close_px = _safe_float(close_index_px) + # 优先严格按用户式:历史币盈亏×卖回价 − 权利金×(开仓价−卖回价) + usdt = true_pnl_usdt_premium_move( + coin_pnl=coin, + premium_paid=paid, + open_index_px=open_px, + sell_back_px=close_px, + ) + 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" + + def settle_sim_coin_pnl_usdt( *, premium_paid: Any, @@ -185,11 +280,10 @@ def settle_sim_coin_pnl_usdt( fee_ccy: str | None = None, premium_ccy: str = "ETH", ) -> dict[str, float | None]: - """模拟盘币本位盈亏(U). + """币本位真盈亏(U):含权利金 USDT 波动,不含现货多买. - 盈利: (权利金btc+盈利btc)×卖回价 − 初始U权利金 − 手续费 - = 收回币×卖回价 − 已付币×开仓指数 − 手续费U - 亏损: 剩余权利金U − 手续费U − 初始U权利金(同一式,结果为负) + = 历史币盈亏×卖回价 − (权利金×开仓价 − 权利金×卖回价) + = 收回币×卖回价 − 已付币×开仓指数 − 手续费U 不用买币预算当成本(预算含缓冲). """ diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py index ac6236e..3d6d320 100644 --- a/lib/options/options_review_lib.py +++ b/lib/options/options_review_lib.py @@ -429,7 +429,8 @@ def sync_options_from_local_trades( ) -> dict[str, Any]: """从本地 options_trades 已平仓记录导入复盘快照(不访问交易所). - 真盈亏=卖光收回−买币(after−before / 已关桥);禁止权利金×指数冒充. + 真盈亏=历史币盈亏×卖回价 − (权利金×开仓价 − 权利金×卖回价); + 计入权利金 USDT 波动,忽略现货多买. """ init_options_review_tables(conn) from lib.options.options_db import init_options_tables @@ -555,13 +556,8 @@ def sync_options_from_local_trades( 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 if close_idx is not None and close_idx > 0: idx = close_idx @@ -570,26 +566,36 @@ def sync_options_from_local_trades( else: idx = idx_cache.get(underly) prem = _safe_float(r["premium_paid"]) - # 真盈亏=卖光收回−买币(账户 after−before 或已关桥);禁止权利金×指数冒充 - from lib.options.options_account_pnl_lib import resolve_true_pnl_usdt - - pnl_u, pnl_src = resolve_true_pnl_usdt( + # 仅期权开/平手续费折 U(不含现货);先解析再算真盈亏 + ofee = abs(_safe_float(r["open_fee"]) or 0.0) + cfee = abs(_safe_float(r["close_fee"]) or 0.0) + ofee, cfee = _resolve_option_fees_coin( 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, + r, + open_fee=ofee, + close_fee=cfee, + premium_ccy=ccy, + ) + 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 idx + # 真盈亏=币盈亏×卖回价 − (权利金×开仓价 − 权利金×卖回价);忽略现货多买 + from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade + + 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_for_prem, + close_index_px=sell_idx, + open_fee=ofee, + close_fee=cfee, + premium_ccy=ccy, + fee_ccy=str(r["premium_ccy"] or ccy or ""), ) - # 快照能算真盈亏但冻结值偏离:纠偏写回(仅纠正错账,不改口径) - 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) + pnl_u is not None + and pnl_src != "none" + and (frozen_usdt is None or abs(float(frozen_usdt) - float(pnl_u)) > 0.05) ): try: conn.execute( @@ -600,49 +606,15 @@ def sync_options_from_local_trades( """, (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 prem_u = stored_initial if prem_u is None: 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 - # 仅期权开/平手续费折 U(不含现货) - ofee = abs(_safe_float(r["open_fee"]) or 0.0) - cfee = abs(_safe_float(r["close_fee"]) or 0.0) - # 模拟盘旧单曾按「权利金×费率」计费偏小:按 OKX 公式/订单回填 - ofee, cfee = _resolve_option_fees_coin( - conn, - r, - open_fee=ofee, - close_fee=cfee, - premium_ccy=ccy, - ) fee_u = None if ccy in ("ETH", "BTC"): fee_parts = 0.0 @@ -650,7 +622,6 @@ def sync_options_from_local_trades( if ofee > 0 and open_idx_for_prem is not None and open_idx_for_prem > 0: fee_parts += ofee * float(open_idx_for_prem) has_fee = True - sell_idx = close_idx if (close_idx is not None and close_idx > 0) else idx if cfee > 0 and sell_idx is not None and sell_idx > 0: fee_parts += cfee * float(sell_idx) has_fee = True @@ -683,7 +654,7 @@ def sync_options_from_local_trades( "option_fee_usdt": fee_u, "created_at": opened_at, "closed_at": closed_at, - "status_label": "待结算" if acct_pending else "已平", + "status_label": "已平", }, ) if action == "inserted": diff --git a/lib/options/templates/options_review_panel.html b/lib/options/templates/options_review_panel.html index 8af16e3..797c364 100644 --- a/lib/options/templates/options_review_panel.html +++ b/lib/options/templates/options_review_panel.html @@ -229,7 +229,7 @@
期权交易记录
-

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

+

真盈亏=历史币盈亏×卖回价 − 权利金×(开仓价−卖回价);计入权利金 USDT 波动,忽略现货多买。期权历史仅记币收益。

@@ -239,7 +239,7 @@ 类型 标的/合约 初始权利金 - 真盈亏 + 真盈亏 盈利率 手续费 开仓时间 diff --git a/tests/test_options_account_pnl.py b/tests/test_options_account_pnl.py index 2089137..63a6781 100644 --- a/tests/test_options_account_pnl.py +++ b/tests/test_options_account_pnl.py @@ -6,7 +6,7 @@ import unittest class AccountPnlSettleTests(unittest.TestCase): - def test_settle_after_minus_before(self): + def test_settle_freezes_premium_usdt_move_not_account_diff(self): from lib.options.options_account_pnl_lib import ( settle_closed_trades_from_trading_account, ) @@ -15,16 +15,19 @@ 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 但不采用 conn.execute( """ INSERT INTO options_trades( inst_id, underlying, opt_type, sheets, eth_amount, - premium_paid, status, bridge_id, trading_usdt_before, pnl_settled, - created_at, closed_at + premium_paid, premium_received, realized_pnl, premium_ccy, + open_index_px, close_index_px, status, bridge_id, + trading_usdt_before, pnl_settled, created_at, closed_at ) VALUES ( 'BTC-USD-261008-86500-P', 'BTC', 'P', 1, 0.01, - 0.01, 'closed', 7, 1000.0, 0, - '2026-10-05 05:11:30', '2026-10-07 10:03:53' + 0.01, 0.02, 0.01, 'BTC', + 90000.0, 80000.0, 'closed', 7, + 1000.0, 0, '2026-10-05 05:11:30', '2026-10-07 10:03:53' ) """ ) @@ -37,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"]), 947.05, places=2) + self.assertAlmostEqual(float(row["realized_pnl_usdt"]), 700.0, places=2) self.assertEqual(int(row["pnl_settled"]), 1) # 已冻结不再改 @@ -48,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"]), 947.05, places=2) + self.assertAlmostEqual(float(row2["realized_pnl_usdt"]), 700.0, places=2) conn.close() def test_stats_use_coin_times_sell_px_not_account(self): @@ -149,48 +152,35 @@ 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 + def test_true_pnl_premium_move_helper(self): + from lib.options.options_pnl_settle_lib import ( + resolve_true_pnl_usdt_from_trade, + true_pnl_usdt_premium_move, + ) - 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) - """ + # A = 0.01*80000 - (0.01*90000 - 0.01*80000) = 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, + ) + ), + 700.0, + places=4, ) - conn.commit() - pnl, src = resolve_true_pnl_usdt( - conn, - trading_usdt_before=500.0, - trading_usdt_after=580.0, - bridge_id=3, + pnl, src = resolve_true_pnl_usdt_from_trade( + premium_paid=0.01, + premium_received=0.02, + realized_pnl=0.01, + open_index_px=90000, + close_index_px=80000, + premium_ccy="BTC", ) - 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() + self.assertEqual(src, "premium_move") + self.assertAlmostEqual(float(pnl), 700.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 9d0a82c..ef944b1 100644 --- a/tests/test_options_pnl_settle.py +++ b/tests/test_options_pnl_settle.py @@ -7,6 +7,7 @@ from lib.options.options_pnl_settle_lib import ( fee_to_premium_ccy, settle_closed_pnl, settle_exchange_realized_pnl, + true_pnl_usdt_premium_move, ) from lib.options.options_stats_lib import ( apply_exchange_pnl_freeze, @@ -24,6 +25,26 @@ class FeeConvertTests(unittest.TestCase): ) +class PremiumMoveFormulaTests(unittest.TestCase): + def test_user_formula_matches_proceeds_minus_open_cost(self): + coin = 0.01 + paid = 0.01 + open_px = 90000.0 + close_px = 80000.0 + a = true_pnl_usdt_premium_move( + coin_pnl=coin, + premium_paid=paid, + open_index_px=open_px, + sell_back_px=close_px, + ) + # 历史盈亏×卖回 − (权利金×开 − 权利金×平) + expect = coin * close_px - (paid * open_px - paid * close_px) + 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) + + class SettleClosedPnlTests(unittest.TestCase): def test_sim_net_times_close_index(self): # 无开仓指数时回退净币×平仓价 diff --git a/tests/test_options_review_pnl_freeze.py b/tests/test_options_review_pnl_freeze.py index 27f3b72..7960759 100644 --- a/tests/test_options_review_pnl_freeze.py +++ b/tests/test_options_review_pnl_freeze.py @@ -1,4 +1,4 @@ -"""复盘真盈亏=卖光收回−买币;禁止权利金×指数冒充.""" +"""复盘真盈亏=币盈亏×卖回价 − 权利金×(开仓价−卖回价).""" from __future__ import annotations import sqlite3 @@ -6,7 +6,7 @@ import unittest class ReviewLocalSyncFreezeTests(unittest.TestCase): - def test_account_after_minus_before_is_true_pnl(self): + def test_premium_usdt_move_formula(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 @@ -17,77 +17,65 @@ 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( inst_id, underlying, opt_type, sheets, eth_amount, premium_paid, premium_received, realized_pnl, premium_ccy, - status, realized_pnl_usdt, close_index_px, + open_fee, close_fee, status, realized_pnl_usdt, + open_index_px, 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, + 0, 0, 'closed', -51.0, + 90000.0, 80000.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()) + out = sync_options_from_local_trades(conn, None) 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) + self.assertAlmostEqual(float(row["realized_pnl_total"]), 700.0, places=2) + # 纠偏写回,不用账户差分 1506 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) + self.assertAlmostEqual(float(healed["realized_pnl_usdt"]), 700.0, places=2) conn.close() - def test_bridge_cash_is_true_pnl_when_no_after(self): + def test_same_open_close_px_equals_coin_times_close(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, + open_fee, close_fee, status, open_index_px, 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', 850.0, 85000.0, - 1000.0, 7, 0, + 0, 0, 'closed', 85000.0, 85000.0, '2026-10-05 05:11:30', '2026-10-07 10:03:53' ) """ @@ -98,44 +86,8 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase): "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 - 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', 850.0, 85000.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, status_raw FROM options_review_trades WHERE history_key=?", - ("local_opt:1",), - ).fetchone() - # 无账户/桥快照:不得用指数折 U 冒充真盈亏 - self.assertIsNone(row["realized_pnl_total"]) + # 开平价相同:波动项为0 → 等于币×卖回价=850 + self.assertAlmostEqual(float(row["realized_pnl_total"]), 850.0, places=2) conn.close()