Add one-expiry-per-day open rhythm gate (default on).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
_SH = ZoneInfo("Asia/Shanghai")
|
||||
@@ -38,3 +39,34 @@ def can_open_new(
|
||||
|
||||
def group_date_ymd(now: datetime | None = None) -> str:
|
||||
return window_key(now)
|
||||
|
||||
|
||||
def used_expiry_ymds_for_day(db: Any, now: datetime | None = None) -> set[str]:
|
||||
"""
|
||||
上海日历日已开过的期权到期日(groups.expiry_ymd,YYMMDD)。
|
||||
按当日组号 G-{YYYYMMDD}-% 统计;含已平仓,用于「同到期一天只开一次」。
|
||||
"""
|
||||
wkey = window_key(now)
|
||||
rows = db.fetchall(
|
||||
"SELECT DISTINCT expiry_ymd FROM groups WHERE group_id LIKE ?",
|
||||
(f"G-{wkey}-%",),
|
||||
)
|
||||
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],
|
||||
*,
|
||||
enabled: bool = True,
|
||||
) -> bool:
|
||||
"""开启时:候选到期已在当日用过则拦截。"""
|
||||
if not enabled:
|
||||
return False
|
||||
y = str(expiry_ymd or "").strip()
|
||||
return bool(y and y in used)
|
||||
|
||||
@@ -82,6 +82,9 @@ class StrategyEngine:
|
||||
)
|
||||
rest_sec = self.ledger.get_setting_int("rest_seconds", s.rest_seconds)
|
||||
skip_weekends = self.ledger.get_setting_bool("skip_weekends", s.skip_weekends)
|
||||
one_expiry_per_day = self.ledger.get_setting_bool(
|
||||
"one_expiry_per_day", s.one_expiry_per_day
|
||||
)
|
||||
leverage = self.ledger.get_setting_float("leverage", s.leverage)
|
||||
perp_mm = str(
|
||||
self.ledger.get_setting_str("perp_margin_mode", s.perp_margin_mode)
|
||||
@@ -213,6 +216,7 @@ class StrategyEngine:
|
||||
"rest_left_sec": rest_left,
|
||||
"rest_seconds": rest_sec,
|
||||
"skip_weekends": skip_weekends,
|
||||
"one_expiry_per_day": one_expiry_per_day,
|
||||
"exit_mode": exit_mode,
|
||||
"net_profit_target": net_target,
|
||||
"premium_exit_multiple": prem_mult,
|
||||
@@ -824,6 +828,27 @@ class StrategyEngine:
|
||||
|
||||
self._set_state(phase="wait_signal")
|
||||
pick = await get_session().pick_for_open_async()
|
||||
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:
|
||||
from .clock import (
|
||||
expiry_blocked_by_one_per_day,
|
||||
used_expiry_ymds_for_day,
|
||||
)
|
||||
|
||||
used = used_expiry_ymds_for_day(self.db)
|
||||
if expiry_blocked_by_one_per_day(
|
||||
pick.pair.expiry_ymd, used, enabled=True
|
||||
):
|
||||
self._set_state(
|
||||
phase="idle",
|
||||
last_error=(
|
||||
f"同到期日一天只开一次:今日已用过 {pick.pair.expiry_ymd},"
|
||||
"请等下一到期日"
|
||||
),
|
||||
)
|
||||
return
|
||||
if pick is None:
|
||||
try:
|
||||
from .open_capacity import assess_open_capacity, funds_gate_blocks
|
||||
|
||||
@@ -295,7 +295,29 @@ 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")
|
||||
|
||||
for ymd in eligible:
|
||||
if one_exp_day and ymd in used_expiries:
|
||||
logger.info(
|
||||
"skip expiry=%s: already used today (one_expiry_per_day)",
|
||||
ymd,
|
||||
)
|
||||
continue
|
||||
pair = select_option_pair(
|
||||
contracts,
|
||||
mark_px=underlying,
|
||||
|
||||
Reference in New Issue
Block a user