Gate one-open-per-expiry by historical expiry_ymd across days.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-03 07:35:44 +08:00
parent 5c334a4d89
commit a10ecf7409
10 changed files with 49 additions and 44 deletions
+3 -3
View File
@@ -77,15 +77,15 @@ async def sim_open_group(
if one_expiry: if one_expiry:
from ..strategy.clock import ( from ..strategy.clock import (
expiry_blocked_by_one_per_day, expiry_blocked_by_one_per_day,
used_expiry_ymds_for_day, used_expiry_ymds,
) )
used = used_expiry_ymds_for_day(get_db()) used = used_expiry_ymds(get_db())
if expiry_blocked_by_one_per_day(pick.pair.expiry_ymd, used, enabled=True): if expiry_blocked_by_one_per_day(pick.pair.expiry_ymd, used, enabled=True):
raise HTTPException( raise HTTPException(
status_code=409, status_code=409,
detail=( detail=(
f"同到期日一天只开一次:今日已用过 {pick.pair.expiry_ymd}" f"每个到期只开一次:已用过 {pick.pair.expiry_ymd}"
"请等下一到期日" "请等下一到期日"
), ),
) )
+1 -1
View File
@@ -67,7 +67,7 @@ class Settings(BaseSettings):
rest_seconds: int = 300 rest_seconds: int = 300
live_order_interval_sec: float = 1.0 # LIVE 私有下单/查单最小间隔(秒) live_order_interval_sec: float = 1.0 # LIVE 私有下单/查单最小间隔(秒)
skip_weekends: bool = True # 上海时区周六日禁止新开仓(已有仓仍可平) skip_weekends: bool = True # 上海时区周六日禁止新开仓(已有仓仍可平)
# 同到期日一天只开一次:当日已开过某 expiry_ymd 后,平仓也不可再开同到期,只能开更远到期 # 每个到期只开一次(跨日):历史上已开过某 expiry_ymd 后不可再开同到期,只能盯下一档
one_expiry_per_day: bool = True one_expiry_per_day: bool = True
leverage: float = 3.0 # 永续杠杆 leverage: float = 3.0 # 永续杠杆
# 永续保证金模式:cross=全仓(默认)| isolated=逐仓;期权仍固定 cashOKX 逐仓/现金) # 永续保证金模式:cross=全仓(默认)| isolated=逐仓;期权仍固定 cashOKX 逐仓/现金)
+11 -7
View File
@@ -41,15 +41,15 @@ def group_date_ymd(now: datetime | None = None) -> str:
return window_key(now) return window_key(now)
def used_expiry_ymds_for_day(db: Any, now: datetime | None = None) -> set[str]: def used_expiry_ymds(db: Any, now: datetime | None = None) -> set[str]:
""" """
海日历日已开过的期权到期日(groups.expiry_ymdYYMMDD)。 历史上已开过的期权到期日(groups.expiry_ymdYYMMDD)。
按当日组号 G-{YYYYMMDD}-% 统计;含已平仓,用于「同到期一天只开一次」 跨日历日:任一已开组(含已平仓)占用该到期后,不可再开同到期,只能盯下一档
""" """
wkey = window_key(now) _ = now
rows = db.fetchall( rows = db.fetchall(
"SELECT DISTINCT expiry_ymd FROM groups WHERE group_id LIKE ?", """SELECT DISTINCT expiry_ymd FROM groups
(f"G-{wkey}-%",), WHERE expiry_ymd IS NOT NULL AND TRIM(expiry_ymd) != ''"""
) )
out: set[str] = set() out: set[str] = set()
for r in rows or []: for r in rows or []:
@@ -59,6 +59,10 @@ def used_expiry_ymds_for_day(db: Any, now: datetime | None = None) -> set[str]:
return out return out
# 兼容旧名(语义已改为跨日/历史到期占用)
used_expiry_ymds_for_day = used_expiry_ymds
def pending_residual_expiry_ymds(db: Any) -> set[str]: def pending_residual_expiry_ymds(db: Any) -> set[str]:
"""待到期结算的残留期权到期日(YYMMDD);监控/下一组开仓应跳过这些档。""" """待到期结算的残留期权到期日(YYMMDD);监控/下一组开仓应跳过这些档。"""
rows = db.fetchall( rows = db.fetchall(
@@ -79,7 +83,7 @@ def expiry_blocked_by_one_per_day(
*, *,
enabled: bool = True, enabled: bool = True,
) -> bool: ) -> bool:
"""开启时:候选到期已在当日用过则拦截。""" """开启时:候选到期已在历史上用过则拦截。"""
if not enabled: if not enabled:
return False return False
y = str(expiry_ymd or "").strip() y = str(expiry_ymd or "").strip()
+3 -3
View File
@@ -873,17 +873,17 @@ class StrategyEngine:
if pick is not None and one_expiry_per_day: if pick is not None and one_expiry_per_day:
from .clock import ( from .clock import (
expiry_blocked_by_one_per_day, expiry_blocked_by_one_per_day,
used_expiry_ymds_for_day, used_expiry_ymds,
) )
used = used_expiry_ymds_for_day(self.db) used = used_expiry_ymds(self.db)
if expiry_blocked_by_one_per_day( if expiry_blocked_by_one_per_day(
pick.pair.expiry_ymd, used, enabled=True pick.pair.expiry_ymd, used, enabled=True
): ):
self._set_state( self._set_state(
phase="idle", phase="idle",
last_error=( last_error=(
f"同到期日一天只开一次:今日已用过 {pick.pair.expiry_ymd}" f"每个到期只开一次:已用过 {pick.pair.expiry_ymd}"
"请等下一到期日" "请等下一到期日"
), ),
) )
+3 -3
View File
@@ -72,13 +72,13 @@ def _as_bool_setting(raw: str | None, default: bool) -> bool:
def _skip_expiry_ymds_for_next() -> set[str]: def _skip_expiry_ymds_for_next() -> set[str]:
""" """
空仓选约/监控应跳过的到期日: 空仓选约/监控应跳过的到期日:
- 当日已开过one_expiry_per_day - 历史上已开过该到期one_expiry_per_day,跨日
- 仍有待结算残留期权的到期档(该档已「完成」开平,盯下一档) - 仍有待结算残留期权的到期档(该档已「完成」开平,盯下一档)
""" """
skip: set[str] = set() skip: set[str] = set()
try: try:
from ..models.db import get_db from ..models.db import get_db
from .clock import pending_residual_expiry_ymds, used_expiry_ymds_for_day from .clock import pending_residual_expiry_ymds, used_expiry_ymds
s = get_settings() s = get_settings()
db = get_db() db = get_db()
@@ -87,7 +87,7 @@ def _skip_expiry_ymds_for_next() -> set[str]:
s.one_expiry_per_day, s.one_expiry_per_day,
) )
if one_exp_day: if one_exp_day:
skip |= used_expiry_ymds_for_day(db) skip |= used_expiry_ymds(db)
skip |= pending_residual_expiry_ymds(db) skip |= pending_residual_expiry_ymds(db)
except Exception: except Exception:
logger.exception("skip-expiry lookup failed; continue without skip") logger.exception("skip-expiry lookup failed; continue without skip")
+10 -9
View File
@@ -152,22 +152,23 @@ def test_one_expiry_per_day(tmp_path, monkeypatch) -> None:
from app.models.db import Database from app.models.db import Database
db = Database(tmp_path / "one_exp.db") db = Database(tmp_path / "one_exp.db")
day = datetime(2026, 8, 1, 12, 0, tzinfo=_SH) # 日历已跨到 8.3,但 8.2 开过的 260803 仍须占用
day = datetime(2026, 8, 3, 0, 0, 2, tzinfo=_SH)
with db._lock: with db._lock:
db._conn.execute(
"""INSERT INTO groups(group_id, status, expiry_ymd, open_at_ms)
VALUES ('G-20260802-01','closed','260803',1)"""
)
db._conn.execute( db._conn.execute(
"""INSERT INTO groups(group_id, status, expiry_ymd, open_at_ms) """INSERT INTO groups(group_id, status, expiry_ymd, open_at_ms)
VALUES ('G-20260801-01','closed','260802',1)""" VALUES ('G-20260801-01','closed','260802',1)"""
) )
db._conn.execute(
"""INSERT INTO groups(group_id, status, expiry_ymd, open_at_ms)
VALUES ('G-20260731-01','closed','260801',1)"""
)
db._conn.commit() db._conn.commit()
used = used_expiry_ymds_for_day(db, day) used = used_expiry_ymds_for_day(db, day)
assert used == {"260802"} assert used == {"260803", "260802"}
assert expiry_blocked_by_one_per_day("260802", used, enabled=True) is True assert expiry_blocked_by_one_per_day("260803", used, enabled=True) is True
assert expiry_blocked_by_one_per_day("260803", used, enabled=True) is False assert expiry_blocked_by_one_per_day("260804", used, enabled=True) is False
assert expiry_blocked_by_one_per_day("260802", used, enabled=False) is False assert expiry_blocked_by_one_per_day("260803", used, enabled=False) is False
db.close() db.close()
+13 -13
View File
@@ -1,11 +1,14 @@
"""空仓选约:跳过当日已用到期与残留待结算到期。""" """空仓选约:跳过历史上已用到期与残留待结算到期。"""
from __future__ import annotations from __future__ import annotations
from datetime import datetime
from zoneinfo import ZoneInfo
from app.strategy.clock import ( from app.strategy.clock import (
expiry_blocked_by_one_per_day, expiry_blocked_by_one_per_day,
pending_residual_expiry_ymds, pending_residual_expiry_ymds,
used_expiry_ymds_for_day, used_expiry_ymds,
) )
@@ -22,32 +25,29 @@ class _FakeDB:
def fetchall(self, sql: str, params: tuple = ()) -> list[dict]: def fetchall(self, sql: str, params: tuple = ()) -> list[dict]:
s = " ".join(sql.split()).lower() s = " ".join(sql.split()).lower()
if "from groups" in s: if "from groups" in s:
like = params[0] if params else ""
prefix = like.replace("%", "")
return [ return [
r r
for r in self._groups for r in self._groups
if str(r.get("group_id", "")).startswith(prefix) if str(r.get("expiry_ymd") or "").strip()
] ]
if "from residual_options" in s: if "from residual_options" in s:
return [r for r in self._residuals if r.get("status") == "pending"] return [r for r in self._residuals if r.get("status") == "pending"]
return [] return []
def test_used_expiry_ymds_for_day() -> None: def test_used_expiry_ymds_across_calendar_days() -> None:
"""8.2 开过 260803 后,8.3 零点仍须拦截同到期。"""
db = _FakeDB( db = _FakeDB(
groups=[ groups=[
{"group_id": "G-20260802-01", "expiry_ymd": "260803"}, {"group_id": "G-20260802-01", "expiry_ymd": "260803"},
{"group_id": "G-20260801-01", "expiry_ymd": "260802"}, {"group_id": "G-20260801-01", "expiry_ymd": "260802"},
] ]
) )
from datetime import datetime now_aug3 = datetime(2026, 8, 3, 0, 0, 2, tzinfo=ZoneInfo("Asia/Shanghai"))
from zoneinfo import ZoneInfo used = used_expiry_ymds(db, now_aug3)
assert used == {"260803", "260802"}
now = datetime(2026, 8, 2, 12, 0, tzinfo=ZoneInfo("Asia/Shanghai")) assert expiry_blocked_by_one_per_day("260803", used, enabled=True)
assert used_expiry_ymds_for_day(db, now) == {"260803"} assert not expiry_blocked_by_one_per_day("260804", used, enabled=True)
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: def test_pending_residual_expiry_ymds() -> None:
+1 -1
View File
@@ -160,7 +160,7 @@ claim 开仓槽 status=opening
|------|------|------| |------|------|------|
| `min_option_hours` | 12 | 过滤过近到期 | | `min_option_hours` | 12 | 过滤过近到期 |
| `min_option_leverage` | 100 | 权利金不能太贵 | | `min_option_leverage` | 100 | 权利金不能太贵 |
| `one_expiry_per_day` | true | 同到期日一天只开一次 | | `one_expiry_per_day` | true | 每个到期只开一次(跨日,按 expiry_ymd 历史占用) |
| `fixed_direction_enabled` | false | 固定方向 | | `fixed_direction_enabled` | false | 固定方向 |
| `fixed_perp_side` | long | long→Put / short→Call | | `fixed_perp_side` | long | long→Put / short→Call |
| `atm_open_offset_enabled` | false | ATM 偏差开关 | | `atm_open_offset_enabled` | false | ATM 偏差开关 |
+2 -2
View File
@@ -302,7 +302,7 @@ export default function PlanPage() {
{fmt(plan?.min_option_leverage, 0)}x · {atmRule} {fmt(plan?.min_option_leverage, 0)}x · {atmRule}
{plan?.skip_weekends ? " · 周末跳过开仓" : ""} {plan?.skip_weekends ? " · 周末跳过开仓" : ""}
{plan?.one_expiry_per_day !== false {plan?.one_expiry_per_day !== false
? " · 同到期日一天只开一次" ? " · 每个到期只开一次"
: ""} : ""}
</p> </p>
<p> <p>
@@ -313,7 +313,7 @@ export default function PlanPage() {
<span className="plan-rules-k"></span> <span className="plan-rules-k"></span>
{plan?.rest_seconds ?? "—"}s · {plan?.rest_seconds ?? "—"}s ·
{plan?.one_expiry_per_day !== false {plan?.one_expiry_per_day !== false
? " · 同到期平仓后改开下一到期" ? " · 同到期用过后改开下一到期"
: ""} : ""}
</p> </p>
</div> </div>
+2 -2
View File
@@ -1230,7 +1230,7 @@ export default function SettingsPage() {
</div> </div>
<div className="field"> <div className="field">
<label htmlFor="oneExpDay"> <label htmlFor="oneExpDay">
</label> </label>
<select <select
id="oneExpDay" id="oneExpDay"
@@ -1244,7 +1244,7 @@ export default function SettingsPage() {
<option value="0"></option> <option value="0"></option>
</select> </select>
<p className="hint" style={{ margin: "0.35rem 0 0" }}> <p className="hint" style={{ margin: "0.35rem 0 0" }}>
0802 0803 0803/ 0804
</p> </p>
</div> </div>
<div className="field"> <div className="field">