"""期权盈亏结算:模拟盘本地扣费;实盘以交易所历史为准并冻结折 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).""" 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, open_fee=0.0001, close_fee=0.0001, premium_ccy="ETH", ) self.assertEqual(src, "premium_move") # -0.0062*2500 - 0.01*(3000-2500) = -15.5 - 5 = -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()