Files
CoinOX/tests/test_hedge_po_mode.py
dekun a841c8f31b Fix btc_eth watching entry, preview sizing, and coin-to-U settlement.
Use dual indices for watch/preview; perp entry uses ETH mark; expiry/close convert coin PnL via option index; align legacy long→Call; omit combo U when index missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-14 21:52:20 +08:00

259 lines
9.4 KiB
Python
Raw Permalink 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.
"""永期实例模式 HEDGE_PO_MODE:钳制/定仓/校验/组合U口径."""
from __future__ import annotations
import os
import unittest
from unittest.mock import patch
from lib.hedge_plan.hedge_plan_option_primary_lib import (
contracts_from_option_sheets,
estimate_combo_net_pnl,
size_from_premium,
)
from lib.hedge_plan.hedge_po_mode_lib import (
MODE_BTC,
MODE_BTC_ETH,
MODE_ETH,
apply_mode_to_start_body,
clamp_hedge_po_mode_on_startup,
combo_net_usdt,
default_option_perp_ratio,
get_hedge_po_mode,
normalize_hedge_po_mode,
option_underlying,
perp_underlying,
public_mode_payload,
size_cross_notional,
validate_body_matches_mode,
)
class TestHedgePoMode(unittest.TestCase):
def test_normalize_aliases(self):
self.assertEqual(normalize_hedge_po_mode("ETH"), MODE_ETH)
self.assertEqual(normalize_hedge_po_mode("btc-eth"), MODE_BTC_ETH)
self.assertEqual(normalize_hedge_po_mode("跨币"), MODE_BTC_ETH)
self.assertEqual(normalize_hedge_po_mode("nope"), "")
def test_underlyings(self):
self.assertEqual(option_underlying(MODE_ETH), "ETH")
self.assertEqual(perp_underlying(MODE_ETH), "ETH")
self.assertEqual(option_underlying(MODE_BTC), "BTC")
self.assertEqual(perp_underlying(MODE_BTC), "BTC")
self.assertEqual(option_underlying(MODE_BTC_ETH), "BTC")
self.assertEqual(perp_underlying(MODE_BTC_ETH), "ETH")
def test_cross_ratio_from_k(self):
with patch.dict(os.environ, {"HEDGE_CROSS_NOTIONAL_K": "0.5"}, clear=False):
self.assertAlmostEqual(default_option_perp_ratio(MODE_BTC_ETH), 2.0)
def test_clamp_startup_and_sync_underly(self):
with patch.dict(
os.environ,
{"HEDGE_PO_MODE": "btc_eth", "OKX_OPTIONS_DEFAULT_UNDERLY": "ETH"},
clear=False,
):
summary = clamp_hedge_po_mode_on_startup()
self.assertEqual(summary["mode"], MODE_BTC_ETH)
self.assertEqual(os.environ["HEDGE_PO_MODE"], MODE_BTC_ETH)
self.assertEqual(os.environ["OKX_OPTIONS_DEFAULT_UNDERLY"], "BTC")
self.assertEqual(get_hedge_po_mode(), MODE_BTC_ETH)
def test_clamp_invalid_falls_back(self):
with patch.dict(
os.environ,
{"HEDGE_PO_MODE": "weird", "OKX_OPTIONS_DEFAULT_UNDERLY": "BTC"},
clear=False,
):
summary = clamp_hedge_po_mode_on_startup()
self.assertEqual(summary["mode"], MODE_BTC)
self.assertTrue(summary["warnings"])
def test_apply_and_validate_body(self):
with patch.dict(os.environ, {"HEDGE_PO_MODE": "btc_eth", "HEDGE_CROSS_NOTIONAL_K": "0.5"}):
clamp_hedge_po_mode_on_startup()
body = apply_mode_to_start_body(
{
"direction": "long",
"underlying": "ETH",
"opt_inst_id": "BTC-USD-260831-90000-C",
"exchange_symbol": "ETH/USDT:USDT",
"profit_target_usdt": 20,
}
)
self.assertEqual(body["underlying"], "BTC")
self.assertEqual(body["perp_underlying"], "ETH")
self.assertEqual(body["exit_mode"], "profit")
self.assertTrue(body["option_primary"])
self.assertAlmostEqual(float(body["option_perp_ratio"]), 2.0)
self.assertIsNone(validate_body_matches_mode(body))
bad = dict(body, opt_inst_id="ETH-USD-260831-2000-C")
self.assertIn("BTC", validate_body_matches_mode(bad) or "")
def test_size_cross_notional_half(self):
# 0.1 BTC @ 100k → 10k U 期权名义; k=0.5 → 5k ETH 名义 @ 2500 → 2 ETH → 200 张(cs=0.01)
sz = size_cross_notional(
option_coin_qty=0.1,
option_index=100_000,
perp_index=2500,
contract_size=0.01,
notional_k=0.5,
)
self.assertTrue(sz["ok"])
self.assertAlmostEqual(sz["option_notional_usdt"], 10_000.0)
self.assertAlmostEqual(sz["perp_notional_usdt"], 5_000.0)
self.assertAlmostEqual(sz["perp_coin"], 2.0)
self.assertAlmostEqual(sz["contracts"], 200.0)
def test_size_from_premium_cross_matches_notional(self):
# ask=0.01 BTC → 预算 1 BTC 权利金(币); usable 0.95 → coin=0.95 → sheets
# 用整张回写后再跨币
sized = size_from_premium(
premium_budget=1000, # 若 ask 用币报价则预算单位需一致;此处用大预算保证 ≥1 张
ask=0.01,
ct_mult=0.01,
ratio=2,
contract_size=0.01,
option_index=100_000,
perp_index=2_500,
)
self.assertTrue(sized["ok"], sized.get("msg"))
self.assertTrue(sized.get("cross_asset") or sized.get("option_notional_usdt"))
# coin_qty = sheets * 0.01; 名义路径
self.assertGreater(float(sized["contracts"]), 0)
def test_contracts_from_sheets_same_vs_cross(self):
same = contracts_from_option_sheets(
sheets=10, ct_mult=0.01, ratio=2, contract_size=0.01
)
self.assertTrue(same["ok"])
# 0.1 币 / 2 / 0.01 = 5 张
self.assertAlmostEqual(same["contracts"], 5.0)
cross = contracts_from_option_sheets(
sheets=10,
ct_mult=0.01,
ratio=2,
contract_size=0.01,
option_index=100_000,
perp_index=2_500,
)
self.assertTrue(cross["ok"])
# 0.1 BTC * 100k * 0.5 / 2500 / 0.01 = 200
self.assertAlmostEqual(cross["contracts"], 200.0)
def test_combo_net_usdt_not_mix_units(self):
net = estimate_combo_net_pnl(
view_side="long",
strike=90000,
index_px=91000,
ask_open=0.01,
bid=0.012,
sheets=1,
ct_mult=0.01,
perp_direction="short",
perp_entry=2500,
perp_mark=2450,
contracts=10,
contract_size=0.01,
fee=0.0,
option_fee=0.0,
)
# opt_net 为币;perp_net 为 U;禁止直接用 net['net'] 出门控
u = combo_net_usdt(net, option_index=91000)
self.assertIsNotNone(u)
self.assertAlmostEqual(u, float(net["opt_net"]) * 91000 + float(net["perp_net"]), places=4)
def test_public_payload(self):
with patch.dict(os.environ, {"HEDGE_PO_MODE": "eth"}):
clamp_hedge_po_mode_on_startup()
p = public_mode_payload()
self.assertEqual(p["po_mode"], MODE_ETH)
self.assertEqual(p["exit_mode"], "profit")
self.assertIn("子账户", p["instance_rule"])
def test_resolve_option_index_cross_no_perp_fallback(self):
# 跨币缺指数时不得用 ETH 永续价折 BTC 权利金(曾导致 0.014×2476≈34U)
from lib.hedge_plan.hedge_plan_live_positions_lib import resolve_option_index_px
with patch(
"lib.exchange.okx_public_http_lib.fetch_index_px",
side_effect=RuntimeError("no net"),
):
px = resolve_option_index_px(
{},
"BTC",
ex_opt=None,
cross_asset=True,
perp_fallback=2476.0,
)
self.assertIsNone(px)
with patch(
"lib.exchange.okx_public_http_lib.fetch_index_px",
return_value=91000.0,
):
px = resolve_option_index_px(
{},
"BTC",
cross_asset=True,
perp_fallback=2476.0,
)
self.assertAlmostEqual(px, 91000.0)
# 同币允许永续价兜底
with patch(
"lib.exchange.okx_public_http_lib.fetch_index_px",
side_effect=RuntimeError("no net"),
):
px = resolve_option_index_px(
{},
"ETH",
cross_asset=False,
perp_fallback=2476.0,
)
self.assertAlmostEqual(px, 2476.0)
def test_settle_coin_option_returns_coin_units(self):
from lib.hedge_plan.hedge_plan_settle_lib import settle_option_leg_at_spot
# BTC put ITM: K=90000 S=80000 → 币内在 (90000-80000)/80000=0.125; 1张×0.01=0.00125币
# 权利金 0.01 币 → 净利 0.00125-0.01 = -0.00875 币(非 ×8万的美元差)
pnl = settle_option_leg_at_spot(
{
"inst_id": "BTC-USD-261018-90000-P",
"opt_type": "P",
"strike": 90000,
"size": 1,
"ct_mult": 0.01,
"premium": 0.01,
},
80000.0,
)
self.assertAlmostEqual(pnl, 0.00125 - 0.01, places=6)
def test_size_from_premium_preview_needs_dual_index(self):
# 无双指数 → 同币路径;有双指数 → 跨币张数
same = size_from_premium(
premium_budget=1000,
ask=0.01,
ct_mult=0.01,
ratio=2,
contract_size=0.01,
)
cross = size_from_premium(
premium_budget=1000,
ask=0.01,
ct_mult=0.01,
ratio=2,
contract_size=0.01,
option_index=100_000,
perp_index=2_500,
)
self.assertTrue(same["ok"] and cross["ok"])
self.assertGreater(float(cross["contracts"]), float(same["contracts"]))
if __name__ == "__main__":
unittest.main()