fix复盘真盈亏按权利金USDT波动公式计算,忽略现货多买。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-13 10:16:44 +08:00
parent c2c69e580c
commit 3416996ae5
10 changed files with 249 additions and 280 deletions
+2 -2
View File
@@ -32,8 +32,8 @@ USDT(交易户)
▼
卖光可卖标的币(禁止按开仓权利金封顶)→ 快照 trading_usdt_after
▼
口径 A(复盘真盈亏): after − before ≡ 卖光收回 − 买币;禁止指数冒充
口径 B(期权历史/数据统计): 仅期权腿币盈亏(×卖回价折 U);不含现货
口径 A(复盘真盈亏): 币盈亏×卖回价 − 权利金×(开仓价−卖回价);含权利金U波动,忽略多买
口径 B(期权历史/数据统计): 仅期权腿币盈亏(×卖回价折 U);不含现货/权利金U波动修正
```
### 0.3 启动装配
+12 -11
View File
@@ -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 波动式:币盈亏×卖回价−权利金×(开−平);忽略多买 |
+28 -87
View File
@@ -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
+2 -3
View File
@@ -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
+98 -4
View File
@@ -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
不用买币预算当成本(预算含缓冲).
"""
+30 -59
View File
@@ -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":
@@ -229,7 +229,7 @@
<span class="or-step" aria-hidden="true">1</span>
<div>
<div class="or-section-title" id="or-list-title">期权交易记录</div>
<p class="or-section-desc">真盈亏=卖光币收回 USDT − 开仓买币花费;点「复盘」填写表单。期权历史仅记期权腿币收益,不含现货。</p>
<p class="or-section-desc">真盈亏=历史币盈亏×卖回价 − 权利金×(开仓价−卖回价);计入权利金 USDT 波动,忽略现货多买。期权历史仅记币收益。</p>
</div>
</div>
<div class="options-strike-table-wrap or-trades-table-wrap" id="or-trades-wrap">
@@ -239,7 +239,7 @@
<th>类型</th>
<th>标的/合约</th>
<th>初始权利金</th>
<th title="卖光收回USDT − 开仓买币花费(账户after−before)">真盈亏</th>
<th title="币盈亏×卖回价 − 权利金×(开仓价−卖回价)">真盈亏</th>
<th>盈利率</th>
<th title="仅期权开/平手续费">手续费</th>
<th>开仓时间</th>
+36 -46
View File
@@ -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
+21
View File
@@ -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):
# 无开仓指数时回退净币×平仓价
+18 -66
View File
@@ -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()