Gate one-open-per-expiry by historical expiry_ymd across days.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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},"
|
||||||
"请等下一到期日"
|
"请等下一到期日"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=逐仓;期权仍固定 cash(OKX 逐仓/现金)
|
# 永续保证金模式:cross=全仓(默认)| isolated=逐仓;期权仍固定 cash(OKX 逐仓/现金)
|
||||||
|
|||||||
@@ -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_ymd,YYMMDD)。
|
历史上已开过的期权到期日(groups.expiry_ymd,YYMMDD)。
|
||||||
按当日组号 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()
|
||||||
|
|||||||
@@ -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},"
|
||||||
"请等下一到期日"
|
"请等下一到期日"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 偏差开关 |
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user