"""半自动出场与参数。""" from __future__ import annotations from app.strategy.selection import list_otm_strikes, pick_otm_strike from app.strategy.semi_auto import ( REASON_PERP_NET, REASON_POINTS, check_semi_exits, effective_min_leverage, ) def test_semi_points_long_needs_net_positive() -> None: # 目标 = 行权价 1800 + 50 = 1850;到点但净利≤0 → 不平 d = check_semi_exits( net_pnl=-1.0, strike=1800, index_px=1850, view_side="long", option_move_points=50, perp_exit_unit=5, risk_k=1, ) assert d.should_close is False assert "净利≤0" in d.detail d2 = check_semi_exits( net_pnl=1.0, strike=1800, index_px=1850, view_side="long", option_move_points=50, perp_exit_unit=5, risk_k=1, ) assert d2.should_close is True assert d2.reason == REASON_POINTS assert d2.target_index == 1850.0 def test_semi_points_uses_strike_not_spot() -> None: # 现价 1915、K1930、+50 → 目标 1980;现价未到则不平(净利勿触达锁定) d = check_semi_exits( net_pnl=1.0, strike=1930, index_px=1915, view_side="long", option_move_points=50, perp_exit_unit=5, risk_k=1, ) assert d.should_close is False assert d.target_index == 1980.0 d2 = check_semi_exits( net_pnl=1.0, strike=1930, index_px=1980, view_side="long", option_move_points=50, perp_exit_unit=5, risk_k=1, ) assert d2.should_close is True assert d2.reason == REASON_POINTS def test_semi_points_short() -> None: # Put:目标 = K − 50 d = check_semi_exits( net_pnl=2.0, strike=1800, index_px=1750, view_side="short", option_move_points=50, perp_exit_unit=5, risk_k=1, ) assert d.should_close is True assert d.reason == REASON_POINTS assert d.target_index == 1750.0 def test_semi_net_exit_with_k() -> None: # 未到点,但净利 ≥ 5×2=10(永续锁定) d = check_semi_exits( net_pnl=10.0, strike=1800, index_px=1810, view_side="long", option_move_points=50, perp_exit_unit=5, risk_k=2, ) assert d.should_close is True assert d.reason == REASON_PERP_NET assert d.net_target == 10.0 def test_semi_not_yet() -> None: d = check_semi_exits( net_pnl=3.0, strike=1800, index_px=1820, view_side="long", option_move_points=50, perp_exit_unit=5, risk_k=1, ) assert d.should_close is False def test_otm_leverage_floor() -> None: assert effective_min_leverage("otm", 100) == 180 assert effective_min_leverage("otm", 200) == 200 assert effective_min_leverage("itm", 100) == 100 def test_pick_otm_within_offset() -> None: strikes = [1800.0, 1825.0, 1850.0, 1875.0] # Call 虚值:标的 1830 → 1850(20点)在 25 内;1875 超 k = pick_otm_strike(strikes, 1830, option_side="call", max_offset=25) assert k == 1850.0 assert ( pick_otm_strike(strikes, 1830, option_side="call", max_offset=15) is None ) # Put 虚值 k2 = pick_otm_strike(strikes, 1830, option_side="put", max_offset=30) assert k2 == 1825.0 def test_list_otm_strikes_near_to_far() -> None: # 现价 1917 → Call 虚值 1920/1930/1940(偏离≤25),近→远 strikes = [1910.0, 1920.0, 1930.0, 1940.0, 1950.0] ks = list_otm_strikes(strikes, 1917.0, option_side="call", max_offset=25) assert ks == [1920.0, 1930.0, 1940.0]