fix复盘真盈亏改为期权币盈亏×平仓价,不再绑卖回与初始权利金差。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-13 12:39:15 +08:00
parent 839baea375
commit fc0d76d174
11 changed files with 98 additions and 211 deletions
+6 -8
View File
@@ -15,7 +15,7 @@ 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 但不采用
# coin_pnl=0.01, close=80000 → U=800; 账户差分不采用
conn.execute(
"""
INSERT INTO options_trades(
@@ -40,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"]), 700.0, places=2)
self.assertAlmostEqual(float(row["realized_pnl_usdt"]), 800.0, places=2)
self.assertEqual(int(row["pnl_settled"]), 1)
# 已冻结不再改
@@ -51,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"]), 700.0, places=2)
self.assertAlmostEqual(float(row2["realized_pnl_usdt"]), 800.0, places=2)
conn.close()
def test_stats_use_coin_times_sell_px_not_account(self):
@@ -158,17 +158,15 @@ class AccountPnlSettleTests(unittest.TestCase):
true_pnl_usdt_premium_move,
)
# (0.01+0.01)*80000 - 0.01*90000 = 700
# 0.01*80000 = 800
self.assertAlmostEqual(
float(
true_pnl_usdt_premium_move(
coin_pnl=0.01,
premium_paid=0.01,
sell_back_px=80000,
initial_premium_usdt=900.0,
)
),
700.0,
800.0,
places=4,
)
pnl, src = resolve_true_pnl_usdt_from_trade(
@@ -180,7 +178,7 @@ class AccountPnlSettleTests(unittest.TestCase):
premium_ccy="BTC",
)
self.assertEqual(src, "premium_move")
self.assertAlmostEqual(float(pnl), 700.0, places=4)
self.assertAlmostEqual(float(pnl), 800.0, places=4)
def test_settle_blocked_when_sellable_coin(self):
from lib.options.options_account_pnl_lib import try_settle_after_sellback
+11 -41
View File
@@ -26,37 +26,18 @@ class FeeConvertTests(unittest.TestCase):
class PremiumMoveFormulaTests(unittest.TestCase):
def test_user_formula_matches_coin_basis_minus_initial(self):
def test_option_pnl_times_close_px(self):
coin = 0.01
paid = 0.01
open_px = 90000.0
close_px = 80000.0
initial = paid * open_px
a = true_pnl_usdt_premium_move(
coin_pnl=coin,
premium_paid=paid,
sell_back_px=close_px,
initial_premium_usdt=initial,
)
# (权利金+盈亏)×卖回 − 初始权利金U
expect = (paid + coin) * close_px - initial
self.assertAlmostEqual(float(a), expect, places=4)
# 无费时 ≡ 收回×平 − 已付×开
recv = paid + coin
self.assertAlmostEqual(float(a), recv * close_px - paid * open_px, places=4)
# 缺初始时用开仓价补算,结果相同
b = true_pnl_usdt_premium_move(
coin_pnl=coin,
premium_paid=paid,
sell_back_px=close_px,
open_index_px=open_px,
)
self.assertAlmostEqual(float(b), float(a), places=4)
self.assertAlmostEqual(float(a), coin * close_px, places=4)
class SettleClosedPnlTests(unittest.TestCase):
def test_sim_net_times_close_index(self):
# 无开仓指数时回退净币×平仓价
s = settle_closed_pnl(
premium_paid=0.01,
premium_received=0.02,
@@ -68,23 +49,23 @@ class SettleClosedPnlTests(unittest.TestCase):
self.assertAlmostEqual(float(s["realized_pnl"]), 0.009, places=8)
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), 27.0, places=4)
def test_sim_proceeds_minus_open_premium_usdt(self):
"""盈利=(收回币)×卖回价 − 已付币×开仓指数 − 手续费U."""
def test_sim_coin_pnl_times_close(self):
"""真盈亏=净币盈亏×平仓价."""
s = settle_closed_pnl(
premium_paid=0.011005,
premium_received=0.02270864,
open_fee=0,
close_fee=0,
premium_ccy="BTC",
index_px=81000, # 卖回价
open_index_px=86000, # 初始 U 权利金用开仓指数
index_px=81000,
open_index_px=86000,
)
# 0.02270864*81000 - 0.011005*86000
expect = 0.02270864 * 81000 - 0.011005 * 86000
net = 0.02270864 - 0.011005
expect = net * 81000
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), expect, places=2)
def test_sim_loss_remaining_minus_fees(self):
"""亏损:费先扣进币盈亏,再按用户式折 U(不另扣手续费U)."""
"""亏损:费先扣进币盈亏,再×平仓价."""
s = settle_closed_pnl(
premium_paid=0.01,
premium_received=0.004,
@@ -94,34 +75,26 @@ class SettleClosedPnlTests(unittest.TestCase):
index_px=2500,
open_index_px=3000,
)
# net_coin=-0.0062; U=-0.0062*2500 - 0.01*(3000-2500)=-20.5
self.assertAlmostEqual(float(s["realized_pnl"]), -0.0062, places=8)
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), -20.5, places=2)
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), -15.5, places=2)
def test_resolve_loss_does_not_deduct_fee_again(self):
"""历史币盈亏已含费;resolve 只跑用户式,不再用 open_fee/close_fee 扣第二遍."""
from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade
# coin 已是 recv-paid-fees=-0.0062
pnl, src = resolve_true_pnl_usdt_from_trade(
premium_paid=0.01,
premium_received=0.004,
realized_pnl=-0.0062,
open_index_px=3000,
close_index_px=2500,
initial_premium_usdt=30.0,
open_fee=0.0001,
close_fee=0.0001,
premium_ccy="ETH",
)
self.assertEqual(src, "premium_move")
# (0.01-0.0062)*2500 - 30 = 9.5 - 30 = -20.5
self.assertAlmostEqual(float(pnl), -20.5, places=2)
# 若再扣一遍费会更亏于 -20.5
self.assertGreater(float(pnl), -20.55)
self.assertAlmostEqual(float(pnl), -15.5, places=2)
def test_budget_ignored_for_usdt(self):
# budget 不得抬高开仓成本
s = settle_closed_pnl(
premium_paid=0.01,
premium_received=0.02,
@@ -158,7 +131,6 @@ class SettleClosedPnlTests(unittest.TestCase):
class SimPremiumPnlStatsTests(unittest.TestCase):
def test_sync_sim_no_longer_overwrites_account_settled(self):
"""账户结算后 sync 兼容入口不得改写."""
import sqlite3
from lib.options.options_db import init_options_tables
@@ -235,13 +207,11 @@ class ExchangeFreezeStatsTests(unittest.TestCase):
]
frozen = apply_exchange_pnl_freeze(conn, history, _Ex())
converted = history_pnl_to_usdt(frozen, _Ex())
# 0.000138*100000=13.8; -0.00648*2500=-16.2; net=-2.4
self.assertAlmostEqual(float(converted[0]["realized_pnl"]), 13.8, places=4)
self.assertAlmostEqual(float(converted[1]["realized_pnl"]), -16.2, places=4)
stats = compute_options_stats_from_history(converted)
self.assertAlmostEqual(float(stats["net_realized_pnl"]), -2.4, places=4)
# 指数再变也不改冻结 U
class _Ex2:
def public_get_market_ticker(self, params):
return {"data": [{"last": "999999"}]}
+13 -23
View File
@@ -1,4 +1,4 @@
"""复盘真盈亏=(权利金+币盈亏)×卖回价 − 初始权利金U."""
"""复盘真盈亏=期权历史币盈亏×平仓价."""
from __future__ import annotations
import sqlite3
@@ -7,7 +7,7 @@ import unittest
class ReviewLocalSyncFreezeTests(unittest.TestCase):
def test_refresh_recomputes_u_keeps_frozen_qty_px(self):
"""刷新按公式重算U;权利金币/币盈亏不变."""
"""刷新按币盈亏×平仓价重算U;币盈亏/平仓价不变."""
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
@@ -41,22 +41,21 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase):
trade = conn.execute(
"SELECT realized_pnl_usdt, open_index_px, close_index_px, pnl_settled, realized_pnl, premium_paid FROM options_trades WHERE id=1"
).fetchone()
# 0.01*80000 - 0.01*(90000-80000)=700;旧错账 -51 被刷新纠正
self.assertAlmostEqual(float(trade["realized_pnl_usdt"]), 700.0, places=2)
# 0.01*80000=800;旧错账 -51 被刷新纠正
self.assertAlmostEqual(float(trade["realized_pnl_usdt"]), 800.0, places=2)
self.assertAlmostEqual(float(trade["open_index_px"]), 90000.0, places=2)
self.assertAlmostEqual(float(trade["close_index_px"]), 80000.0, places=2)
self.assertEqual(int(trade["pnl_settled"]), 1)
self.assertAlmostEqual(float(trade["realized_pnl"]), 0.01, places=8)
self.assertAlmostEqual(float(trade["premium_paid"]), 0.01, places=8)
row = conn.execute(
"SELECT realized_pnl_total FROM options_review_trades WHERE history_key=?",
("local_opt:1",),
).fetchone()
self.assertAlmostEqual(float(row["realized_pnl_total"]), 700.0, places=2)
self.assertAlmostEqual(float(row["realized_pnl_total"]), 800.0, places=2)
conn.close()
def test_refresh_prefers_bridge_sell_vwap_over_wrong_index(self):
"""库内卖回价若是指数占位,刷新须用桥上卖回 VWAP;初始权利金U冻结不改."""
"""有卖回 VWAP 时优先用它折U."""
from lib.options.options_db import init_options_tables
from lib.options.options_review_lib import sync_options_from_local_trades
from lib.options.options_spot_bridge_lib import (
@@ -86,13 +85,13 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase):
inst_id, underlying, opt_type, sheets, eth_amount,
premium_paid, premium_received, realized_pnl, premium_ccy,
open_fee, close_fee, status, realized_pnl_usdt,
open_index_px, close_index_px, initial_premium_usdt, pnl_settled, bridge_id,
open_index_px, close_index_px, pnl_settled, bridge_id,
created_at, closed_at
) VALUES (
'BTC-USD-261015-84000-P', 'BTC', 'P', 1, 0.01,
0.01, 0.02, 0.01, 'BTC',
0, 0, 'closed', 1544.0,
88086.0, 85400.0, 840.10, 1, ?,
88086.0, 85400.0, 1, ?,
'2026-10-12 01:46:50', '2026-10-13 08:07:35'
)
""",
@@ -101,14 +100,11 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase):
conn.commit()
sync_options_from_local_trades(conn, None)
trade = conn.execute(
"SELECT realized_pnl_usdt, open_index_px, close_index_px, initial_premium_usdt FROM options_trades WHERE id=1"
"SELECT realized_pnl_usdt, close_index_px FROM options_trades WHERE id=1"
).fetchone()
# 已冻开仓价/初始权利金不覆盖;卖回价用桥 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)
# (0.01+0.01)*81418 - 840.10 = 788.26
self.assertAlmostEqual(float(trade["realized_pnl_usdt"]), 788.26, places=2)
# 0.01*81418
self.assertAlmostEqual(float(trade["realized_pnl_usdt"]), 814.18, places=2)
conn.close()
def test_premium_usdt_move_formula(self):
@@ -122,9 +118,6 @@ 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(
@@ -151,12 +144,11 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase):
"SELECT realized_pnl_total FROM options_review_trades WHERE history_key=?",
("local_opt:1",),
).fetchone()
self.assertAlmostEqual(float(row["realized_pnl_total"]), 700.0, places=2)
# 纠偏写回,不用账户差分 1506
self.assertAlmostEqual(float(row["realized_pnl_total"]), 800.0, places=2)
healed = conn.execute(
"SELECT realized_pnl_usdt FROM options_trades WHERE id=1"
).fetchone()
self.assertAlmostEqual(float(healed["realized_pnl_usdt"]), 700.0, places=2)
self.assertAlmostEqual(float(healed["realized_pnl_usdt"]), 800.0, places=2)
conn.close()
def test_uncomputable_old_trade_marked(self):
@@ -170,7 +162,6 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase):
init_options_tables(conn)
ensure_runtime_settings_table(conn)
runtime_set(conn, TRADING_MODE_KEY, MODE_SIM)
# 未冻结且无开仓/卖回价 → 缺价未核(不瞎写)
conn.execute(
"""
INSERT INTO options_trades(
@@ -233,7 +224,6 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase):
"SELECT realized_pnl_total FROM options_review_trades WHERE history_key=?",
("local_opt:1",),
).fetchone()
# 开平价相同:波动项为0 → 等于币×卖回价=850
self.assertAlmostEqual(float(row["realized_pnl_total"]), 850.0, places=2)
conn.close()
+11 -33
View File
@@ -24,12 +24,10 @@ class SpotVwapBridgeTests(unittest.TestCase):
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)
@@ -56,15 +54,13 @@ class SpotVwapBridgeTests(unittest.TestCase):
add_bridge_close_sell_fill(conn, bid, usdt_recovered=1600.0, coin_sold=0.8)
add_bridge_close_sell_fill(conn, bid, usdt_recovered=400.0, coin_sold=0.2)
update_bridge(conn, bid, status=BRIDGE_CLOSED, close=True)
# buy_vwap=2000, sell_vwap=2000; 初始权利金按买 VWAP=20
# U=(0.01+0.01)*2000 - 20 = 20; 若误用指数 80000 会得到大数
# sell_vwap=2000; U=0.01*2000=20; 不用指数 80000
pnl, src = resolve_true_pnl_usdt_from_trade(
premium_paid=0.01,
premium_received=0.02,
realized_pnl=0.01,
open_index_px=2000.0,
open_index_px=90000.0,
close_index_px=80000.0,
initial_premium_usdt=20.0,
premium_ccy="BTC",
conn=conn,
bridge_id=bid,
@@ -73,13 +69,12 @@ class SpotVwapBridgeTests(unittest.TestCase):
self.assertAlmostEqual(float(pnl), 20.0, places=2)
conn.close()
def test_pending_sell_blocks_true_pnl(self):
def test_pending_sell_still_allows_option_pnl(self):
"""现货待卖回不阻断期权盈亏折U(用冻结平仓价)."""
from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade
from lib.options.options_spot_bridge_lib import (
BRIDGE_HOLDING,
BRIDGE_PENDING_SELL,
add_bridge_buy_fill,
add_bridge_close_sell_fill,
ensure_bridge_table,
insert_bridge,
update_bridge,
@@ -90,7 +85,6 @@ class SpotVwapBridgeTests(unittest.TestCase):
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,
@@ -102,26 +96,12 @@ class SpotVwapBridgeTests(unittest.TestCase):
conn=conn,
bridge_id=bid,
)
self.assertEqual(src, "pending_sell")
self.assertIsNone(pnl)
# holding 未关桥同样不得用指数提前结算
update_bridge(conn, bid, status=BRIDGE_HOLDING)
pnl2, src2 = 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(src2, "pending_sell")
self.assertIsNone(pnl2)
self.assertEqual(src, "premium_move")
self.assertAlmostEqual(float(pnl), 30.0, places=2)
conn.close()
def test_closed_bridge_without_sell_vwap_is_missing(self):
"""已关桥但无 close_sell 时不得用指数冒充卖回价."""
def test_closed_bridge_without_sell_falls_back_close_index(self):
"""已关桥无卖回 VWAP 时回退冻结平仓价."""
from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade
from lib.options.options_spot_bridge_lib import (
BRIDGE_CLOSED,
@@ -142,17 +122,15 @@ class SpotVwapBridgeTests(unittest.TestCase):
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)
self.assertEqual(src, "premium_move")
self.assertAlmostEqual(float(pnl), 900.0, places=2)
conn.close()
def test_buy_vwap_ignores_shrunk_coin_bought(self):
"""开仓后 coin_bought 被改成权利金量时,不得用 budget/缩水量当买价."""
from lib.options.options_spot_bridge_lib import (
add_bridge_buy_fill,
bridge_buy_vwap,
@@ -166,7 +144,7 @@ class SpotVwapBridgeTests(unittest.TestCase):
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)
update_bridge(conn, bid, coin_bought=0.01) # 缩成权利金
update_bridge(conn, bid, coin_bought=0.01)
self.assertAlmostEqual(float(bridge_buy_vwap(conn, bid)), 2000.0, places=4)
conn.close()