Let semi-auto reopen nearest expiry; ignore one-expiry-per-day.

Far quarterly skips were masking usable near OTM quotes like 260810.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-08 14:48:07 +08:00
parent f1e3d5527a
commit 5ea54db43b
3 changed files with 42 additions and 21 deletions
+6 -1
View File
@@ -1186,7 +1186,12 @@ class StrategyEngine:
one_expiry_per_day = self.ledger.get_setting_bool(
"one_expiry_per_day", s.one_expiry_per_day
)
if pick is not None and one_expiry_per_day:
# 半自动人工授权:允许同到期再开;全自动仍受一日一到期约束
if (
pick is not None
and one_expiry_per_day
and not is_semi_auto(self.ledger)
):
from .clock import (
expiry_blocked_by_one_per_day,
used_expiry_ymds,
+34 -20
View File
@@ -89,10 +89,10 @@ def _as_bool_setting(raw: str | None, default: bool) -> bool:
return str(raw).strip().lower() in ("1", "true", "yes", "on")
def _skip_expiry_ymds_for_next() -> set[str]:
def _skip_expiry_ymds_for_next(*, include_used: bool = True) -> set[str]:
"""
空仓选约/监控应跳过的到期日:
- 历史上已开过该到期(one_expiry_per_day,跨日)
- 历史上已开过该到期(one_expiry_per_day,跨日);半自动可关
- 仍有待结算残留期权的到期档(该档已「完成」开平,盯下一档)
"""
skip: set[str] = set()
@@ -102,12 +102,13 @@ def _skip_expiry_ymds_for_next() -> set[str]:
s = get_settings()
db = get_db()
one_exp_day = _as_bool_setting(
db.get_setting("one_expiry_per_day", str(s.one_expiry_per_day)),
s.one_expiry_per_day,
)
if one_exp_day:
skip |= used_expiry_ymds(db)
if include_used:
one_exp_day = _as_bool_setting(
db.get_setting("one_expiry_per_day", str(s.one_expiry_per_day)),
s.one_expiry_per_day,
)
if one_exp_day:
skip |= used_expiry_ymds(db)
skip |= pending_residual_expiry_ymds(db)
except Exception:
logger.exception("skip-expiry lookup failed; continue without skip")
@@ -765,14 +766,25 @@ class StrategySession:
self._last_pick_fail = f"无剩余≥{min_hours:g}h 的到期"
return None
skip_expiries = _skip_expiry_ymds_for_next()
# 半自动:人工授权可重复开同到期,不受 one_expiry_per_day 挡住;
# 仍跳过有残余腿的到期。全自动保持原「一日一到期」。
skip_expiries = _skip_expiry_ymds_for_next(include_used=not semi_on)
first_skip = ""
last_skip = ""
def _note_skip(msg: str) -> None:
nonlocal first_skip, last_skip
last_skip = msg
if not first_skip:
first_skip = msg
for ymd in eligible:
if ymd in skip_expiries:
_note_skip(f"{ymd} 有残余期权(跳过该到期)")
logger.info(
"skip expiry=%s: used today and/or residual pending",
"skip expiry=%s: residual pending (semi=%s)",
ymd,
semi_on,
)
continue
pair = select_option_pair(
@@ -785,7 +797,7 @@ class StrategySession:
)
if pair is None:
if semi_on and semi_mny == "otm":
last_skip = (
_note_skip(
f"{ymd}{opt_side_hint or '?'}虚值"
f"(偏离≤{float(semi_otm_off or 0):g}"
)
@@ -797,7 +809,7 @@ class StrategySession:
underlying,
)
else:
last_skip = f"{ymd} 无合格行权价"
_note_skip(f"{ymd} 无合格行权价")
continue
if fixed_on:
from .selection import is_otm
@@ -809,13 +821,13 @@ class StrategySession:
strike=pair.strike,
mark_px=underlying,
):
last_skip = f"{ymd} K{pair.strike:g} 非虚值"
_note_skip(f"{ymd} K{pair.strike:g} 非虚值")
continue
if (
atm_open_offset(pair.strike, underlying)
> float(semi_otm_off or 0) + 1e-9
):
last_skip = (
_note_skip(
f"{ymd} K{pair.strike:g} 偏离>"
f"{float(semi_otm_off or 0):g}"
)
@@ -828,7 +840,7 @@ class StrategySession:
strike=pair.strike,
mark_px=underlying,
):
last_skip = f"{ymd} K{pair.strike:g} 非实值/平值"
_note_skip(f"{ymd} K{pair.strike:g} 非实值/平值")
logger.info(
"skip expiry=%s strike=%.0f not ITM/ATM for %s mark=%.2f",
ymd,
@@ -845,7 +857,7 @@ class StrategySession:
max_offset=max_atm_off,
enabled=atm_off_on,
):
last_skip = f"{ymd} ATM偏离{offset:.1f}>{max_atm_off:g}"
_note_skip(f"{ymd} ATM偏离{offset:.1f}>{max_atm_off:g}")
logger.info(
"skip expiry=%s strike=%.0f atm_offset=%.1f > max=%.1f",
ymd,
@@ -878,13 +890,13 @@ class StrategySession:
need = "Call" if (opt_side_hint == "call") else (
"Put" if opt_side_hint == "put" else "Call/Put"
)
last_skip = f"{ymd} K{pair.strike:g}{need}卖一"
_note_skip(f"{ymd} K{pair.strike:g}{need}卖一")
continue
opt_ask = sig.call_ask if sig.option_side == "call" else sig.put_ask
lev = option_leverage(underlying, opt_ask)
hours_left = hours_until_expiry(ymd, expiry_ms=pair.expiry_ms)
if lev is None or lev + 1e-9 < min_lev:
last_skip = (
_note_skip(
f"{ymd} {sig.option_side.upper()}@{pair.strike:g} "
f"杠杆{(f'{lev:.0f}x' if lev else 'n/a')}<{min_lev:g}x"
)
@@ -921,14 +933,16 @@ class StrategySession:
underlying_px=underlying,
hedge_mode="perp_option",
)
if last_skip:
# 报最近到期(列表最前)的原因,避免只显示远月 261225 造成误会
why = first_skip or last_skip
if why:
hint = ""
if semi_on:
hint = (
f"(半自动{opt_side_hint or '?'}·"
f"{semi_mny or '?'}·≥{min_lev:g}x·≥{min_hours:g}h"
)
self._last_pick_fail = f"最近跳过: {last_skip}{hint}"
self._last_pick_fail = f"{why}{hint}"
else:
self._last_pick_fail = "合格到期均被跳过(一日一到期/残余等)"
return None
+2
View File
@@ -49,6 +49,8 @@
**以损定仓(半自动)**:系统设置 `sizing_mode=risk_based` 时,开仓前用**盘口卖一**估算单位成本 → 预算反推 k →
`期权量 = 期权单位 × k``永续量 = 永续单位 × k`(配比不变)。首页「半自动 · 本单」下方按当前报价档卖一实时预览开仓量。
选约到期与报价列表一致:取剩余 ≥ 最短 h 的**最近**到期。半自动**不受**全自动「一日一到期」限制(仍可跳过有残余腿的到期)。
---
## 3. 出场