Files
2026-08-08 15:00:02 +08:00

138 lines
3.6 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.
"""半自动出场与参数。"""
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 → 185020点)在 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]