626c9c1323
Co-authored-by: Cursor <cursoragent@cursor.com>
138 lines
3.6 KiB
Python
138 lines
3.6 KiB
Python
"""半自动出场与参数。"""
|
||
|
||
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]
|