Extend semi-auto with ITM/ATM/OTM, offset, and 1:4 sizing.

Moneyness and perp:option units are plan-only; OTM enforces leverage >=180.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-08 11:42:57 +08:00
parent 4585dba3c3
commit 457838ef64
17 changed files with 428 additions and 41 deletions
+66 -5
View File
@@ -46,6 +46,53 @@ def pick_atm_strike(strikes: list[float], mark_px: float) -> float | None:
return min(strikes, key=lambda s: (abs(s - mark_px), s))
def pick_otm_strike(
strikes: list[float],
mark_px: float,
*,
option_side: str,
max_offset: float,
) -> float | None:
"""
虚值:Call K>S、Put K<S;取最接近标的且 |KS|≤max_offset 的一档。
"""
if not strikes or mark_px <= 0:
return None
side = (option_side or "").strip().lower()
spot = float(mark_px)
cap = max(0.0, float(max_offset))
if side == "call":
cands = [
float(s)
for s in strikes
if float(s) > spot + 1e-9 and float(s) - spot <= cap + 1e-9
]
elif side == "put":
cands = [
float(s)
for s in strikes
if float(s) < spot - 1e-9 and spot - float(s) <= cap + 1e-9
]
else:
return None
if not cands:
return None
return min(cands, key=lambda s: (abs(s - spot), s))
def is_otm(*, option_side: str, strike: float, mark_px: float) -> bool:
if mark_px <= 0:
return False
side = (option_side or "").strip().lower()
k = float(strike)
s = float(mark_px)
if side == "call":
return k > s + 1e-9
if side == "put":
return k < s - 1e-9
return False
def pick_itm_or_atm_strike(
strikes: list[float],
mark_px: float,
@@ -166,11 +213,14 @@ def select_option_pair(
min_hours: float | None = None,
now: datetime | None = None,
option_side: str | None = None,
moneyness: str | None = None,
otm_max_offset: float | None = None,
) -> OptionPair | None:
"""
选到期 + 行权价。
option_side 为 call/put 时:按实值/平值选档(固定方向模式);
option_side 为 call/put 时:按 moneyness 选档(默认实值/平值,兼容固定方向);
否则仍选 ATM(现有规则)。
moneyness: itm | atm | otm(仅半自动传入 otm/atm)。
"""
complete = _complete_by_expiry(contracts)
if not complete:
@@ -194,13 +244,24 @@ def select_option_pair(
ymd = eligible[0]
ems, strikes_map = complete[ymd]
keys = list(strikes_map.keys())
side = (option_side or "").strip().lower() or None
mny = (moneyness or "").strip().lower() or None
if side in ("call", "put"):
strike = pick_itm_or_atm_strike(
list(strikes_map.keys()), mark_px, option_side=side
)
if mny == "otm":
strike = pick_otm_strike(
keys,
mark_px,
option_side=side,
max_offset=float(otm_max_offset or 0),
)
elif mny == "atm":
strike = pick_atm_strike(keys, mark_px)
else:
# itm 或未指定:实值/平值(固定方向默认)
strike = pick_itm_or_atm_strike(keys, mark_px, option_side=side)
else:
strike = pick_atm_strike(list(strikes_map.keys()), mark_px)
strike = pick_atm_strike(keys, mark_px)
if strike is None:
return None
legs = strikes_map[strike]