Anchor semi exit to strike +/- move points; side-by-side semi cards.
Forward target is K+/-N not spot+/-N; UI shows option target and perp net lock. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -978,9 +978,9 @@ class StrategyEngine:
|
||||
rk = 1.0
|
||||
semi_d = check_semi_exits(
|
||||
net_pnl=float(upl.get("net_pnl") or 0),
|
||||
entry_index=(
|
||||
float(upl["entry_index_px"])
|
||||
if upl.get("entry_index_px") is not None
|
||||
strike=(
|
||||
float(upl["strike"])
|
||||
if upl.get("strike") is not None
|
||||
else None
|
||||
),
|
||||
index_px=(
|
||||
@@ -992,6 +992,11 @@ class StrategyEngine:
|
||||
option_move_points=float(sp["option_move_points"]),
|
||||
perp_exit_unit=float(sp["perp_exit_unit"]),
|
||||
risk_k=rk,
|
||||
entry_index=(
|
||||
float(upl["entry_index_px"])
|
||||
if upl.get("entry_index_px") is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
# 复用 ExitDecision 形态
|
||||
from .exits import ExitDecision
|
||||
|
||||
@@ -201,17 +201,18 @@ class SemiExitDecision:
|
||||
def check_semi_exits(
|
||||
*,
|
||||
net_pnl: float,
|
||||
entry_index: float | None,
|
||||
strike: float | None,
|
||||
index_px: float | None,
|
||||
view_side: str,
|
||||
option_move_points: float,
|
||||
perp_exit_unit: float,
|
||||
risk_k: float = 1.0,
|
||||
entry_index: float | None = None,
|
||||
) -> SemiExitDecision:
|
||||
"""
|
||||
顺方向:标的波动达到目标点 且 组合净利>0 → 全平。
|
||||
逆方向兑现:组合净利 ≥ 永续出场基数×k → 全平。
|
||||
流动性在 close_group 内再验;平仓顺序已是先期权后永续。
|
||||
顺方向:指数到达「行权价 ± 波动点」且组合净利>0 → 全平。
|
||||
多/Call:目标 = K + N;空/Put:目标 = K − N(N 为设置的波动点,不是现价±N)。
|
||||
逆方向兑现(永续锁定净利):组合净利 ≥ 净利基数×k → 全平。
|
||||
"""
|
||||
view = (view_side or "long").strip().lower()
|
||||
if view not in ("long", "short"):
|
||||
@@ -221,34 +222,39 @@ def check_semi_exits(
|
||||
net_tgt = max(0.0, float(perp_exit_unit)) * k
|
||||
net = float(net_pnl)
|
||||
|
||||
# 逆方向 / 对冲兑现:净利达标即可离场(不必等点位)
|
||||
# 逆方向 / 永续净利锁定:达标即可离场(不必等点位)
|
||||
if net_tgt > 0 and net + 1e-9 >= net_tgt:
|
||||
return SemiExitDecision(
|
||||
True,
|
||||
REASON_PERP_NET,
|
||||
f"半自动·净利≥{net_tgt:.2f}U(基数×k)",
|
||||
f"半自动·永续净利锁定≥{net_tgt:.2f}U(基数×k)",
|
||||
net_target=net_tgt,
|
||||
)
|
||||
|
||||
if entry_index is None or index_px is None:
|
||||
if index_px is None:
|
||||
return SemiExitDecision(False, "", "缺指数")
|
||||
entry = float(entry_index)
|
||||
idx = float(index_px)
|
||||
if entry <= 0 or idx <= 0 or move <= 0:
|
||||
# 锚定行权价;无 strike 时才回退开仓指数(兼容旧仓)
|
||||
anchor = None
|
||||
if strike is not None and float(strike) > 0:
|
||||
anchor = float(strike)
|
||||
elif entry_index is not None and float(entry_index) > 0:
|
||||
anchor = float(entry_index)
|
||||
if anchor is None or idx <= 0 or move <= 0:
|
||||
return SemiExitDecision(False, "", "点位无效")
|
||||
|
||||
if view == "long":
|
||||
target_idx = entry + move
|
||||
target_idx = anchor + move
|
||||
hit = idx + 1e-9 >= target_idx
|
||||
else:
|
||||
target_idx = entry - move
|
||||
target_idx = anchor - move
|
||||
hit = idx - 1e-9 <= target_idx
|
||||
|
||||
if hit and net > 0:
|
||||
return SemiExitDecision(
|
||||
True,
|
||||
REASON_POINTS,
|
||||
f"半自动·标的到{target_idx:.2f}且组合净利>0",
|
||||
f"半自动·指数到期权目标{target_idx:.2f}(K{anchor:g}±{move:g})且净利>0",
|
||||
target_index=target_idx,
|
||||
net_target=0.0,
|
||||
)
|
||||
@@ -256,13 +262,13 @@ def check_semi_exits(
|
||||
return SemiExitDecision(
|
||||
False,
|
||||
"",
|
||||
f"已到点位{target_idx:.2f}但组合净利≤0({net:.2f}),继续持有",
|
||||
f"已到期权目标{target_idx:.2f}但组合净利≤0({net:.2f}),继续持有",
|
||||
target_index=target_idx,
|
||||
)
|
||||
return SemiExitDecision(
|
||||
False,
|
||||
"",
|
||||
f"未到点位(目标{target_idx:.2f})",
|
||||
f"未到期权目标(K{anchor:g}→{target_idx:.2f})",
|
||||
target_index=target_idx,
|
||||
net_target=net_tgt,
|
||||
)
|
||||
|
||||
@@ -12,10 +12,10 @@ from app.strategy.semi_auto import (
|
||||
|
||||
|
||||
def test_semi_points_long_needs_net_positive() -> None:
|
||||
# 到点但净利≤0 → 不平
|
||||
# 目标 = 行权价 1800 + 50 = 1850;到点但净利≤0 → 不平
|
||||
d = check_semi_exits(
|
||||
net_pnl=-1.0,
|
||||
entry_index=1800,
|
||||
strike=1800,
|
||||
index_px=1850,
|
||||
view_side="long",
|
||||
option_move_points=50,
|
||||
@@ -27,7 +27,7 @@ def test_semi_points_long_needs_net_positive() -> None:
|
||||
|
||||
d2 = check_semi_exits(
|
||||
net_pnl=1.0,
|
||||
entry_index=1800,
|
||||
strike=1800,
|
||||
index_px=1850,
|
||||
view_side="long",
|
||||
option_move_points=50,
|
||||
@@ -36,12 +36,41 @@ def test_semi_points_long_needs_net_positive() -> None:
|
||||
)
|
||||
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=5.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=5.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,
|
||||
entry_index=1800,
|
||||
strike=1800,
|
||||
index_px=1750,
|
||||
view_side="short",
|
||||
option_move_points=50,
|
||||
@@ -50,13 +79,14 @@ def test_semi_points_short() -> None:
|
||||
)
|
||||
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
|
||||
# 未到点,但净利 ≥ 5×2=10(永续锁定)
|
||||
d = check_semi_exits(
|
||||
net_pnl=10.0,
|
||||
entry_index=1800,
|
||||
strike=1800,
|
||||
index_px=1810,
|
||||
view_side="long",
|
||||
option_move_points=50,
|
||||
@@ -71,7 +101,7 @@ def test_semi_net_exit_with_k() -> None:
|
||||
def test_semi_not_yet() -> None:
|
||||
d = check_semi_exits(
|
||||
net_pnl=3.0,
|
||||
entry_index=1800,
|
||||
strike=1800,
|
||||
index_px=1820,
|
||||
view_side="long",
|
||||
option_move_points=50,
|
||||
|
||||
Reference in New Issue
Block a user