Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .github/workflows/daily-digest-notify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
#
Expand Down Expand Up @@ -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 * * *"

Expand Down Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions docs/notifications-quant-sentinel.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down
3 changes: 2 additions & 1 deletion docs/unified-daily-notification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 7 additions & 4 deletions docs/workflows/daily-digest-notify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
#
Expand Down Expand Up @@ -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 * * *"

Expand Down Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions python/tests/test_credential_maintenance_read_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions python/tests/test_daily_digest_aggregator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
4 changes: 2 additions & 2 deletions python/tests/test_notification_consolidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@ 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,
QSL_ROUTINE_NOTIFICATION_MODE="immediate", DIGEST_EVENT_NAME="schedule",
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)

Expand Down
4 changes: 2 additions & 2 deletions python/tests/test_unified_daily_digest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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={
Expand Down
Loading