"""永期「以期权为主」定仓/方向/目标位/校验.""" import unittest from lib.hedge_plan.hedge_plan_option_primary_lib import ( PREMIUM_EXEC_FACTOR, build_option_primary_preview, estimate_combo_net_pnl, floor2, opt_type_for_view, option_bid_liquidity_ok, perp_direction_for_view, pick_option_primary_candidate, size_from_premium, target_hit, validate_option_primary_start, validate_option_primary_watch, ) from lib.hedge_plan.hedge_plan_orders_lib import build_po_path_plan, validate_start_body class TestOptionPrimary(unittest.TestCase): def test_direction_mapping(self): self.assertEqual(opt_type_for_view("long"), "C") self.assertEqual(opt_type_for_view("short"), "P") self.assertEqual(perp_direction_for_view("long"), "short") self.assertEqual(perp_direction_for_view("short"), "long") def test_size_from_premium_095_and_eth_2dp(self): # ask=10 → 1 ETH 成本 10U; 预算 100 → usable 95 → eth=9.5 → sheets=950 (ct=0.01) sized = size_from_premium( premium_budget=100, ask=10, ct_mult=0.01, ratio=2, contract_size=0.01, ) self.assertTrue(sized["ok"]) self.assertAlmostEqual(sized["usable_premium"], 95.0) self.assertEqual(sized["eth_qty"], 9.5) self.assertEqual(sized["sheets"], 950.0) # perp_eth = 9.5/2=4.75; contracts=4.75/0.01=475 self.assertAlmostEqual(sized["contracts"], 475.0) self.assertEqual(PREMIUM_EXEC_FACTOR, 0.95) def test_floor2(self): self.assertEqual(floor2(1.239), 1.23) self.assertEqual(floor2(0.009), 0.0) def test_target_hit(self): self.assertTrue(target_hit(view_side="long", index_px=1950, strike=1900, points=50)) self.assertFalse(target_hit(view_side="long", index_px=1949, strike=1900, points=50)) self.assertTrue(target_hit(view_side="short", index_px=1850, strike=1900, points=50)) self.assertFalse(target_hit(view_side="short", index_px=1851, strike=1900, points=50)) self.assertFalse(target_hit(view_side="long", index_px=1900, strike=1900, points=0)) def test_bid_liquidity(self): ok, _ = option_bid_liquidity_ok(1.2, 10, need_sheets=5) self.assertTrue(ok) ok2, msg = option_bid_liquidity_ok(None, 10, need_sheets=1) self.assertFalse(ok2) self.assertIn("买一", msg) def test_net_pnl_uses_buy_fee_for_sell(self): net = estimate_combo_net_pnl( view_side="long", strike=1900, index_px=1950, ask_open=20, bid=30, sheets=2, ct_mult=0.01, perp_direction="short", perp_entry=1900, perp_mark=1950, contracts=10, contract_size=0.01, fee=0.001, ) # opt: proceeds=30*2*0.01=0.6; premium=0.4; fees=0.0004+0.0006; opt_net=0.6-0.4-0.001=0.199 self.assertIn("net", net) self.assertEqual(net["fee_rate"], 0.001) def test_path_option_primary_no_tpsl_options_first(self): path = build_po_path_plan( { "option_primary": True, "direction": "long", "opt_inst_id": "ETH-USD-260831-1900-C", "sheets": 2, "exchange_symbol": "ETH/USDT:USDT", "contracts": 1, } ) self.assertEqual(path[0]["step"], "options_buy_limit") self.assertEqual(path[1]["direction"], "short") self.assertFalse(path[1]["attach_tpsl"]) def test_validate_option_primary_watch_and_start(self): watch_body = { "option_primary": True, "watch_entry": 1, "direction": "long", "exchange_symbol": "ETH/USDT:USDT", "premium_budget": 100, "option_target_points": 50, "perp_target_points": 30, "option_perp_ratio": 4, "option_leverage": 200, "moneyness": "otm", } self.assertIsNone(validate_option_primary_watch(watch_body)) self.assertIsNone(validate_start_body("perp_options", watch_body)) body = { "option_primary": True, "watch_entry": 0, "direction": "long", "contracts": 1, "opt_inst_id": "ETH-USD-260831-1900-C", "opt_type": "C", "sheets": 2, "exchange_symbol": "ETH/USDT:USDT", "premium_budget": 100, "option_target_points": 50, "perp_target_points": 30, "option_perp_ratio": 2, "strike": 1900, "index_px": 1905, "ask": 10, "moneyness": "atm", "strike_interval": 15, "min_option_hours": 36, "hours_to_expiry": 40, "option_leverage": 100, } self.assertIsNone(validate_option_primary_start(body)) self.assertIsNone(validate_start_body("perp_options", body)) bad = dict(body, opt_type="P") self.assertIsNotNone(validate_start_body("perp_options", bad)) def test_pick_candidate_respects_leverage(self): chain = { "index_px": 1900, "expiries": [ { "exp_time": 9_999_999_999_999, "contracts": [ {"inst_id": "LOW", "opt_type": "C", "strike": 1920, "ask": 20, "ct_mult": 0.01}, {"inst_id": "OK", "opt_type": "C", "strike": 1925, "ask": 8, "ct_mult": 0.01}, ], } ], } # 1900/20=95 < 200; 1900/8=237.5 ≥ 200 picked = pick_option_primary_candidate( chain, direction="long", moneyness="otm", strike_interval=50, min_hours=1, min_opt_leverage=200, ) self.assertIsNotNone(picked) self.assertEqual(picked["inst_id"], "OK") def test_preview_builds_scenarios(self): body = { "direction": "long", "strike": 1900, "option_target_points": 50, "perp_target_points": 30, "ask": 20, "sheets": 10, "ct_mult": 0.01, "contracts": 5, "contract_size": 0.01, "entry": 1900, "index_px": 1900, "premium_budget": 100, } out = build_option_primary_preview(body) self.assertTrue(out["option_primary"]) self.assertEqual(len(out["scenarios"]), 2) def test_hours_to_expiry_from_ms(self): from lib.hedge_plan.hedge_plan_option_primary_lib import hours_to_expiry_from_ms now = 1_700_000_000_000.0 # +40h in ms h = hours_to_expiry_from_ms(now + 40 * 3600 * 1000, now_ms=now) self.assertAlmostEqual(h, 40.0, places=3) if __name__ == "__main__": unittest.main()