Revise strategy: TTM+ATM+leverage option pick, % exit, perp leverage/margin.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-25 08:38:03 +08:00
parent 7db4c9c8a3
commit 3957a83761
16 changed files with 532 additions and 261 deletions
+84 -29
View File
@@ -1,4 +1,4 @@
"""合约选择:次日 16:00(上海)到期 + ATM 行权价(暂定默认,待拍板可改)"""
"""合约选择:剩余时长过滤 + ATM 平值期权"""
from __future__ import annotations
@@ -42,13 +42,15 @@ def expiry_ms_from_ymd(ymd: str) -> int:
return int(dt.timestamp() * 1000)
def hours_until_expiry(ymd: str, now: datetime | None = None) -> float:
"""距到期剩余小时(可为负)。"""
n = (now or datetime.now(tz=_SH)).astimezone(_SH)
left_ms = expiry_ms_from_ymd(ymd) - int(n.timestamp() * 1000)
return left_ms / 3_600_000.0
def next_session_expiry_ymd(now: datetime | None = None) -> str:
"""
业务约定:开仓选「次日 16:00」到期。
- 上海时间 >= 当日 16:00:目标到期日 = 次日
- 上海时间 < 当日 16:00:目标到期日 = 当日(当日 16:00 到期仍可用作盘口对齐/预热)
正式开仓窗从当日 16:00 起,届时「次日」即日历次日。
"""
"""兼容旧逻辑:次日/当日 16:00 到期键(展示/测试用)。"""
now_sh = (now or datetime.now(tz=_SH)).astimezone(_SH)
open_today = now_sh.replace(hour=16, minute=0, second=0, microsecond=0)
if now_sh >= open_today:
@@ -64,30 +66,19 @@ def pick_atm_strike(strikes: list[float], mark_px: float) -> float | None:
return min(strikes, key=lambda s: (abs(s - mark_px), s))
def select_option_pair(
def _complete_by_expiry(
instruments: list[dict[str, Any]],
*,
mark_px: float,
expiry_ymd: str | None = None,
now: datetime | None = None,
) -> OptionPair | None:
"""
从 live 合约列表中选出:目标到期日 + ATM 同行权价 Call/Put。
行权价规则暂定 ATM(最接近标记/指数价);待拍板后可替换。
"""
ymd = expiry_ymd or next_session_expiry_ymd(now)
by_strike: dict[float, dict[str, str]] = {}
) -> dict[str, dict[float, dict[str, str]]]:
"""expiry_ymd -> strike -> {C|P: instId},仅完整 Call+Put。"""
by_exp: dict[str, dict[float, dict[str, str]]] = {}
for row in instruments:
if not isinstance(row, dict):
continue
state = str(row.get("state") or "live").lower()
if state and state != "live":
continue
inst_id = str(row.get("instId") or "")
y, stk, opt = parse_option_inst_id(inst_id)
if y is None or stk is None or opt is None:
exp = safe_float(row.get("expTime"))
if exp:
@@ -96,20 +87,77 @@ def select_option_pair(
stk = safe_float(row.get("stk"))
opt_raw = str(row.get("optType") or "").upper()
opt = opt_raw if opt_raw in ("C", "P") else None
if not inst_id or y != ymd or stk is None or opt not in ("C", "P"):
if not inst_id or not y or stk is None or opt not in ("C", "P"):
continue
by_strike.setdefault(float(stk), {})[opt] = inst_id
by_exp.setdefault(y, {}).setdefault(float(stk), {})[opt] = inst_id
complete = {s: v for s, v in by_strike.items() if "C" in v and "P" in v}
out: dict[str, dict[float, dict[str, str]]] = {}
for ymd, strikes in by_exp.items():
complete = {s: v for s, v in strikes.items() if "C" in v and "P" in v}
if complete:
out[ymd] = complete
return out
def list_eligible_expiry_ymds(
instruments: list[dict[str, Any]],
*,
min_hours: float,
now: datetime | None = None,
) -> list[str]:
"""剩余时间 >= min_hours 的到期日,由近到远。"""
complete = _complete_by_expiry(instruments)
eligible = [
ymd
for ymd in complete
if hours_until_expiry(ymd, now) + 1e-9 >= float(min_hours)
]
return sorted(eligible, key=lambda y: expiry_ms_from_ymd(y))
def select_option_pair(
instruments: list[dict[str, Any]],
*,
mark_px: float,
expiry_ymd: str | None = None,
min_hours: float | None = None,
now: datetime | None = None,
) -> OptionPair | None:
"""
选 ATM Call/Put。
- 若给 expiry_ymd:在该到期日选平值。
- 若给 min_hours:选「剩余时长合格」中最近到期日的平值。
- 否则回退 next_session_expiry_ymd。
"""
complete = _complete_by_expiry(instruments)
if not complete:
return None
atm = pick_atm_strike(list(complete.keys()), mark_px)
if expiry_ymd:
ymd = expiry_ymd
if ymd not in complete:
return None
elif min_hours is not None:
eligible = list_eligible_expiry_ymds(
instruments, min_hours=min_hours, now=now
)
if not eligible:
return None
ymd = eligible[0]
else:
ymd = next_session_expiry_ymd(now)
if ymd not in complete:
# 回退到最近合格到期
eligible = list_eligible_expiry_ymds(instruments, min_hours=0, now=now)
if not eligible:
return None
ymd = eligible[0]
strikes_map = complete[ymd]
atm = pick_atm_strike(list(strikes_map.keys()), mark_px)
if atm is None:
return None
legs = complete[atm]
legs = strikes_map[atm]
return OptionPair(
expiry_ymd=ymd,
expiry_ms=expiry_ms_from_ymd(ymd),
@@ -117,3 +165,10 @@ def select_option_pair(
call_inst_id=legs["C"],
put_inst_id=legs["P"],
)
def option_leverage(underlying_px: float, premium_ask: float) -> float | None:
"""现价 / 卖一权利金。"""
if underlying_px <= 0 or premium_ask is None or premium_ask <= 0:
return None
return float(underlying_px) / float(premium_ask)