Skip to content

Commit 0344fc4

Browse files
committed
Merge main into rebuild/console-holdings-panel to refresh required checks
2 parents 8dbfae3 + 6292b99 commit 0344fc4

15 files changed

Lines changed: 1149 additions & 2 deletions

‎.github/workflows/daily-digest-notify.yml‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ on:
3838
required: false
3939
type: string
4040
default: ""
41+
layout:
42+
description: "Message layout. v1 = current production; sections_v2 = NOTIFY-04 four-section report (try with dry_run first)."
43+
required: false
44+
type: choice
45+
default: v1
46+
options:
47+
- v1
48+
- sections_v2
4149
include_github_evidence:
4250
description: "Query allowlisted platform workflow conclusions (counts stay unknown unless candidates supply them)."
4351
required: true
@@ -79,6 +87,12 @@ jobs:
7987
DIGEST_CANDIDATES_JSON: ${{ secrets.DIGEST_CANDIDATES_JSON }}
8088
# Optional path override (Environment variable); relative to workspace.
8189
DIGEST_CANDIDATES_PATH_VAR: ${{ vars.DIGEST_CANDIDATES_PATH || '' }}
90+
# NOTIFY-04 B2: producer store (unset = disabled). Production value:
91+
# gs://qsl-runtime-logs-shared/digest-candidates (reader SA: prefix-conditioned objectViewer)
92+
# Layout: <uri>/<platform_id>/<business_day>/<target>.json
93+
DIGEST_CANDIDATES_GCS_URI: ${{ vars.DIGEST_CANDIDATES_GCS_URI || '' }}
94+
DIGEST_CANDIDATES_READER_WIF_PROVIDER: ${{ vars.DIGEST_CANDIDATES_READER_WIF_PROVIDER || vars.DIGEST_GCP_WORKLOAD_IDENTITY_PROVIDER || 'projects/252919773759/locations/global/workloadIdentityPools/github-actions/providers/github-main' }}
95+
DIGEST_CANDIDATES_READER_SERVICE_ACCOUNT: ${{ vars.DIGEST_CANDIDATES_READER_SERVICE_ACCOUNT || vars.DIGEST_GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT || 'qsl-artifact-evidence@longbridgequant.iam.gserviceaccount.com' }}
8296
# GCP project that hosts a sentinel bot-token replica (LongBridge path).
8397
GCP_PROJECT_ID: ${{ vars.DIGEST_GCP_PROJECT_ID || 'longbridgequant' }}
8498
GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ vars.DIGEST_GCP_WORKLOAD_IDENTITY_PROVIDER || 'projects/252919773759/locations/global/workloadIdentityPools/github-actions/providers/github-main' }}
@@ -98,6 +112,7 @@ jobs:
98112
INPUT_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}
99113
INPUT_BUSINESS_DAY: ${{ inputs.business_day || '' }}
100114
INPUT_LOCALE: ${{ inputs.locale || '' }}
115+
INPUT_LAYOUT: ${{ inputs.layout || 'v1' }}
101116
INPUT_INCLUDE_GITHUB: ${{ github.event_name != 'workflow_dispatch' && true || inputs.include_github_evidence }}
102117
run: |
103118
set -euo pipefail
@@ -113,6 +128,15 @@ jobs:
113128
fi
114129
echo "business_day=$INPUT_BUSINESS_DAY" >> "$GITHUB_OUTPUT"
115130
echo "locale=$INPUT_LOCALE" >> "$GITHUB_OUTPUT"
131+
# Scheduled runs stay on v1 until the NOTIFY-04 switch-over is approved.
132+
if [[ "${{ github.event_name }}" == "schedule" ]]; then
133+
echo "layout=v1" >> "$GITHUB_OUTPUT"
134+
else
135+
case "$INPUT_LAYOUT" in
136+
sections_v2) echo "layout=sections_v2" >> "$GITHUB_OUTPUT" ;;
137+
*) echo "layout=v1" >> "$GITHUB_OUTPUT" ;;
138+
esac
139+
fi
116140
if [[ "$INPUT_INCLUDE_GITHUB" == "true" ]]; then
117141
echo "include_github=true" >> "$GITHUB_OUTPUT"
118142
else
@@ -152,6 +176,50 @@ jobs:
152176
echo "Wire steps: docs/digest-candidates-wiring.zh-CN.md"
153177
fi
154178
179+
- name: Authenticate to Google Cloud for producer candidates (optional)
180+
id: candidates_gcp_auth
181+
if: ${{ env.DIGEST_CANDIDATES_GCS_URI != '' }}
182+
continue-on-error: true
183+
uses: google-github-actions/auth@v3
184+
with:
185+
workload_identity_provider: ${{ env.DIGEST_CANDIDATES_READER_WIF_PROVIDER }}
186+
service_account: ${{ env.DIGEST_CANDIDATES_READER_SERVICE_ACCOUNT }}
187+
188+
- name: Set up gcloud for producer candidates (optional)
189+
if: ${{ steps.candidates_gcp_auth.outcome == 'success' }}
190+
uses: google-github-actions/setup-gcloud@v3
191+
192+
- name: Fetch producer candidates from private store (optional)
193+
if: ${{ env.DIGEST_CANDIDATES_GCS_URI != '' }}
194+
env:
195+
INPUT_BUSINESS_DAY: ${{ steps.opts.outputs.business_day }}
196+
AUTH_OUTCOME: ${{ steps.candidates_gcp_auth.outcome }}
197+
run: |
198+
set -euo pipefail
199+
# Never echo row contents (may carry equity); counts only.
200+
if [[ "$AUTH_OUTCOME" != "success" ]]; then
201+
echo "Producer candidates store configured but authentication failed; continuing without it." >&2
202+
echo "::warning::digest candidates store auth failed"
203+
exit 0
204+
fi
205+
day="${INPUT_BUSINESS_DAY:-$(TZ=America/New_York date +%F)}"
206+
fetched="data/input/daily-digest/fetched"
207+
mkdir -p "$fetched"
208+
uri="${DIGEST_CANDIDATES_GCS_URI%/}"
209+
found=0
210+
while IFS= read -r object; do
211+
[[ "$object" == *"/${day}/"*.json ]] || continue
212+
rel="${object#"${uri}/"}"
213+
mkdir -p "$fetched/$(dirname "$rel")"
214+
gcloud storage cp "$object" "$fetched/$rel" --no-user-output-enabled
215+
found=$((found + 1))
216+
done < <(gcloud storage ls "${uri}/*/${day}/*.json" 2>/dev/null || true)
217+
echo "Producer candidate files for ${day}: ${found}"
218+
base="${DIGEST_CANDIDATES_PATH:-}"
219+
out="data/input/daily-digest/candidates-merged.json"
220+
python3 python/scripts/merge_digest_candidates.py ${base:+--base "$base"} --input-dir "$fetched" --output "$out"
221+
echo "DIGEST_CANDIDATES_PATH=$out" >> "$GITHUB_ENV"
222+
155223
- name: Authenticate to Google Cloud for sentinel secret (optional)
156224
id: gcp_auth
157225
if: ${{ env.TELEGRAM_TOKEN == '' && env.TELEGRAM_TOKEN_SECRET_NAME != '' && steps.opts.outputs.dry_run != 'true' }}
@@ -177,6 +245,7 @@ jobs:
177245
args=(
178246
python3 python/scripts/send_daily_digest_telegram.py
179247
--write-receipt data/output/daily-digest/receipt.json
248+
--layout "${{ steps.opts.outputs.layout }}"
180249
)
181250
if [[ -n "${{ steps.opts.outputs.locale }}" ]]; then
182251
args+=(--locale "${{ steps.opts.outputs.locale }}")
@@ -211,6 +280,10 @@ jobs:
211280
print(f"- status: `{receipt.get('status')}`")
212281
print(f"- business_day: `{receipt.get('business_day')}`")
213282
print(f"- locale: `{receipt.get('locale')}`")
283+
print(f"- layout: `{receipt.get('layout')}`")
284+
window = receipt.get("window") or {}
285+
if window:
286+
print(f"- window (Asia/Shanghai): `{window.get('start_local')}` → `{window.get('end_local')}`")
214287
print(f"- evidence_status: `{receipt.get('evidence_status')}`")
215288
print(f"- run_count: `{receipt.get('run_count')}`")
216289
print(f"- total_fills: `{receipt.get('total_fills')}`")
@@ -228,6 +301,7 @@ jobs:
228301
print(f" - github_sources_matched: `{coverage.get('github_sources_matched')}`")
229302
print(f" - has_source_failures: `{coverage.get('has_source_failures')}`")
230303
print(f" - is_complete_for_verified_idle: `{coverage.get('is_complete_for_verified_idle')}`")
304+
print(f" - stale_candidates_count: `{coverage.get('stale_candidates_count')}`")
231305
failures = receipt.get("failures") or coverage.get("github_failures") or []
232306
if failures:
233307
print("- failures:")
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Digest Store Permission Check
2+
3+
# NOTIFY-04 B2 (read-only): probe whether this repo's GitHub identity can write /
4+
# read the shared digest-candidate prefix in gs://qsl-runtime-logs-shared, and
5+
# whether the bucket is private. Uses testIamPermissions / metadata reads only;
6+
# never creates, overwrites or deletes objects. Prints booleans and HTTP codes.
7+
on:
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
id-token: write
13+
14+
jobs:
15+
probe:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 5
18+
env:
19+
GCP_PROJECT_ID: longbridgequant
20+
GCP_WORKLOAD_IDENTITY_PROVIDER: projects/252919773759/locations/global/workloadIdentityPools/github-actions/providers/github-main
21+
GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT: qsl-artifact-evidence@longbridgequant.iam.gserviceaccount.com
22+
DIGEST_STORE_BUCKET: qsl-runtime-logs-shared
23+
DIGEST_STORE_PREFIX: digest-candidates/
24+
steps:
25+
- name: Checkout repository
26+
uses: actions/checkout@v6
27+
28+
- name: Authenticate to Google Cloud
29+
uses: google-github-actions/auth@v3
30+
with:
31+
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
32+
service_account: ${{ env.GCP_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }}
33+
34+
- name: Set up gcloud
35+
uses: google-github-actions/setup-gcloud@v3
36+
37+
- name: Probe digest store permissions (read-only)
38+
run: python3 python/scripts/digest_store_permission_check.py

‎docs/digest-candidates-wiring.zh-CN.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ GitHub workflow 成功结论只是**存在性**辅助证据;成交/订单数
4242

4343
- 聚合键为 `(platform_id, strategy_profile, opaque_account_uid|unknown, target_id|unknown)`;同策略多账户必须带身份,禁止仅用平台+策略合并。
4444
- `actually_ran` 必须显式布尔;缺省不再默认 `true`。
45+
- **NOTIFY-04 新鲜度**:每行必须带 `business_day`(America/New_York 交易日,ISO 日期),或者带 `as_of`(ISO 时间戳,按纽约日历日换算;无时区按 UTC)。和本次日报业务日不一致、缺失或无法解析的行一律丢弃,不渲染金额/持仓;日报末尾加一段「⚠️ 数据未更新,已略去:· 平台 / 策略(数据业务日 …)」,receipt 写 `stale_candidates[]`(只含平台、策略、日期、原因码)和 `source_coverage.stale_candidates_count`。有丢弃行时不判定「已验证无运行」。
4546
- 新 stub / 存在性证据:`fill_count`/`order_count` 用 `null` + `field_status.counts_unknown` 或 `reason_code=github_workflow_existence_only`。
4647
- 旧生产者若仍给 `int 0` 且无 `field_status`,按「已验证零成交」兼容。
4748
- 权益/持仓/信号/调仓字段可选;有则渲染,无则省略;中央不编造。
@@ -106,6 +107,16 @@ python3 python/scripts/send_daily_digest_telegram.py \
106107
- receipt:`total_fills=null`(`total_fills_status=unknown`),非 0
107108
- 渲染文案含「成交数未知」/「成交 未知」;**不**出现「无成交」「链路正常」
108109

110+
### 私有存储自动接线(NOTIFY-04 B2,默认关闭)
111+
112+
- 生产者(Schwab `digest-candidates-daily.yml`、IBKR `execution-report-heartbeat.yml` 定时)每天生成候选,只有设置了仓库变量 `DIGEST_CANDIDATES_GCS_URI` 才会上传到 `<uri>/<platform_id>/<business_day>/<target>.json`;没设就是 dry-run,文件只留在 runner 临时目录。
113+
- 中央日报设置了同名 Environment 变量才去读取:先用 `DIGEST_CANDIDATES_READER_*` 的 WIF 认证,再把 `<uri>/*/<business_day>/*.json` 和旧 secret(如果还有)用 `merge_digest_candidates.py` 合并(日志只打印条数)。过期行仍由 B0 规则丢弃。
114+
- 存储位置:复用现有的共享 bucket,前缀为 `gs://qsl-runtime-logs-shared/digest-candidates/`(不新建 bucket,不做 Cloud Scheduler 准点触发,继续用 GitHub Actions cron)。
115+
- 2026-10-11 只读探测(各仓库的 `Digest Store Permission Check`):Schwab 和 IBKR 的部署 SA 对该 bucket 只能 get/list,**不能 create**;QRS 读取 SA `qsl-artifact-evidence@longbridgequant` **不能 get/list**;匿名 list 返回 401。
116+
- 2026-10-11 owner 已加好 3 条带前缀条件的 IAM。生产者仓库的变量值为 `DIGEST_CANDIDATES_GCS_URI=gs://qsl-runtime-logs-shared/digest-candidates`。QRS 的同名变量**暂不设置**:一旦设置,日报内容会改变(B7)。
117+
- (原计划)需要 owner 一次性加带前缀条件的 IAM:生产者在 `digest-candidates/<platform>/` 下给 objectUser;读取者给 objectViewer,条件为对象前缀或 `objectListPrefix` 前缀。完成后先设生产者仓库的变量,日报仓库的变量最后设,因为它会改变日报内容(B7,需要批准)。全部核对通过之后,才删除 `DIGEST_CANDIDATES_JSON`。
118+
- 不用 Actions artifact 中转:这几个仓库都是公开仓库,artifact 任何登录用户都能下载,而候选数据可能带有账户权益。
119+
109120
### 生产注入(人闸)
110121

111122
写入 Environment `runtime-strategy-switch` 的 secret `DIGEST_CANDIDATES_JSON`(或由私有前置步骤写出文件后设 `DIGEST_CANDIDATES_PATH`),再对 `daily-digest-notify.yml` 做 **dry-run** `workflow_dispatch`。改生产 Environment / 关 dry-run 需人工确认;本任务与自动化**不得**直接改生产 Environment。

‎docs/notifications-quant-sentinel.zh-CN.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ Chat 目标仍只通过运行时注入:`QSL_GLOBAL_TELEGRAM_CHAT_ID`(首选
5454
- **观察字段(有证据才写,禁止编造)**:`strategy_label`、`equity`、`holdings[]`(symbol/value/qty)、`signal_summary`、`rebalance_kind` / `rebalance_conclusion`、可选 `tips` / `account_hint`。每个观察块标题前缀 `[account]`:`account_hint`(人类可读)→ `{platform_id} {account_scope|execution_mode}`(如 `schwab live` / `ibkr live` / `firstrade paper`)→ 短 `opaque_account_uid` → `target_id`(末位)。GitHub workflow 成功结论只证明「跑过」,不填金额/持仓。
5555
- **不改交易/风控判定**;纯聚合展示。金额与账户标识不得写入公开 CI 日志。
5656

57+
### NOTIFY-04 四段式日报(sections_v2,试运行中)
58+
59+
- 版式:`--layout sections_v2` / env `DIGEST_LAYOUT` / workflow_dispatch 输入 `layout`。**定时运行固定 `v1`**,切换需要单独批准(NOTIFY-04 B7)。
60+
- 窗口:纽约交易日 `B` 的日报标为上海 `B+1`,覆盖 `[B 06:00, B+1 06:00)` Asia/Shanghai(= `[B-1 22:00, B 22:00)` UTC);receipt 写 `window{start_local,end_local,start_utc,end_utc,report_date}`。
61+
- 顺序:⚠️ 待处理(异常行、过期候选、证据未知)→ 📈 美股日策略(纽约周末写「休市」,工作日无证据写「本窗口无运行证据」)→ 💰 定投(`us_dca_month_end` 和加密 `*_dca`;只在有运行时出现)→ 🪙 加密小时策略(只在有运行时出现)→ 🧾 覆盖。
62+
- 分段:候选行的 `section`(`us_daily` / `dca` / `crypto_intraday`)优先;没有就按 `platform-config.json` 的 domain / scheduler_profile 推断。Binance 小时策略里的 BTC 定投腿要等生产者单独出一行 `section=dca`(B3)。
63+
5764
### 调度(合同默认)
5865

5966
- 时区:`Asia/Shanghai`

0 commit comments

Comments
 (0)