Files
crypto_okx/tests/test_options_pnl_settle.py
T

279 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""期权盈亏结算:模拟盘本地扣费;实盘以交易所历史为准并冻结折 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_coin_basis_minus_initial(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)
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)."""
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,
)
# 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)
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)
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()