fix真盈亏收口为币本位×卖回价减初始权利金U,禁指数冒充平仓价。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-13 12:29:33 +08:00
parent d42e1c7edf
commit 839baea375
10 changed files with 171 additions and 83 deletions
+14 -5
View File
@@ -243,9 +243,9 @@ def settle_closed_trades_from_trading_account(
bridge_id: int | None = None, bridge_id: int | None = None,
trade_id: int | None = None, trade_id: int | None = None,
) -> int: ) -> int:
"""卖回后:拍 after 快照,并冻结复盘真盈亏(权利金 USDT 波动口径). """卖回后:拍 after 快照,并冻结复盘真盈亏.
realized_pnl_usdt = 历史币盈亏×卖回价 − (权利金×开仓价 − 权利金×卖回价); realized_pnl_usdt = (权利金+币盈亏)×卖回VWAP − 初始权利金U;
不含现货多买.账户 after 仅审计.已 pnl_settled=1 的行永不改写. 不含现货多买.账户 after 仅审计.已 pnl_settled=1 的行永不改写.
""" """
from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade 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""" f"""
SELECT id, trading_usdt_before, premium_paid, premium_received, realized_pnl, 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, open_index_px, close_index_px, open_fee, close_fee, premium_ccy, fee_ccy,
bridge_id bridge_id, initial_premium_usdt
FROM options_trades FROM options_trades
WHERE {" AND ".join(clauses)} 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] row_bid = row["bridge_id"] if hasattr(row, "keys") else row[11]
if row_bid is None and bridge_id is not None: if row_bid is None and bridge_id is not None:
row_bid = bridge_id 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( open_px, close_px, _gate = resolve_spot_vwap_prices(
conn, conn,
bridge_id=row_bid, 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], realized_pnl=row["realized_pnl"] if hasattr(row, "keys") else row[4],
open_index_px=open_px, open_index_px=open_px,
close_index_px=close_px, close_index_px=close_px,
initial_premium_usdt=initial_u,
open_fee=row["open_fee"] if hasattr(row, "keys") else row[7], open_fee=row["open_fee"] if hasattr(row, "keys") else row[7],
close_fee=row["close_fee"] if hasattr(row, "keys") else row[8], close_fee=row["close_fee"] if hasattr(row, "keys") else row[8],
premium_ccy=row["premium_ccy"] if hasattr(row, "keys") else row[9], 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"): if pnl_u is None or src in ("none", "pending_sell"):
continue 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( conn.execute(
""" """
UPDATE options_trades UPDATE options_trades
@@ -316,10 +324,11 @@ def settle_closed_trades_from_trading_account(
realized_pnl_usdt = ?, realized_pnl_usdt = ?,
open_index_px = COALESCE(open_index_px, ?), open_index_px = COALESCE(open_index_px, ?),
close_index_px = COALESCE(?, close_index_px), close_index_px = COALESCE(?, close_index_px),
initial_premium_usdt = COALESCE(initial_premium_usdt, ?),
pnl_settled = 1 pnl_settled = 1
WHERE id = ? AND COALESCE(pnl_settled, 0)=0 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 updated += 1
return updated return updated
+10 -4
View File
@@ -505,20 +505,26 @@ def close_option_by_bid1(
row_open_idx = float(bv) row_open_idx = float(bv)
except Exception: except Exception:
pass pass
row_pc = str(row["premium_ccy"] or row_ccy or premium_ccy or "").strip().upper()
settled = settle_closed_pnl( settled = settle_closed_pnl(
premium_paid=paid, premium_paid=paid,
premium_received=recv, premium_received=recv,
open_fee=row_ofee, open_fee=row_ofee,
close_fee=row_cfee, 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, index_px=close_idx,
open_index_px=row_open_idx, open_index_px=row_open_idx,
budget_usdt=row_budget, budget_usdt=row_budget,
fee_ccy=str(row["fee_ccy"] or row["premium_ccy"] or row_ccy or ""), fee_ccy=str(row["fee_ccy"] or row["premium_ccy"] or row_ccy or ""),
) )
pnl = settled.get("realized_pnl") pnl = settled.get("realized_pnl")
# 复盘真盈亏=权利金 USDT 波动口径;账户 after 另在卖回后拍快照 # BTC/ETH:币盈亏可落库;真盈亏U与平仓价等卖回VWAP,禁止用指数占位
pnl_usdt = settled.get("realized_pnl_usdt") 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 = "" note_sql = ""
# 占位: close_quote/recv/pnl/ord/closed_at/open_fee/close_fee/fee_ccy/close_idx/open_idx/pnl_u/[note]/id # 占位: 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_ofee,
row_cfee, row_cfee,
str(row["premium_ccy"] or row_ccy or premium_ccy or ""), 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, settled.get("open_index_px") or row_open_idx,
pnl_usdt, pnl_usdt,
] ]
+9 -2
View File
@@ -403,7 +403,14 @@ def sync_open_options_trades(
fee_ccy=fee_ccy, fee_ccy=fee_ccy,
) )
realized_pnl = settled.get("realized_pnl") 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( conn.execute(
""" """
@@ -431,7 +438,7 @@ def sync_open_options_trades(
realized_pnl, realized_pnl,
close_ord_id, close_ord_id,
closed_at, closed_at,
settled.get("close_index_px"), close_px_store,
settled.get("open_index_px"), settled.get("open_index_px"),
pnl_usdt, pnl_usdt,
prem_ccy, prem_ccy,
+52 -36
View File
@@ -178,29 +178,32 @@ def true_pnl_usdt_premium_move(
*, *,
coin_pnl: Any, coin_pnl: Any,
premium_paid: Any, premium_paid: Any,
open_index_px: Any,
sell_back_px: Any, sell_back_px: Any,
initial_premium_usdt: Any = None,
open_index_px: Any = None,
) -> float | None: ) -> float | None:
"""复盘真盈亏(U):计入权利金 USDT 波动;现货多买数量不进公式. """复盘真盈亏(U):币本位×卖回价 − 初始权利金U.
真盈亏 = 期权历史盈亏(币)×平仓卖回价 币本位 = 权利金 + 币盈亏
− (权利金×开仓价 − 权利金×平仓卖回价) 真盈亏U = 币本位 × 卖回VWAP − 初始权利金U
= 历史币盈亏×卖回价 − 权利金×(开仓价 − 卖回价) 初始权利金U 优先用开仓冻结值;缺失时才用 权利金×开仓价 补算.
现货多买数量不进公式(只影响均价).
币量=期权腿(realized_pnl / premium_paid);价=买卖 VWAP.
无手续费时 ≡ 期权收回权利金×卖回价 − 权利金×开仓价
(收回≠账户卖掉的全部现货;多余仓只影响均价).
""" """
pnl = _safe_float(coin_pnl) pnl = _safe_float(coin_pnl)
paid = _safe_float(premium_paid) paid = _safe_float(premium_paid)
open_px = _safe_float(open_index_px)
close_px = _safe_float(sell_back_px) close_px = _safe_float(sell_back_px)
if pnl is None or paid is None: if pnl is None or paid is None:
return 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 return None
prem_move = float(paid) * float(open_px) - float(paid) * float(close_px) initial = _safe_float(initial_premium_usdt)
return round(float(pnl) * float(close_px) - prem_move, 4) 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( def resolve_spot_vwap_prices(
@@ -210,13 +213,18 @@ def resolve_spot_vwap_prices(
open_index_px: Any = None, open_index_px: Any = None,
close_index_px: Any = None, close_index_px: Any = None,
) -> tuple[float | None, float | None, str]: ) -> 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): 返回 (open_px, close_px, gate):
gate='' 可算; 'pending_sell' 卖回未完成; 'missing' 缺价 gate='' 可算; 'pending_sell' 卖回未完成; 'missing' 缺价
""" """
open_px = _safe_float(open_index_px) 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 bid = None
try: try:
if bridge_id is not None and str(bridge_id).strip() != "": if bridge_id is not None and str(bridge_id).strip() != "":
@@ -233,26 +241,34 @@ def resolve_spot_vwap_prices(
) )
ensure_bridge_table(conn) ensure_bridge_table(conn)
bv = bridge_buy_vwap(conn, bid)
if bv is not None and bv > 0:
open_px = bv
row = conn.execute( row = conn.execute(
"SELECT status FROM options_spot_bridge WHERE id=?", "SELECT status FROM options_spot_bridge WHERE id=?",
(bid,), (bid,),
).fetchone() ).fetchone()
st = str( if row is None:
row["status"] if row and hasattr(row, "keys") else (row[0] if row else "") or "" # 桥记录不存在:按无桥处理,close_index 视为调用方卖回价
) close_px = close_hint
# 桥未关闭(=卖回流程未结束):禁止用平仓指数冒充卖回 VWAP 提前结算 else:
if st and st != BRIDGE_CLOSED: if open_px is None or open_px <= 0:
return open_px, None, "pending_sell" bv = bridge_buy_vwap(conn, bid)
sv = bridge_sell_vwap(conn, bid) if bv is not None and bv > 0:
if sv is not None and sv > 0: open_px = bv
close_px = sv st = str(
# 已关桥但无卖回 VWAP:允许回退 close_index_px;两者都无则 missing 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: except Exception:
pass 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, "missing"
return open_px, close_px, "" return open_px, close_px, ""
@@ -264,6 +280,7 @@ def resolve_true_pnl_usdt_from_trade(
realized_pnl: Any = None, realized_pnl: Any = None,
open_index_px: Any = None, open_index_px: Any = None,
close_index_px: Any = None, close_index_px: Any = None,
initial_premium_usdt: Any = None,
open_fee: Any = 0, open_fee: Any = 0,
close_fee: Any = 0, close_fee: Any = 0,
premium_ccy: str | None = None, premium_ccy: str | None = None,
@@ -271,7 +288,7 @@ def resolve_true_pnl_usdt_from_trade(
conn: Any = None, conn: Any = None,
bridge_id: Any = None, bridge_id: Any = None,
) -> tuple[float | None, str]: ) -> tuple[float | None, str]:
"""由成交字段算复盘真盈亏;开/卖回价优先多笔 VWAP. """由成交字段算复盘真盈亏: (权利金+币盈亏)×卖回价 − 初始权利金U.
返回 (pnl_u, source): premium_move | stable | pending_sell | none 返回 (pnl_u, source): premium_move | stable | pending_sell | none
""" """
@@ -299,12 +316,12 @@ def resolve_true_pnl_usdt_from_trade(
) )
if gate == "pending_sell": if gate == "pending_sell":
return None, "pending_sell" return None, "pending_sell"
# 只用:历史币盈亏×卖回价 − 权利金×(开仓价−卖回价)
usdt = true_pnl_usdt_premium_move( usdt = true_pnl_usdt_premium_move(
coin_pnl=coin, coin_pnl=coin,
premium_paid=paid, premium_paid=paid,
open_index_px=open_px,
sell_back_px=close_px, sell_back_px=close_px,
initial_premium_usdt=initial_premium_usdt,
open_index_px=open_px,
) )
if usdt is not None: if usdt is not None:
return usdt, "premium_move" return usdt, "premium_move"
@@ -325,8 +342,7 @@ def settle_sim_coin_pnl_usdt(
"""由毛权利金生成净币盈亏,再按用户式折 U(费只扣进币盈亏一次). """由毛权利金生成净币盈亏,再按用户式折 U(费只扣进币盈亏一次).
币盈亏 = 收回 − 已付 − 开/平手续费 币盈亏 = 收回 − 已付 − 开/平手续费
真盈亏U = 币盈亏×卖回价 − 权利金×(开仓价−卖回价) 真盈亏U = (权利金+币盈亏)×卖回价 − 初始权利金U
不再用「毛收回折U后再减手续费U」(避免与历史币盈亏口径两套算法).
""" """
del fee_ccy del fee_ccy
paid = float(_safe_float(premium_paid) or 0.0) 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( pnl_u = true_pnl_usdt_premium_move(
coin_pnl=net_coin, coin_pnl=net_coin,
premium_paid=paid, premium_paid=paid,
open_index_px=open_idx,
sell_back_px=sell_px, sell_back_px=sell_px,
initial_premium_usdt=initial_u,
) )
return { return {
"realized_pnl": net_coin, "realized_pnl": net_coin,
@@ -391,7 +407,7 @@ def settle_closed_pnl(
币数量净盈亏 = 收回 − 已付 − 开/平手续费(只扣一次). 币数量净盈亏 = 收回 − 已付 − 开/平手续费(只扣一次).
币本位 U: 币本位 U:
期权收回权利金×卖回价 − 权利金×开仓价 − 手续费U (权利金+币盈亏)×卖回价 − 初始权利金U
(开仓价缺失时回退为净币×卖回价;币量不含现货多余仓) (开仓价缺失时回退为净币×卖回价;币量不含现货多余仓)
USDC: 净额原样为 U. USDC: 净额原样为 U.
+15 -8
View File
@@ -575,7 +575,8 @@ def sync_options_from_local_trades(
close_fee=cfee, close_fee=cfee,
premium_ccy=ccy, 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 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 sell_idx = close_idx if (close_idx is not None and close_idx > 0) else None
from lib.options.options_pnl_settle_lib import ( from lib.options.options_pnl_settle_lib import (
@@ -585,7 +586,7 @@ def sync_options_from_local_trades(
status_label = "已平" status_label = "已平"
pnl_u = None pnl_u = None
# 始终问桥:买/卖回 VWAP 优先于库内指数占位价(防预算反推开仓价、平仓指数冒充卖回价) # 有桥时卖回价只认 close_sell VWAP;开仓价不覆盖已冻值
opx, cpx, _gate = resolve_spot_vwap_prices( opx, cpx, _gate = resolve_spot_vwap_prices(
conn, conn,
bridge_id=r["bridge_id"], bridge_id=r["bridge_id"],
@@ -598,6 +599,7 @@ def sync_options_from_local_trades(
realized_pnl=r["realized_pnl"], realized_pnl=r["realized_pnl"],
open_index_px=opx, open_index_px=opx,
close_index_px=cpx, close_index_px=cpx,
initial_premium_usdt=stored_initial,
open_fee=ofee, open_fee=ofee,
close_fee=cfee, close_fee=cfee,
premium_ccy=ccy, premium_ccy=ccy,
@@ -610,30 +612,35 @@ def sync_options_from_local_trades(
pnl_u = None pnl_u = None
elif pnl_u is not None and pnl_src not in ("none", "pending_sell"): elif pnl_u is not None and pnl_src not in ("none", "pending_sell"):
try: 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( conn.execute(
""" """
UPDATE options_trades UPDATE options_trades
SET realized_pnl_usdt = ?, 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), close_index_px = COALESCE(?, close_index_px),
initial_premium_usdt = COALESCE(initial_premium_usdt, ?),
pnl_settled = 1 pnl_settled = 1
WHERE id = ? 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 = opx
open_idx_for_prem = opx open_idx_for_prem = opx
if cpx is not None and cpx > 0: if cpx is not None and cpx > 0:
sell_idx = cpx sell_idx = cpx
if stored_initial is None and heal_initial is not None:
stored_initial = heal_initial
except Exception: except Exception:
pass pass
else: else:
status_label = "缺价未核" status_label = "缺价未核"
pnl_u = None pnl_u = None
# 初始权利金(U)=开仓指数×权利金币量;不含多买 # 初始权利金U:优先开仓冻结值
stored_initial = _safe_float(r["initial_premium_usdt"])
prem_u = stored_initial prem_u = stored_initial
if prem_u is None: if prem_u is None:
prem_u = _to_usdt(prem, ccy=ccy, idx=open_idx_for_prem) prem_u = _to_usdt(prem, ccy=ccy, idx=open_idx_for_prem)
@@ -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, SELECT id, inst_id, underlying, premium_paid, premium_received, realized_pnl,
open_index_px, close_index_px, realized_pnl_usdt, bridge_id, 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 FROM options_trades
WHERE status='closed' WHERE status='closed'
ORDER BY id 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_open = _safe_float(r["open_index_px"])
old_close = _safe_float(r["close_index_px"]) old_close = _safe_float(r["close_index_px"])
old_u = _safe_float(r["realized_pnl_usdt"]) 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( new_u = true_pnl_usdt_premium_move(
coin_pnl=coin, coin_pnl=coin,
premium_paid=paid, premium_paid=paid,
open_index_px=open_px or old_open,
sell_back_px=close_px or old_close, sell_back_px=close_px or old_close,
initial_premium_usdt=initial_u,
open_index_px=use_open,
) )
item = { item = {
"id": tid, "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), SET open_index_px = COALESCE(?, open_index_px),
close_index_px = COALESCE(?, close_index_px), close_index_px = COALESCE(?, close_index_px),
realized_pnl_usdt = COALESCE(?, realized_pnl_usdt), realized_pnl_usdt = COALESCE(?, realized_pnl_usdt),
initial_premium_usdt = CASE initial_premium_usdt = COALESCE(?, initial_premium_usdt),
WHEN ? IS NOT NULL AND premium_paid IS NOT NULL
THEN round(premium_paid * ?, 4)
ELSE initial_premium_usdt
END,
pnl_settled = 1 pnl_settled = 1
WHERE id = ? WHERE id = ?
""", """,
@@ -243,8 +244,7 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]:
open_px, open_px,
close_px, close_px,
new_u, new_u,
open_px, initial_u,
open_px,
tid, tid,
), ),
) )
+2 -2
View File
@@ -158,14 +158,14 @@ class AccountPnlSettleTests(unittest.TestCase):
true_pnl_usdt_premium_move, 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( self.assertAlmostEqual(
float( float(
true_pnl_usdt_premium_move( true_pnl_usdt_premium_move(
coin_pnl=0.01, coin_pnl=0.01,
premium_paid=0.01, premium_paid=0.01,
open_index_px=90000,
sell_back_px=80000, sell_back_px=80000,
initial_premium_usdt=900.0,
) )
), ),
700.0, 700.0,
+16 -6
View File
@@ -26,23 +26,32 @@ class FeeConvertTests(unittest.TestCase):
class PremiumMoveFormulaTests(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 coin = 0.01
paid = 0.01 paid = 0.01
open_px = 90000.0 open_px = 90000.0
close_px = 80000.0 close_px = 80000.0
initial = paid * open_px
a = true_pnl_usdt_premium_move( a = true_pnl_usdt_premium_move(
coin_pnl=coin, coin_pnl=coin,
premium_paid=paid, premium_paid=paid,
open_index_px=open_px,
sell_back_px=close_px, sell_back_px=close_px,
initial_premium_usdt=initial,
) )
# 历史盈亏×卖回 − (权利金×开 − 权利金×平) # (权利金+盈亏)×卖回 − 初始权利金U
expect = coin * close_px - (paid * open_px - paid * close_px) expect = (paid + coin) * close_px - initial
self.assertAlmostEqual(float(a), expect, places=4) self.assertAlmostEqual(float(a), expect, places=4)
# 无费时 ≡ 收回×平 − 已付×开 (recv=paid+coin) # 无费时 ≡ 收回×平 − 已付×开
recv = paid + coin recv = paid + coin
self.assertAlmostEqual(float(a), recv * close_px - paid * open_px, places=4) 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): class SettleClosedPnlTests(unittest.TestCase):
@@ -100,12 +109,13 @@ class SettleClosedPnlTests(unittest.TestCase):
realized_pnl=-0.0062, realized_pnl=-0.0062,
open_index_px=3000, open_index_px=3000,
close_index_px=2500, close_index_px=2500,
initial_premium_usdt=30.0,
open_fee=0.0001, open_fee=0.0001,
close_fee=0.0001, close_fee=0.0001,
premium_ccy="ETH", premium_ccy="ETH",
) )
self.assertEqual(src, "premium_move") 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) self.assertAlmostEqual(float(pnl), -20.5, places=2)
# 若再扣一遍费会更亏于 -20.5 # 若再扣一遍费会更亏于 -20.5
self.assertGreater(float(pnl), -20.55) self.assertGreater(float(pnl), -20.55)
+9 -7
View File
@@ -1,4 +1,4 @@
"""复盘真盈亏=币盈亏×卖回价 − 权利金×(开仓价−卖回价).""" """复盘真盈亏=(权利金+币盈亏)×卖回价 − 初始权利金U."""
from __future__ import annotations from __future__ import annotations
import sqlite3 import sqlite3
@@ -56,7 +56,7 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase):
conn.close() conn.close()
def test_refresh_prefers_bridge_sell_vwap_over_wrong_index(self): 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_db import init_options_tables
from lib.options.options_review_lib import sync_options_from_local_trades from lib.options.options_review_lib import sync_options_from_local_trades
from lib.options.options_spot_bridge_lib import ( from lib.options.options_spot_bridge_lib import (
@@ -86,13 +86,13 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase):
inst_id, underlying, opt_type, sheets, eth_amount, inst_id, underlying, opt_type, sheets, eth_amount,
premium_paid, premium_received, realized_pnl, premium_ccy, premium_paid, premium_received, realized_pnl, premium_ccy,
open_fee, close_fee, status, realized_pnl_usdt, 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 created_at, closed_at
) VALUES ( ) VALUES (
'BTC-USD-261015-84000-P', 'BTC', 'P', 1, 0.01, 'BTC-USD-261015-84000-P', 'BTC', 'P', 1, 0.01,
0.01, 0.02, 0.01, 'BTC', 0.01, 0.02, 0.01, 'BTC',
0, 0, 'closed', 1544.0, 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' '2026-10-12 01:46:50', '2026-10-13 08:07:35'
) )
""", """,
@@ -101,11 +101,13 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase):
conn.commit() conn.commit()
sync_options_from_local_trades(conn, None) sync_options_from_local_trades(conn, None)
trade = conn.execute( 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() ).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) 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) self.assertAlmostEqual(float(trade["realized_pnl_usdt"]), 788.26, places=2)
conn.close() conn.close()
+35 -4
View File
@@ -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=1600.0, coin_sold=0.8)
add_bridge_close_sell_fill(conn, bid, usdt_recovered=400.0, coin_sold=0.2) add_bridge_close_sell_fill(conn, bid, usdt_recovered=400.0, coin_sold=0.2)
update_bridge(conn, bid, status=BRIDGE_CLOSED, close=True) update_bridge(conn, bid, status=BRIDGE_CLOSED, close=True)
# buy_vwap=2000, sell_vwap=2000; coin_pnl=0.01 # buy_vwap=2000, sell_vwap=2000; 初始权利金按买 VWAP=20
# A = 0.01*2000 - (0.01*2000 - 0.01*2000) = 20 # U=(0.01+0.01)*2000 - 20 = 20; 若误用指数 80000 会得到大数
# 若误用指数 90000/80000 会得到 700
pnl, src = resolve_true_pnl_usdt_from_trade( pnl, src = resolve_true_pnl_usdt_from_trade(
premium_paid=0.01, premium_paid=0.01,
premium_received=0.02, premium_received=0.02,
realized_pnl=0.01, realized_pnl=0.01,
open_index_px=90000.0, open_index_px=2000.0,
close_index_px=80000.0, close_index_px=80000.0,
initial_premium_usdt=20.0,
premium_ccy="BTC", premium_ccy="BTC",
conn=conn, conn=conn,
bridge_id=bid, bridge_id=bid,
@@ -120,6 +120,37 @@ class SpotVwapBridgeTests(unittest.TestCase):
self.assertIsNone(pnl2) self.assertIsNone(pnl2)
conn.close() 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): def test_buy_vwap_ignores_shrunk_coin_bought(self):
"""开仓后 coin_bought 被改成权利金量时,不得用 budget/缩水量当买价.""" """开仓后 coin_bought 被改成权利金量时,不得用 budget/缩水量当买价."""
from lib.options.options_spot_bridge_lib import ( from lib.options.options_spot_bridge_lib import (