Skip used/residual expiries so ATM monitors the next day.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-02 10:31:07 +08:00
parent 05520f82bd
commit 9427d9c429
3 changed files with 132 additions and 23 deletions
+14
View File
@@ -59,6 +59,20 @@ def used_expiry_ymds_for_day(db: Any, now: datetime | None = None) -> set[str]:
return out
def pending_residual_expiry_ymds(db: Any) -> set[str]:
"""待到期结算的残留期权到期日(YYMMDD);监控/下一组开仓应跳过这些档。"""
rows = db.fetchall(
"""SELECT DISTINCT expiry_ymd FROM residual_options
WHERE status='pending' AND expiry_ymd IS NOT NULL AND TRIM(expiry_ymd) != ''"""
)
out: set[str] = set()
for r in rows or []:
y = str(r["expiry_ymd"] or "").strip()
if y:
out.add(y)
return out
def expiry_blocked_by_one_per_day(
expiry_ymd: str | None,
used: set[str],
+57 -23
View File
@@ -69,6 +69,31 @@ 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]:
"""
空仓选约/监控应跳过的到期日:
- 当日已开过(one_expiry_per_day
- 仍有待结算残留期权的到期档(该档已「完成」开平,盯下一档)
"""
skip: set[str] = set()
try:
from ..models.db import get_db
from .clock import pending_residual_expiry_ymds, used_expiry_ymds_for_day
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_for_day(db)
skip |= pending_residual_expiry_ymds(db)
except Exception:
logger.exception("skip-expiry lookup failed; continue without skip")
return skip
def _strategy_floats() -> tuple[float, float, float, bool]:
"""min_hours, min_leverage, max_atm_open_offset, atm_open_offset_enabled"""
s = get_settings()
@@ -264,12 +289,32 @@ class StrategySession:
fixed_on, fixed_perp = _fixed_direction()
opt_side = _option_side_for_perp(fixed_perp) if fixed_on else None
contracts = self.ex.list_option_contracts(s.option_inst_family)
pair = select_option_pair(
contracts,
mark_px=float(mark),
min_hours=min_hours,
option_side=opt_side,
)
eligible = list_eligible_expiry_ymds(contracts, min_hours=min_hours)
skip = _skip_expiry_ymds_for_next()
pair = None
for ymd in eligible:
if ymd in skip:
logger.info(
"align skip expiry=%s (used today and/or residual pending)",
ymd,
)
continue
pair = select_option_pair(
contracts,
mark_px=float(mark),
expiry_ymd=ymd,
option_side=opt_side,
)
if pair is not None:
break
if pair is None and eligible:
# 候选都被跳过时仍回退最近合格档,避免盘口空白
pair = select_option_pair(
contracts,
mark_px=float(mark),
expiry_ymd=eligible[0],
option_side=opt_side,
)
if pair is None:
kind = f"实值/平值 {opt_side}" if opt_side else "ATM"
raise RuntimeError(
@@ -295,26 +340,12 @@ class StrategySession:
logger.info("no expiry with hours>=%.1f", min_hours)
return None
used_expiries: set[str] = set()
one_exp_day = bool(s.one_expiry_per_day)
try:
from ..models.db import get_db
from .clock import used_expiry_ymds_for_day
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:
used_expiries = used_expiry_ymds_for_day(db)
except Exception:
logger.exception("one_expiry_per_day lookup failed; continue without skip")
skip_expiries = _skip_expiry_ymds_for_next()
for ymd in eligible:
if one_exp_day and ymd in used_expiries:
if ymd in skip_expiries:
logger.info(
"skip expiry=%s: already used today (one_expiry_per_day)",
"skip expiry=%s: used today and/or residual pending",
ymd,
)
continue
@@ -459,6 +490,9 @@ class StrategySession:
< min_hours
):
return True
skip = _skip_expiry_ymds_for_next()
if str(self._pair.expiry_ymd or "") in skip:
return True
mark = mark_px if mark_px is not None else self._mark_for_atm()
if mark is None or mark <= 0:
return False
+61
View File
@@ -0,0 +1,61 @@
"""空仓选约:跳过当日已用到期与残留待结算到期。"""
from __future__ import annotations
from app.strategy.clock import (
expiry_blocked_by_one_per_day,
pending_residual_expiry_ymds,
used_expiry_ymds_for_day,
)
class _FakeDB:
def __init__(
self,
*,
groups: list[dict] | None = None,
residuals: list[dict] | None = None,
) -> None:
self._groups = groups or []
self._residuals = residuals or []
def fetchall(self, sql: str, params: tuple = ()) -> list[dict]:
s = " ".join(sql.split()).lower()
if "from groups" in s:
like = params[0] if params else ""
prefix = like.replace("%", "")
return [
r
for r in self._groups
if str(r.get("group_id", "")).startswith(prefix)
]
if "from residual_options" in s:
return [r for r in self._residuals if r.get("status") == "pending"]
return []
def test_used_expiry_ymds_for_day() -> None:
db = _FakeDB(
groups=[
{"group_id": "G-20260802-01", "expiry_ymd": "260803"},
{"group_id": "G-20260801-01", "expiry_ymd": "260802"},
]
)
from datetime import datetime
from zoneinfo import ZoneInfo
now = datetime(2026, 8, 2, 12, 0, tzinfo=ZoneInfo("Asia/Shanghai"))
assert used_expiry_ymds_for_day(db, now) == {"260803"}
assert expiry_blocked_by_one_per_day("260803", {"260803"}, enabled=True)
assert not expiry_blocked_by_one_per_day("260804", {"260803"}, enabled=True)
def test_pending_residual_expiry_ymds() -> None:
db = _FakeDB(
residuals=[
{"expiry_ymd": "260803", "status": "pending"},
{"expiry_ymd": "260802", "status": "settled"},
{"expiry_ymd": "", "status": "pending"},
]
)
assert pending_residual_expiry_ymds(db) == {"260803"}