diff --git a/docs/系统分析-期权主链条.md b/docs/系统分析-期权主链条.md index 8e39712..b67c8dc 100644 --- a/docs/系统分析-期权主链条.md +++ b/docs/系统分析-期权主链条.md @@ -32,7 +32,7 @@ USDT(交易户) ▼ 卖光可卖标的币(禁止按开仓权利金封顶)→ 快照 trading_usdt_after ▼ -口径 A(复盘真盈亏): 币盈亏×卖回价 − 权利金×(开仓价−卖回价);含权利金U波动,忽略多买 +口径 A(复盘真盈亏): 币盈亏×卖回VWAP − 权利金×(买VWAP−卖回VWAP);卖完再结算 口径 B(期权历史/数据统计): 仅期权腿币盈亏(×卖回价折 U);不含现货/权利金U波动修正 ``` diff --git a/docs/规格-期权交易账户盈亏结算.md b/docs/规格-期权交易账户盈亏结算.md index 1f96910..0ed808e 100644 --- a/docs/规格-期权交易账户盈亏结算.md +++ b/docs/规格-期权交易账户盈亏结算.md @@ -42,8 +42,18 @@ 含义:**计入权利金的 USDT 波动**;**忽略现货多买缓冲**。 有开/平手续费时:开仓费按开仓价、平仓费按卖回价折 U 后扣除。 +**多笔成交(VWAP):** + +| 价格 | 定义 | +|------|------| +| 开仓价 | 买币多笔加权:`Σ买币花费USDT / Σ买入币量`(缓冲买单计入均价,公式仍只乘 `premium_paid`) | +| 卖回价 | 平仓后卖回多笔加权:`Σ收回USDT / Σ卖出币量`(**不含**开仓多余卖回) | + +- 卖回未完成(仍有可卖)→ 复盘标「待卖回」,不冻结真盈亏 +- 无 VWAP 时回退冻结指数;仍缺则「缺价未核」 + - **消费者**:期权复盘「期权交易记录」盈亏列、复盘 KPI、`realized_pnl_usdt` 冻结 -- **不含**:现货多买 / 账户 after−before / 桥 `usdt_recovered−budget` +- **不含**:现货多买差分当盈亏 / 账户 after−before - 账户 `trading_usdt_before/after` 仅审计用,**不**作复盘真盈亏 ### 2.2 口径 B — 期权历史 / 数据统计(仅期权腿) @@ -271,3 +281,4 @@ IDLE | 2026-10-13 | 交易记录改回真盈亏 A;期权历史/统计仅期权腿 B,不含现货 | | 2026-10-13 | 真盈亏明确为卖光收回−买币;复盘禁止指数折 U 冒充;桥可回退 | | 2026-10-13 | 真盈亏改回权利金 USDT 波动式:币盈亏×卖回价−权利金×(开−平);忽略多买 | +| 2026-10-13 | 多笔买/卖回用 VWAP;卖完再结算;待卖回/缺价未核标记 | diff --git a/lib/common/static/options_review.js b/lib/common/static/options_review.js index fb67169..a28189a 100644 --- a/lib/common/static/options_review.js +++ b/lib/common/static/options_review.js @@ -451,11 +451,17 @@ var badgeExtra = reviewed ? ' 已复盘' : ""; - var needsMark = String(t.status_raw || "") === "缺价未核"; + var stRaw = String(t.status_raw || ""); + var needsMark = stRaw === "缺价未核"; + var pendingSell = stRaw === "待卖回"; if (needsMark) { badgeExtra += ' 缺价未核'; } + if (pendingSell) { + badgeExtra += + ' 待卖回'; + } return ( '' + - (needsMark ? '—' : fmtPnl(t.realized_pnl_total)) + + (needsMark || pendingSell + ? '—' + : fmtPnl(t.realized_pnl_total)) + "" + ' tuple[float | None, float | None, str]: + """开仓价/卖回价:优先桥上多笔 VWAP,否则冻结指数. + + 返回 (open_px, close_px, gate): + gate='' 可算; 'pending_sell' 卖回未完成; 'missing' 缺价 + """ + open_px = _safe_float(open_index_px) + close_px = _safe_float(close_index_px) + bid = None + try: + if bridge_id is not None and str(bridge_id).strip() != "": + bid = int(bridge_id) + except (TypeError, ValueError): + bid = None + if conn is not None and bid is not None: + try: + from lib.options.options_spot_bridge_lib import ( + BRIDGE_CLOSED, + BRIDGE_PENDING_SELL, + bridge_buy_vwap, + bridge_sell_vwap, + ensure_bridge_table, + ) + + 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 "" + ) + if st == BRIDGE_PENDING_SELL: + return open_px, None, "pending_sell" + sv = bridge_sell_vwap(conn, bid) + if sv is not None and sv > 0: + close_px = sv + elif st == BRIDGE_CLOSED and (close_px is None or close_px <= 0): + # 已关桥但无 VWAP/冻结卖回价 + 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: + return open_px, close_px, "missing" + return open_px, close_px, "" + + def resolve_true_pnl_usdt_from_trade( *, premium_paid: Any = None, @@ -213,10 +269,12 @@ def resolve_true_pnl_usdt_from_trade( close_fee: Any = 0, premium_ccy: str | None = None, fee_ccy: str | None = None, + conn: Any = None, + bridge_id: Any = None, ) -> tuple[float | None, str]: - """由成交字段算复盘真盈亏;不含现货多买/账户差分. + """由成交字段算复盘真盈亏;开/卖回价优先多笔 VWAP. - 返回 (pnl_u, source): premium_move | stable | none + 返回 (pnl_u, source): premium_move | stable | pending_sell | none """ del fee_ccy # 费率折 U 已体现在历史币盈亏里;本函数不重扣 pc = str(premium_ccy or "").strip().upper() @@ -233,8 +291,14 @@ def resolve_true_pnl_usdt_from_trade( return round(float(coin), 4), "stable" return None, "none" - open_px = _safe_float(open_index_px) - close_px = _safe_float(close_index_px) + 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" # 优先严格按用户式:历史币盈亏×卖回价 − 权利金×(开仓价−卖回价) usdt = true_pnl_usdt_premium_move( coin_pnl=coin, diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py index 828e09a..8c99f77 100644 --- a/lib/options/options_review_lib.py +++ b/lib/options/options_review_lib.py @@ -591,19 +591,35 @@ def sync_options_from_local_trades( close_fee=cfee, premium_ccy=ccy, fee_ccy=str(r["premium_ccy"] or ccy or ""), + conn=conn, + bridge_id=r["bridge_id"], ) - # 能算新公式则写回;算不出则清空旧冻结并标记「缺价未核」 + # 能算新公式则写回;卖回未完成→待卖回;算不出→缺价未核 status_label = "已平" - if pnl_u is not None and pnl_src != "none": + 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 frozen_usdt is None or abs(float(frozen_usdt) - float(pnl_u)) > 0.05: try: + from lib.options.options_pnl_settle_lib import resolve_spot_vwap_prices + + 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 = ?, pnl_settled = 1 + 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, trade_id), + (pnl_u, opx, cpx, trade_id), ) except Exception: pass diff --git a/lib/options/options_spot_bridge_lib.py b/lib/options/options_spot_bridge_lib.py index 045a35b..802dcf1 100644 --- a/lib/options/options_spot_bridge_lib.py +++ b/lib/options/options_spot_bridge_lib.py @@ -96,6 +96,153 @@ def ensure_bridge_table(conn: sqlite3.Connection) -> None: ON options_spot_bridge(status) """ ) + # 多笔买卖 VWAP:买侧累加(含缓冲);平仓后卖回单独累加(不含开仓多余卖回) + for ddl in ( + "ALTER TABLE options_spot_bridge ADD COLUMN buy_usdt_spent REAL", + "ALTER TABLE options_spot_bridge ADD COLUMN buy_coin_accum REAL", + "ALTER TABLE options_spot_bridge ADD COLUMN close_sell_coin REAL", + "ALTER TABLE options_spot_bridge ADD COLUMN close_sell_usdt REAL", + ): + try: + conn.execute(ddl) + except Exception: + pass + + +def add_bridge_buy_fill( + conn: sqlite3.Connection, + bridge_id: int, + *, + usdt_spent: float | None, + coin_bought: float | None, +) -> None: + """累加开仓买币成交,供买侧 VWAP=ΣUSDT/Σ币.""" + ensure_bridge_table(conn) + u = _safe_float(usdt_spent) + c = _safe_float(coin_bought) + if bridge_id is None or u is None or c is None or u <= 0 or c <= 0: + return + row = conn.execute( + "SELECT buy_usdt_spent, buy_coin_accum FROM options_spot_bridge WHERE id=?", + (int(bridge_id),), + ).fetchone() + if not row: + return + prev_u = _safe_float(row["buy_usdt_spent"] if hasattr(row, "keys") else row[0]) or 0.0 + prev_c = _safe_float(row["buy_coin_accum"] if hasattr(row, "keys") else row[1]) or 0.0 + conn.execute( + """ + UPDATE options_spot_bridge + SET buy_usdt_spent = ?, buy_coin_accum = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + """, + (round(prev_u + float(u), 8), round(prev_c + float(c), 8), int(bridge_id)), + ) + + +def add_bridge_close_sell_fill( + conn: sqlite3.Connection, + bridge_id: int, + *, + usdt_recovered: float | None, + coin_sold: float | None, +) -> None: + """累加平仓后卖回成交(多笔),供卖回 VWAP=ΣUSDT/Σ币;同时累加 usdt_recovered.""" + ensure_bridge_table(conn) + u = _safe_float(usdt_recovered) + c = _safe_float(coin_sold) + if bridge_id is None: + return + if u is not None and u > 0: + add_bridge_usdt_recovered(conn, int(bridge_id), u) + if (u is None or u <= 0) and (c is None or c <= 0): + return + row = conn.execute( + """ + SELECT close_sell_usdt, close_sell_coin, coin_sold + FROM options_spot_bridge WHERE id=? + """, + (int(bridge_id),), + ).fetchone() + if not row: + return + prev_u = _safe_float(row["close_sell_usdt"] if hasattr(row, "keys") else row[0]) or 0.0 + prev_c = _safe_float(row["close_sell_coin"] if hasattr(row, "keys") else row[1]) or 0.0 + prev_sold = _safe_float(row["coin_sold"] if hasattr(row, "keys") else row[2]) or 0.0 + new_u = prev_u + (float(u) if u is not None and u > 0 else 0.0) + new_c = prev_c + (float(c) if c is not None and c > 0 else 0.0) + new_sold = prev_sold + (float(c) if c is not None and c > 0 else 0.0) + conn.execute( + """ + UPDATE options_spot_bridge + SET close_sell_usdt = ?, close_sell_coin = ?, coin_sold = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + """, + (round(new_u, 8), round(new_c, 8), round(new_sold, 8), int(bridge_id)), + ) + + +def bridge_buy_vwap(conn: sqlite3.Connection, bridge_id: int | None) -> float | None: + """开仓买币 VWAP(USDT/币);多笔加权.忽略后续把 coin_bought 改成权利金量.""" + if bridge_id is None: + return None + ensure_bridge_table(conn) + row = conn.execute( + "SELECT buy_usdt_spent, buy_coin_accum, budget_usdt, coin_bought FROM options_spot_bridge WHERE id=?", + (int(bridge_id),), + ).fetchone() + if not row: + return None + spent = _safe_float(row["buy_usdt_spent"] if hasattr(row, "keys") else row[0]) + coins = _safe_float(row["buy_coin_accum"] if hasattr(row, "keys") else row[1]) + if spent is not None and coins is not None and spent > 0 and coins > 0: + return round(float(spent) / float(coins), 8) + # 旧桥无累加字段:用预算/买入量近似单笔 + budget = _safe_float(row["budget_usdt"] if hasattr(row, "keys") else row[2]) + bought = _safe_float(row["coin_bought"] if hasattr(row, "keys") else row[3]) + if budget is not None and bought is not None and budget > 0 and bought > 0: + return round(float(budget) / float(bought), 8) + return None + + +def bridge_sell_vwap(conn: sqlite3.Connection, bridge_id: int | None) -> float | None: + """平仓后卖回 VWAP(USDT/币);仅 close_sell_* 多笔加权,不含开仓多余卖回.""" + if bridge_id is None: + return None + ensure_bridge_table(conn) + row = conn.execute( + "SELECT close_sell_usdt, close_sell_coin, usdt_recovered, coin_sold, status FROM options_spot_bridge WHERE id=?", + (int(bridge_id),), + ).fetchone() + if not row: + return None + u = _safe_float(row["close_sell_usdt"] if hasattr(row, "keys") else row[0]) + c = _safe_float(row["close_sell_coin"] if hasattr(row, "keys") else row[1]) + if u is not None and c is not None and u > 0 and c > 0: + return round(float(u) / float(c), 8) + # 旧桥无分拆字段且已关桥:用总收回/总卖出近似(可能含开仓多余卖回) + st = str(row["status"] if hasattr(row, "keys") else row[4] or "") + if st == BRIDGE_CLOSED: + ru = _safe_float(row["usdt_recovered"] if hasattr(row, "keys") else row[2]) + sc = _safe_float(row["coin_sold"] if hasattr(row, "keys") else row[3]) + if ru is not None and sc is not None and ru > 0 and sc > 0: + return round(float(ru) / float(sc), 8) + return None + + +def bridge_sell_complete(conn: sqlite3.Connection, bridge_id: int | None) -> bool: + """平仓卖回是否已完成(桥已关闭).""" + if bridge_id is None: + return False + ensure_bridge_table(conn) + row = conn.execute( + "SELECT status FROM options_spot_bridge WHERE id=?", + (int(bridge_id),), + ).fetchone() + if not row: + return False + return str(row["status"] if hasattr(row, "keys") else row[0] or "") == BRIDGE_CLOSED def list_open_bridges(conn: sqlite3.Connection) -> list[dict[str, Any]]: @@ -546,11 +693,37 @@ def sell_residual_after_option_flat( # 一次一仓空仓后卖光可用(含盈利币);禁止用 coin_bought/权利金封顶 # 此处无业务连接:模拟盘 convert 可安全另开写钱包 + usdt_before_sell = None + try: + from lib.options.options_account_pnl_lib import fetch_trading_usdt + + usdt_before_sell = fetch_trading_usdt({"fetch_options_balances": None}, ex) + except Exception: + usdt_before_sell = None sell = spot_market_sell_coin_to_usdt( ex, underlying=underlying, coin_amount=None, ) + # 实盘订单回报常无 usdt_recovered:用交易账户 USDT 增量补 + if sell.get("ok") and _safe_float(sell.get("usdt_recovered")) is None: + try: + from lib.exchange.okx_options_lib import invalidate_options_balance_cache + from lib.options.options_account_pnl_lib import fetch_trading_usdt + + invalidate_options_balance_cache() + usdt_after_sell = fetch_trading_usdt({"fetch_options_balances": None}, ex) + if ( + usdt_before_sell is not None + and usdt_after_sell is not None + and float(usdt_after_sell) > float(usdt_before_sell) + ): + sell = dict(sell) + sell["usdt_recovered"] = round( + float(usdt_after_sell) - float(usdt_before_sell), 8 + ) + except Exception: + pass if bid is None: if not sell.get("ok"): msg = str(sell.get("msg") or "") @@ -609,8 +782,14 @@ def sell_residual_after_option_flat( "sell_complete": False, } recovered = _safe_float(sell.get("usdt_recovered")) - if recovered is not None and recovered > 0: - add_bridge_usdt_recovered(c, bid, recovered) + sold_amt = _safe_float(sell.get("coin_sold")) + # 平仓后卖回(可多笔):累加 close_sell_* 供卖回 VWAP;开仓多余卖回不走这里 + add_bridge_close_sell_fill( + c, + bid, + usdt_recovered=recovered, + coin_sold=sold_amt, + ) # 卖后仍有可卖 → 不关桥、不结算(盈利币未卖回时曾误冻亏损) try: left = fetch_trading_coin_available(ex, underlying) @@ -623,7 +802,6 @@ def sell_residual_after_option_flat( bid, status=BRIDGE_PENDING_SELL, sell_ord_id=str(sell.get("ord_id") or ""), - coin_sold=_safe_float(sell.get("coin_sold")), inst_id=inst_id, message=left_msg, ) @@ -641,7 +819,6 @@ def sell_residual_after_option_flat( bid, status=BRIDGE_CLOSED, sell_ord_id=str(sell.get("ord_id") or ""), - coin_sold=_safe_float(sell.get("coin_sold")), inst_id=inst_id, message="期权已平,币已卖回 USDT", close=True, @@ -652,6 +829,7 @@ def sell_residual_after_option_flat( "bridge_id": bid, "bridge_status": BRIDGE_CLOSED, "sell_complete": True, + "sell_vwap": bridge_sell_vwap(c, bid), } if get_db is not None: diff --git a/lib/options/templates/options_review_panel.html b/lib/options/templates/options_review_panel.html index 18a8348..8b45ac2 100644 --- a/lib/options/templates/options_review_panel.html +++ b/lib/options/templates/options_review_panel.html @@ -229,7 +229,7 @@
期权交易记录
-

真盈亏=历史币盈亏×卖回价 − 权利金×(开仓价−卖回价)。缺开仓/卖回价的旧单标记「缺价未核」,不沿用旧冻结。期权历史仅记币收益。

+

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

diff --git a/scripts/repair_options_dual_pnl.py b/scripts/repair_options_dual_pnl.py index 97d1844..681acb9 100644 --- a/scripts/repair_options_dual_pnl.py +++ b/scripts/repair_options_dual_pnl.py @@ -91,8 +91,7 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: elif coin_pnl is not None and open_idx is not None and open_idx > 0: stats_u = round(coin_pnl * open_idx, 4) - # 口径 A:权利金 USDT 波动式(忽略现货多买/账户差分) - # 真盈亏必须同时有开仓价与卖回价;缺一则标记缺价未核(不用开仓价冒充卖回价) + # 口径 A:权利金 USDT 波动式;开/卖回价优先桥多笔 VWAP true_a, src = resolve_true_pnl_usdt_from_trade( premium_paid=paid, premium_received=recv, @@ -102,6 +101,8 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: open_fee=r["open_fee"], close_fee=r["close_fee"], premium_ccy=ccy, + conn=conn, + bridge_id=r["bridge_id"], ) action = "ok" @@ -109,7 +110,12 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: new_pnl_settled = 1 if settled else 0 status_label = "已平" - if true_a is not None and src != "none": + if src == "pending_sell": + action = "mark_pending_sell" + new_pnl_settled = 0 + new_settled_u = None + status_label = "待卖回" + elif true_a is not None and src != "none": if settled_u is None or abs(float(settled_u) - float(true_a)) > 0.05: action = "rewrite_A_premium_move" new_settled_u = round(float(true_a), 4) @@ -119,7 +125,6 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: new_settled_u = round(float(true_a), 4) new_pnl_settled = 1 else: - # 缺开仓价/卖回价/币盈亏等:无法按新公式更新 → 标记「缺价未核」 action = "mark_uncomputable_A" new_pnl_settled = 0 new_settled_u = None @@ -158,7 +163,7 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: "UPDATE options_trades SET initial_premium_usdt=? WHERE id=?", (new_initial, tid), ) - if action == "mark_uncomputable_A": + if action in ("mark_uncomputable_A", "mark_pending_sell"): conn.execute( """ UPDATE options_trades diff --git a/tests/test_options_spot_vwap.py b/tests/test_options_spot_vwap.py new file mode 100644 index 0000000..7c78c83 --- /dev/null +++ b/tests/test_options_spot_vwap.py @@ -0,0 +1,110 @@ +"""多笔买/卖回 VWAP 与真盈亏.""" +from __future__ import annotations + +import sqlite3 +import unittest + + +class SpotVwapBridgeTests(unittest.TestCase): + def test_buy_and_close_sell_vwap_multi_fill(self): + from lib.options.options_spot_bridge_lib import ( + BRIDGE_CLOSED, + add_bridge_buy_fill, + add_bridge_close_sell_fill, + bridge_buy_vwap, + bridge_sell_vwap, + 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=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) + conn.close() + + def test_true_pnl_uses_bridge_vwap_not_index(self): + from lib.options.options_db import init_options_tables + 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, + add_bridge_close_sell_fill, + ensure_bridge_table, + insert_bridge, + update_bridge, + ) + + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + init_options_tables(conn) + 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) + 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 + 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, + close_index_px=80000.0, + premium_ccy="BTC", + conn=conn, + bridge_id=bid, + ) + self.assertEqual(src, "premium_move") + self.assertAlmostEqual(float(pnl), 20.0, places=2) + conn.close() + + def test_pending_sell_blocks_true_pnl(self): + from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade + from lib.options.options_spot_bridge_lib import ( + BRIDGE_PENDING_SELL, + add_bridge_buy_fill, + add_bridge_close_sell_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="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, + 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(src, "pending_sell") + self.assertIsNone(pnl) + conn.close() + + +if __name__ == "__main__": + unittest.main()