From 63d0dcc79528960168125b887998bd78e3a0f02d Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:30:30 +0800 Subject: [PATCH] Schedule routine digest after candidate uploads Co-Authored-By: Codex --- .github/workflows/daily-digest-notify.yml | 11 +++++++---- docs/notifications-quant-sentinel.zh-CN.md | 4 ++-- docs/unified-daily-notification.md | 3 ++- docs/workflows/daily-digest-notify.yml | 11 +++++++---- .../tests/test_credential_maintenance_read_check.py | 4 ++-- python/tests/test_daily_digest_aggregator.py | 4 ++-- python/tests/test_notification_consolidation.py | 4 ++-- python/tests/test_unified_daily_digest.py | 4 ++-- 8 files changed, 26 insertions(+), 19 deletions(-) diff --git a/.github/workflows/daily-digest-notify.yml b/.github/workflows/daily-digest-notify.yml index 2b437b52..c2d0148a 100644 --- a/.github/workflows/daily-digest-notify.yml +++ b/.github/workflows/daily-digest-notify.yml @@ -10,7 +10,10 @@ name: Daily Digest Notify # when GitHub/candidates evidence could not be loaded). # # Schedule (GitHub Actions cron is UTC): -# 0 22 * * 1-5 == Tue–Sat 06:00 Asia/Shanghai (covers prior US RTH close). +# 10 23 * * 1-5 == Tue–Sat 07:10 Asia/Shanghai (after 06:20 producer uploads). +# The business-day cutoff remains 06:00 Asia/Shanghai. This gives the +# 22:20 UTC producer jobs time to upload; their actual completion is verified +# by candidate coverage, not assumed from the schedule. # Optional Asia cash session digest is not scheduled yet; add a second cron # only after an Asia evidence contract exists (do not invent runs). # @@ -67,8 +70,8 @@ on: type: boolean default: true schedule: - # Asia/Shanghai Tue–Sat 06:00 → UTC Mon–Fri 22:00 - - cron: "0 22 * * 1-5" + # Asia/Shanghai Tue–Sat 07:10 → UTC Mon–Fri 23:10, after 22:20 producers + - cron: "10 23 * * 1-5" # Additional crypto windows run only when unified mode is explicitly enabled. - cron: "0 23 * * *" @@ -142,7 +145,7 @@ jobs: mode = os.environ.get("QSL_ROUTINE_NOTIFICATION_MODE", "immediate") if os.environ.get("QRS_UNIFIED_NOTIFICATION_ENABLED", "").strip().lower() == "true": mode = "daily_digest" - expected = "0 23 * * *" if mode == "daily_digest" else "0 22 * * 1-5" + expected = "0 23 * * *" if mode == "daily_digest" else "10 23 * * 1-5" proceed = os.environ.get("DIGEST_EVENT_NAME") != "schedule" or os.environ.get("DIGEST_EVENT_SCHEDULE") == expected with open(os.environ["GITHUB_OUTPUT"], "a") as output: output.write(f"proceed={str(proceed).lower()}\n") diff --git a/docs/notifications-quant-sentinel.zh-CN.md b/docs/notifications-quant-sentinel.zh-CN.md index 79b09716..7a384132 100644 --- a/docs/notifications-quant-sentinel.zh-CN.md +++ b/docs/notifications-quant-sentinel.zh-CN.md @@ -64,7 +64,7 @@ Chat 目标仍只通过运行时注入:`QSL_GLOBAL_TELEGRAM_CHAT_ID`(首选 ### 调度(合同默认) - 时区:`Asia/Shanghai` -- Cron:`0 6 * * 2-6`(周二至周六 06:00,覆盖前一美股 RTH 收盘) +- 普通模式 Cron:UTC `10 23 * * 1-5`(上海时间周二至周六 07:10,在平台 06:20 候选上传后;业务日窗口仍于 06:00 截止,覆盖前一美股 RTH 收盘)。统一模式按现有 UTC 23:00 每日槽运行。 - 加密按同一窗口并入;若当日 UTC 有周期则出现在列表中 ### 文案与 i18n @@ -171,7 +171,7 @@ Receipt totals follow the same evidence boundary: absent evidence, stale candida - Workflow:`.github/workflows/daily-digest-notify.yml`(文稿同步于 `docs/workflows/daily-digest-notify.yml`) - 脚本:`python/scripts/daily_digest_aggregator.py` + `python/scripts/send_daily_digest_telegram.py` -- 调度:UTC `0 22 * * 1-5` = **Asia/Shanghai 周二至周六 06:00**(覆盖前一美股 RTH) +- 调度:UTC `10 23 * * 1-5` = **Asia/Shanghai 周二至周六 07:10**(在 06:20 的平台候选上传后;业务日截止仍为 06:00,覆盖前一美股 RTH) - 证据:私有日报候选存储 `DigestCandidateStore` 经 `DIGEST_CANDIDATES_GCS_URI` 读取,与保留的 `DIGEST_CANDIDATES_JSON` 合并后由 `DIGEST_CANDIDATES_PATH` 消费(见 [接线与采用状态](./digest-candidates-wiring.zh-CN.md));另有可选 allowlisted GitHub workflow 成功结论(**存在性 only;成交数未知≠0**)。接线完成、自然周期和真实送达分别验收。 - 聚合键:`platform_id + strategy + opaque_account_uid|unknown + target_id|unknown`;同策略多账户不得混拼 - 发送:Environment `runtime-strategy-switch`;`TELEGRAM_TOKEN` 或 `TELEGRAM_TOKEN_SECRET_NAME`→GCP `quant-sentinel-telegram-bot-token`;chat 走 `QSL_GLOBAL_TELEGRAM_CHAT_ID` / `GLOBAL_TELEGRAM_CHAT_ID` diff --git a/docs/unified-daily-notification.md b/docs/unified-daily-notification.md index fb109d6b..e8806181 100644 --- a/docs/unified-daily-notification.md +++ b/docs/unified-daily-notification.md @@ -8,7 +8,8 @@ stale reports and execution alerts add a diagnostics section only when present. The UTF-16 budget is bounded; long digests are summarized without multiple sends. Unified mode uses the existing digest workflow at 23:00 UTC daily, after the -existing 22:20 UTC report monitors. Legacy mode keeps 22:00 UTC weekdays. +existing 22:20 UTC report monitors. Legacy mode runs at 23:10 UTC on weekdays, +after those monitors; the business-day cutoff remains 22:00 UTC. No runtime trading schedule or disabled target is changed. Reports cover the completed 06:00 SGT window. Platform producers must supply real archived counts; a workflow conclusion alone is never an order or fill fact. diff --git a/docs/workflows/daily-digest-notify.yml b/docs/workflows/daily-digest-notify.yml index a7f78bb5..c21e1935 100644 --- a/docs/workflows/daily-digest-notify.yml +++ b/docs/workflows/daily-digest-notify.yml @@ -10,7 +10,10 @@ name: Daily Digest Notify # when GitHub/candidates evidence could not be loaded). # # Schedule (GitHub Actions cron is UTC): -# 0 22 * * 1-5 == Tue–Sat 06:00 Asia/Shanghai (covers prior US RTH close). +# 10 23 * * 1-5 == Tue–Sat 07:10 Asia/Shanghai (after 06:20 producer uploads). +# The business-day cutoff remains 06:00 Asia/Shanghai. This gives the +# 22:20 UTC producer jobs time to upload; their actual completion is verified +# by candidate coverage, not assumed from the schedule. # Optional Asia cash session digest is not scheduled yet; add a second cron # only after an Asia evidence contract exists (do not invent runs). # @@ -67,8 +70,8 @@ on: type: boolean default: true schedule: - # Asia/Shanghai Tue–Sat 06:00 → UTC Mon–Fri 22:00 - - cron: "0 22 * * 1-5" + # Asia/Shanghai Tue–Sat 07:10 → UTC Mon–Fri 23:10, after 22:20 producers + - cron: "10 23 * * 1-5" # Additional crypto windows run only when unified mode is explicitly enabled. - cron: "0 23 * * *" @@ -139,7 +142,7 @@ jobs: python3 - <<'PY_SLOT' import os mode = os.environ.get("QSL_ROUTINE_NOTIFICATION_MODE", "immediate") - expected = "0 23 * * *" if mode == "daily_digest" else "0 22 * * 1-5" + expected = "0 23 * * *" if mode == "daily_digest" else "10 23 * * 1-5" proceed = os.environ.get("DIGEST_EVENT_NAME") != "schedule" or os.environ.get("DIGEST_EVENT_SCHEDULE") == expected with open(os.environ["GITHUB_OUTPUT"], "a") as output: output.write(f"proceed={str(proceed).lower()}\n") diff --git a/python/tests/test_credential_maintenance_read_check.py b/python/tests/test_credential_maintenance_read_check.py index 0dc1c30d..62868423 100644 --- a/python/tests/test_credential_maintenance_read_check.py +++ b/python/tests/test_credential_maintenance_read_check.py @@ -52,8 +52,8 @@ def test_workflow_isolated_from_digest_with_minimum_existing_authority(self): default: false ''', source) self.assertIn(''' schedule: - # Asia/Shanghai Tue–Sat 06:00 → UTC Mon–Fri 22:00 - - cron: "0 22 * * 1-5" + # Asia/Shanghai Tue–Sat 07:10 → UTC Mon–Fri 23:10, after 22:20 producers + - cron: "10 23 * * 1-5" ''', source) digest, check = source.split('jobs:\n digest:\n', 1)[1].split('\n maintenance-read-check:\n', 1) exclude_check = "github.event_name != 'workflow_dispatch' || inputs.maintenance_read_check != true" diff --git a/python/tests/test_daily_digest_aggregator.py b/python/tests/test_daily_digest_aggregator.py index 5fd1623c..a1b7fb19 100644 --- a/python/tests/test_daily_digest_aggregator.py +++ b/python/tests/test_daily_digest_aggregator.py @@ -534,7 +534,7 @@ def test_workflow_schedule_matches_shanghai_post_close(self): Path(__file__).resolve().parents[2] / "docs/workflows/daily-digest-notify.yml" ).read_text(encoding="utf-8") - self.assertIn('cron: "0 22 * * 1-5"', workflow) + self.assertIn('cron: "10 23 * * 1-5"', workflow) self.assertIn("Asia/Shanghai", workflow) self.assertIn("workflow_dispatch", workflow) self.assertIn("quant-sentinel-telegram-bot-token", workflow) @@ -852,7 +852,7 @@ def test_workflow_schedule_forces_v1_layout(self): # The deployable workflow has a default-off release flag. The user-owned # documentation template is intentionally not edited in this code PR. template = (ROOT.parent / "docs/workflows/daily-digest-notify.yml").read_text(encoding="utf-8") - for schedule in ('cron: "0 22 * * 1-5"', 'cron: "0 23 * * *"'): + for schedule in ('cron: "10 23 * * 1-5"', 'cron: "0 23 * * *"'): self.assertIn(schedule, text) self.assertIn(schedule, template) self.assertIn("QRS_UNIFIED_NOTIFICATION_ENABLED || 'false'", text) diff --git a/python/tests/test_notification_consolidation.py b/python/tests/test_notification_consolidation.py index 870006aa..4c98d9eb 100644 --- a/python/tests/test_notification_consolidation.py +++ b/python/tests/test_notification_consolidation.py @@ -64,7 +64,7 @@ def test_both_crons_and_manual_calls_share_one_selected_slot(self): script = textwrap.dedent(steps[0].split(" run: |\n", 1)[1]) self.assertIn("QRS_UNIFIED_NOTIFICATION_ENABLED || 'false'", source) for value in ("", "false", "invalid", "true", " TRUE "): - for slot in ("0 22 * * 1-5", "0 23 * * *"): + for slot in ("10 23 * * 1-5", "0 23 * * *"): with tempfile.TemporaryDirectory() as directory: output = Path(directory) / "out" env = dict(PATH=os.environ["PATH"], QRS_UNIFIED_NOTIFICATION_ENABLED=value, @@ -72,7 +72,7 @@ def test_both_crons_and_manual_calls_share_one_selected_slot(self): DIGEST_EVENT_SCHEDULE=slot, GITHUB_OUTPUT=str(output)) result = subprocess.run(["bash", "-c", script], env=env, capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) - expected = "0 23 * * *" if value.strip().lower() == "true" else "0 22 * * 1-5" + expected = "0 23 * * *" if value.strip().lower() == "true" else "10 23 * * 1-5" self.assertEqual(output.read_text(), f"proceed={str(slot == expected).lower()}\n") self.assertIn("cancel-in-progress: false", source) diff --git a/python/tests/test_unified_daily_digest.py b/python/tests/test_unified_daily_digest.py index 11b171fb..fca0eec4 100644 --- a/python/tests/test_unified_daily_digest.py +++ b/python/tests/test_unified_daily_digest.py @@ -107,8 +107,8 @@ def test_environment_mode_selects_exactly_one_slot_without_credentials(self): steps = re.split(r'(?m)^ - (?:name|uses): ', job)[1:] self.assertIn('id: schedule_slot', steps[0]) script = textwrap.dedent(steps[0].split(' run: |\n', 1)[1]) - for mode, selected in [('immediate', '0 22 * * 1-5'), ('daily_digest', '0 23 * * *')]: - for slot in ['0 22 * * 1-5', '0 23 * * *']: + for mode, selected in [('immediate', '10 23 * * 1-5'), ('daily_digest', '0 23 * * *')]: + for slot in ['10 23 * * 1-5', '0 23 * * *']: with self.subTest(mode=mode, slot=slot), tempfile.TemporaryDirectory() as directory: output = Path(directory) / 'output' result = subprocess.run(['bash', '-c', script], env={