Skip used/residual expiries so ATM monitors the next day.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
Reference in New Issue
Block a user