3416996ae5
Co-authored-by: Cursor <cursoragent@cursor.com>
247 lines
8.8 KiB
Python
247 lines
8.8 KiB
Python
"""期权盈亏结算:模拟盘本地扣费;实盘以交易所历史为准并冻结折 U."""
|
||
from __future__ import annotations
|
||
|
||
import unittest
|
||
|
||
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,
|
||
compute_options_stats_from_history,
|
||
history_pnl_to_usdt,
|
||
)
|
||
|
||
|
||
class FeeConvertTests(unittest.TestCase):
|
||
def test_fee_same_ccy(self):
|
||
self.assertAlmostEqual(
|
||
fee_to_premium_ccy(0.001, "ETH", premium_ccy="ETH", index_px=3000),
|
||
0.001,
|
||
places=8,
|
||
)
|
||
|
||
|
||
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):
|
||
# 无开仓指数时回退净币×平仓价
|
||
s = settle_closed_pnl(
|
||
premium_paid=0.01,
|
||
premium_received=0.02,
|
||
open_fee=0.0005,
|
||
close_fee=0.0005,
|
||
premium_ccy="ETH",
|
||
index_px=3000,
|
||
)
|
||
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."""
|
||
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 权利金用开仓指数
|
||
)
|
||
# 0.02270864*81000 - 0.011005*86000
|
||
expect = 0.02270864 * 81000 - 0.011005 * 86000
|
||
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), expect, places=2)
|
||
|
||
def test_sim_loss_remaining_minus_fees(self):
|
||
"""亏损:剩余权利金U − 手续费U − 初始U(同一式)."""
|
||
s = settle_closed_pnl(
|
||
premium_paid=0.01,
|
||
premium_received=0.004,
|
||
open_fee=0.0001,
|
||
close_fee=0.0001,
|
||
premium_ccy="ETH",
|
||
index_px=2500,
|
||
open_index_px=3000,
|
||
)
|
||
# 0.004*2500 - 0.01*3000 - (0.0001*3000+0.0001*2500) = 10 - 30 - 0.55 = -20.55
|
||
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), -20.55, places=2)
|
||
|
||
def test_budget_ignored_for_usdt(self):
|
||
# budget 不得抬高开仓成本
|
||
s = settle_closed_pnl(
|
||
premium_paid=0.01,
|
||
premium_received=0.02,
|
||
open_fee=0,
|
||
close_fee=0,
|
||
premium_ccy="ETH",
|
||
index_px=3000,
|
||
budget_usdt=26.0,
|
||
)
|
||
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), 30.0, places=4)
|
||
|
||
def test_usdc_freeze_is_net(self):
|
||
s = settle_closed_pnl(
|
||
premium_paid=10,
|
||
premium_received=12,
|
||
open_fee=0.1,
|
||
close_fee=0.1,
|
||
premium_ccy="USDC",
|
||
index_px=None,
|
||
)
|
||
self.assertAlmostEqual(float(s["realized_pnl"]), 1.8, places=4)
|
||
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), 1.8, places=4)
|
||
|
||
def test_exchange_realized_not_fee_again(self):
|
||
s = settle_exchange_realized_pnl(
|
||
realized_pnl=0.009,
|
||
premium_ccy="ETH",
|
||
index_px=3000,
|
||
fee_hint=0.001,
|
||
)
|
||
self.assertAlmostEqual(float(s["realized_pnl"]), 0.009, places=8)
|
||
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), 27.0, places=4)
|
||
|
||
|
||
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
|
||
from lib.options.options_stats_lib import sync_sim_cash_realized_pnl_usdt
|
||
|
||
conn = sqlite3.connect(":memory:")
|
||
conn.row_factory = sqlite3.Row
|
||
init_options_tables(conn)
|
||
conn.execute(
|
||
"""
|
||
INSERT INTO options_trades(
|
||
inst_id, underlying, opt_type, sheets, eth_amount,
|
||
premium_paid, premium_received, realized_pnl, premium_ccy,
|
||
status, trading_usdt_before, trading_usdt_after,
|
||
realized_pnl_usdt, pnl_settled, created_at, closed_at
|
||
) VALUES (
|
||
'ETH-USD-261009-2500-P', 'ETH', 'P', 1, 0.01,
|
||
0.01, 0.02, 0.01, 'ETH',
|
||
'closed', 1000.0, 1952.96,
|
||
952.96, 1, '2026-10-06 10:00:00', '2026-10-08 14:00:00'
|
||
)
|
||
"""
|
||
)
|
||
conn.commit()
|
||
n = sync_sim_cash_realized_pnl_usdt(conn, None)
|
||
self.assertEqual(n, 0)
|
||
row = conn.execute(
|
||
"SELECT realized_pnl_usdt FROM options_trades WHERE id=1"
|
||
).fetchone()
|
||
self.assertAlmostEqual(float(row["realized_pnl_usdt"]), 952.96, places=2)
|
||
conn.close()
|
||
|
||
|
||
class ExchangeFreezeStatsTests(unittest.TestCase):
|
||
def test_freeze_uses_exchange_coin_pnl(self):
|
||
import sqlite3
|
||
|
||
from lib.options.options_db import init_options_tables
|
||
|
||
conn = sqlite3.connect(":memory:")
|
||
conn.row_factory = sqlite3.Row
|
||
init_options_tables(conn)
|
||
|
||
class _Ex:
|
||
def public_get_market_ticker(self, params):
|
||
inst = (params or {}).get("instId") or ""
|
||
if inst.startswith("BTC"):
|
||
return {"data": [{"last": "100000"}]}
|
||
return {"data": [{"last": "2500"}]}
|
||
|
||
history = [
|
||
{
|
||
"status": "closed",
|
||
"source": "exchange",
|
||
"history_key": "ex|BTC-USD-261009-84000-P|1",
|
||
"inst_id": "BTC-USD-261009-84000-P",
|
||
"underlying": "BTC",
|
||
"premium_ccy": "BTC",
|
||
"realized_pnl": 0.000138,
|
||
"created_at": "2026-10-08 10:00:00",
|
||
"closed_at": "2026-10-08 22:22:56",
|
||
},
|
||
{
|
||
"status": "closed",
|
||
"source": "exchange",
|
||
"history_key": "ex|ETH-USD-261006-2700-P|2",
|
||
"inst_id": "ETH-USD-261006-2700-P",
|
||
"underlying": "ETH",
|
||
"premium_ccy": "ETH",
|
||
"realized_pnl": -0.00648,
|
||
"created_at": "2026-10-05 10:00:00",
|
||
"closed_at": "2026-10-06 16:00:27",
|
||
},
|
||
]
|
||
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"}]}
|
||
|
||
frozen2 = apply_exchange_pnl_freeze(conn, history, _Ex2())
|
||
converted2 = history_pnl_to_usdt(frozen2, _Ex2())
|
||
self.assertAlmostEqual(float(converted2[0]["realized_pnl"]), 13.8, places=4)
|
||
conn.close()
|
||
|
||
def test_frozen_usdt_ignores_live_index(self):
|
||
history = [
|
||
{
|
||
"status": "closed",
|
||
"inst_id": "ETH-USD-260701-2500-C",
|
||
"underlying": "ETH",
|
||
"premium_ccy": "ETH",
|
||
"realized_pnl": 0.01,
|
||
"realized_pnl_usdt": 25.0,
|
||
"close_index_px": 2500.0,
|
||
"created_at": "2026-10-01 10:00:00",
|
||
"closed_at": "2026-10-01 12:00:00",
|
||
}
|
||
]
|
||
|
||
class _Ex:
|
||
def public_get_market_ticker(self, params):
|
||
return {"data": [{"last": "99999"}]}
|
||
|
||
converted = history_pnl_to_usdt(history, _Ex())
|
||
self.assertAlmostEqual(float(converted[0]["realized_pnl"]), 25.0, places=4)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|