diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index 1a15d9a..68f89b1 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -61,6 +61,12 @@ jobs: CRISIS_ALERT_PUSH_PRIORITY: ${{ vars.CRISIS_ALERT_PUSH_PRIORITY }} CRISIS_ALERT_PUSH_TAGS: ${{ vars.CRISIS_ALERT_PUSH_TAGS }} CRISIS_ALERT_PUSH_BODY_MAX_CHARS: ${{ vars.CRISIS_ALERT_PUSH_BODY_MAX_CHARS }} + CRISIS_ALERT_TELEGRAM_CHAT_IDS: ${{ vars.CRISIS_ALERT_TELEGRAM_CHAT_IDS }} + CRISIS_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME: ${{ vars.CRISIS_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME }} + CRISIS_ALERT_TELEGRAM_API_BASE_URL: ${{ vars.CRISIS_ALERT_TELEGRAM_API_BASE_URL }} + CRISIS_ALERT_TELEGRAM_PARSE_MODE: ${{ vars.CRISIS_ALERT_TELEGRAM_PARSE_MODE }} + CRISIS_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW: ${{ vars.CRISIS_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW }} + CRISIS_ALERT_TELEGRAM_BODY_MAX_CHARS: ${{ vars.CRISIS_ALERT_TELEGRAM_BODY_MAX_CHARS }} # Strategy-owned defaults continue to come from UsEquityStrategies; this workflow only syncs platform/runtime inputs. EXECUTION_REPORT_GCS_URI: ${{ vars.EXECUTION_REPORT_GCS_URI }} IB_GATEWAY_ZONE: ${{ vars.IB_GATEWAY_ZONE }} @@ -72,6 +78,7 @@ jobs: CRISIS_ALERT_SMS_AUTH_TOKEN: ${{ secrets.CRISIS_ALERT_SMS_AUTH_TOKEN }} CRISIS_ALERT_PUSH_APP_TOKEN: ${{ secrets.CRISIS_ALERT_PUSH_APP_TOKEN }} CRISIS_ALERT_PUSH_ACCESS_TOKEN: ${{ secrets.CRISIS_ALERT_PUSH_ACCESS_TOKEN }} + CRISIS_ALERT_TELEGRAM_BOT_TOKEN: ${{ secrets.CRISIS_ALERT_TELEGRAM_BOT_TOKEN }} steps: - name: Check whether env sync is enabled id: config @@ -366,6 +373,17 @@ jobs: shared_remove_secret_vars+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN") fi + if [ -n "${CRISIS_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME:-}" ]; then + shared_secret_pairs+=("CRISIS_ALERT_TELEGRAM_BOT_TOKEN=${CRISIS_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME}:latest") + shared_remove_env_vars+=("CRISIS_ALERT_TELEGRAM_BOT_TOKEN") + elif [ -n "${CRISIS_ALERT_TELEGRAM_BOT_TOKEN:-}" ]; then + shared_env_pairs+=("CRISIS_ALERT_TELEGRAM_BOT_TOKEN=${CRISIS_ALERT_TELEGRAM_BOT_TOKEN}") + shared_remove_secret_vars+=("CRISIS_ALERT_TELEGRAM_BOT_TOKEN") + else + shared_remove_env_vars+=("CRISIS_ALERT_TELEGRAM_BOT_TOKEN") + shared_remove_secret_vars+=("CRISIS_ALERT_TELEGRAM_BOT_TOKEN") + fi + if [ -n "${CRISIS_ALERT_CHANNELS:-}" ]; then shared_env_pairs+=("CRISIS_ALERT_CHANNELS=${CRISIS_ALERT_CHANNELS}") else @@ -486,6 +504,36 @@ jobs: shared_remove_env_vars+=("CRISIS_ALERT_PUSH_BODY_MAX_CHARS") fi + if [ -n "${CRISIS_ALERT_TELEGRAM_CHAT_IDS:-}" ]; then + shared_env_pairs+=("CRISIS_ALERT_TELEGRAM_CHAT_IDS=${CRISIS_ALERT_TELEGRAM_CHAT_IDS}") + else + shared_remove_env_vars+=("CRISIS_ALERT_TELEGRAM_CHAT_IDS") + fi + + if [ -n "${CRISIS_ALERT_TELEGRAM_API_BASE_URL:-}" ]; then + shared_env_pairs+=("CRISIS_ALERT_TELEGRAM_API_BASE_URL=${CRISIS_ALERT_TELEGRAM_API_BASE_URL}") + else + shared_remove_env_vars+=("CRISIS_ALERT_TELEGRAM_API_BASE_URL") + fi + + if [ -n "${CRISIS_ALERT_TELEGRAM_PARSE_MODE:-}" ]; then + shared_env_pairs+=("CRISIS_ALERT_TELEGRAM_PARSE_MODE=${CRISIS_ALERT_TELEGRAM_PARSE_MODE}") + else + shared_remove_env_vars+=("CRISIS_ALERT_TELEGRAM_PARSE_MODE") + fi + + if [ -n "${CRISIS_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW:-}" ]; then + shared_env_pairs+=("CRISIS_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW=${CRISIS_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW}") + else + shared_remove_env_vars+=("CRISIS_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW") + fi + + if [ -n "${CRISIS_ALERT_TELEGRAM_BODY_MAX_CHARS:-}" ]; then + shared_env_pairs+=("CRISIS_ALERT_TELEGRAM_BODY_MAX_CHARS=${CRISIS_ALERT_TELEGRAM_BODY_MAX_CHARS}") + else + shared_remove_env_vars+=("CRISIS_ALERT_TELEGRAM_BODY_MAX_CHARS") + fi + for target_index in "${!target_services[@]}"; do cloud_run_service="${target_services[${target_index}]}" mapfile -t target_env_pairs < <(emit_target_env_pairs "${target_index}") diff --git a/README.md b/README.md index ad4305e..784fd03 100644 --- a/README.md +++ b/README.md @@ -112,13 +112,15 @@ For IBKR, keep `paper` as a single account-group entry. If you later add live ac | `TELEGRAM_TOKEN` | Yes | Telegram bot token. For Cloud Run, prefer a Secret Manager reference instead of a literal env var. | | `GLOBAL_TELEGRAM_CHAT_ID` | Yes | Telegram chat ID used by this service. | | `NOTIFY_LANG` | No | `en` (default) or `zh` | -| `CRISIS_ALERT_CHANNELS` | No | Optional crisis alert channel list: `email`, `sms`, and/or `push`. | +| `CRISIS_ALERT_CHANNELS` | No | Optional crisis alert channel list: `email`, `sms`, `push`, and/or `telegram`. | | `CRISIS_ALERT_EMAIL_RECIPIENTS` | No | Comma/semicolon/newline-separated email-form recipients. Use a normal mailbox for email-only delivery, or a Google Voice-associated mailbox/address to also trigger Google Voice prompts. | | `CRISIS_ALERT_EMAIL_SENDER_EMAIL` | No | Sender email address used for crisis alert email. Gmail is the default transport, but the sender naming is provider-neutral. | | `CRISIS_ALERT_EMAIL_SENDER_PASSWORD` | No | Sender SMTP password or app password. For Cloud Run, prefer `CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME` in env sync. | | `CRISIS_ALERT_EMAIL_SMTP_HOST` | No | Optional SMTP host override. Defaults to Gmail SMTP when unset. | | `CRISIS_ALERT_EMAIL_SMTP_PORT` | No | Optional SMTP port override. Defaults to `465` when unset. | | `CRISIS_ALERT_EMAIL_SMTP_SECURITY` | No | Optional SMTP security override: `ssl`, `starttls`, or `none`. Defaults to `ssl` when unset. | +| `CRISIS_ALERT_TELEGRAM_CHAT_IDS` | No | Dedicated crisis-alert Telegram chat IDs. Separate from the strategy-cycle Telegram chat. | +| `CRISIS_ALERT_TELEGRAM_BOT_TOKEN` | No | Dedicated crisis-alert Telegram bot token. For Cloud Run, prefer `CRISIS_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME` in env sync. | The selected account-group entry must provide at least: @@ -426,13 +428,15 @@ IBKR 账户 | `TELEGRAM_TOKEN` | 是 | Telegram 机器人 Token。Cloud Run 上更推荐走 Secret Manager 引用,不要直接写成明文 env。 | | `GLOBAL_TELEGRAM_CHAT_ID` | 是 | 这个服务使用的 Telegram Chat ID。 | | `NOTIFY_LANG` | 否 | `en`(默认)或 `zh` | -| `CRISIS_ALERT_CHANNELS` | 否 | 可选危机告警通道列表:`email`、`sms` 和/或 `push`。 | +| `CRISIS_ALERT_CHANNELS` | 否 | 可选危机告警通道列表:`email`、`sms`、`push` 和/或 `telegram`。 | | `CRISIS_ALERT_EMAIL_RECIPIENTS` | 否 | 通知收件邮箱。普通邮箱只收邮件;关联 Google Voice 的邮箱/地址会额外触发 Google Voice 提醒。支持逗号、分号或换行分隔。 | | `CRISIS_ALERT_EMAIL_SENDER_EMAIL` | 否 | 邮件通知的发送方邮箱。默认传输走 Gmail SMTP,但命名不绑定 Gmail。 | | `CRISIS_ALERT_EMAIL_SENDER_PASSWORD` | 否 | 发送方 SMTP 密码或 app password。Cloud Run env sync 建议配置 `CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME`。 | | `CRISIS_ALERT_EMAIL_SMTP_HOST` | 否 | 可选 SMTP host 覆盖。不设置时默认 Gmail SMTP。 | | `CRISIS_ALERT_EMAIL_SMTP_PORT` | 否 | 可选 SMTP port 覆盖。不设置时默认 `465`。 | | `CRISIS_ALERT_EMAIL_SMTP_SECURITY` | 否 | 可选 SMTP 加密方式:`ssl`、`starttls` 或 `none`。不设置时默认 `ssl`。 | +| `CRISIS_ALERT_TELEGRAM_CHAT_IDS` | 否 | 危机告警专用 Telegram chat ID,和常规策略周期 Telegram 分开。 | +| `CRISIS_ALERT_TELEGRAM_BOT_TOKEN` | 否 | 危机告警专用 Telegram bot token。Cloud Run env sync 建议配置 `CRISIS_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME`。 | 选中的账号组配置里,至少要有: diff --git a/requirements.txt b/requirements.txt index 0ccd37b..47533c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ flask gunicorn -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@2fdb7c35edf2f39c1592a7dd9319b7516276fabf -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@b626ee3e42feabbf6cd427af6faf5655ca6cd76f +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b520bcaa216e36b94ce60c8c6a13f982de826830 +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@df91828c8b521516c85512e136e724064b6b9dac pandas numpy requests diff --git a/runtime_config_support.py b/runtime_config_support.py index 54458b6..ca42eb0 100644 --- a/runtime_config_support.py +++ b/runtime_config_support.py @@ -96,6 +96,12 @@ class PlatformRuntimeSettings: crisis_alert_push_priority: str | None = None crisis_alert_push_tags: str | None = None crisis_alert_push_body_max_chars: str | None = None + crisis_alert_telegram_chat_ids: tuple[str, ...] = () + crisis_alert_telegram_bot_token: str | None = None + crisis_alert_telegram_api_base_url: str | None = None + crisis_alert_telegram_parse_mode: str | None = None + crisis_alert_telegram_disable_web_page_preview: str | None = None + crisis_alert_telegram_body_max_chars: str | None = None runtime_target: RuntimeTarget | None = None @@ -274,6 +280,24 @@ def load_platform_runtime_settings( crisis_alert_push_body_max_chars=first_non_empty( os.getenv("CRISIS_ALERT_PUSH_BODY_MAX_CHARS") ), + crisis_alert_telegram_chat_ids=split_env_list( + os.getenv("CRISIS_ALERT_TELEGRAM_CHAT_IDS") + ), + crisis_alert_telegram_bot_token=first_non_empty( + os.getenv("CRISIS_ALERT_TELEGRAM_BOT_TOKEN") + ), + crisis_alert_telegram_api_base_url=first_non_empty( + os.getenv("CRISIS_ALERT_TELEGRAM_API_BASE_URL") + ), + crisis_alert_telegram_parse_mode=first_non_empty( + os.getenv("CRISIS_ALERT_TELEGRAM_PARSE_MODE") + ), + crisis_alert_telegram_disable_web_page_preview=first_non_empty( + os.getenv("CRISIS_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW") + ), + crisis_alert_telegram_body_max_chars=first_non_empty( + os.getenv("CRISIS_ALERT_TELEGRAM_BODY_MAX_CHARS") + ), runtime_target=runtime_target, ) diff --git a/tests/conftest.py b/tests/conftest.py index ceab2f5..87ce62c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,6 +46,12 @@ def load_strategy_module(**env_overrides): "CRISIS_ALERT_PUSH_PRIORITY": None, "CRISIS_ALERT_PUSH_TAGS": None, "CRISIS_ALERT_PUSH_BODY_MAX_CHARS": None, + "CRISIS_ALERT_TELEGRAM_CHAT_IDS": None, + "CRISIS_ALERT_TELEGRAM_BOT_TOKEN": None, + "CRISIS_ALERT_TELEGRAM_API_BASE_URL": None, + "CRISIS_ALERT_TELEGRAM_PARSE_MODE": None, + "CRISIS_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW": None, + "CRISIS_ALERT_TELEGRAM_BODY_MAX_CHARS": None, } defaults.update(env_overrides) if "RUNTIME_TARGET_JSON" not in defaults: diff --git a/tests/test_runtime_config_support.py b/tests/test_runtime_config_support.py index fb39696..2b55c0e 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -161,6 +161,12 @@ def test_load_platform_runtime_settings_uses_minimal_group_config(monkeypatch): assert settings.crisis_alert_push_priority is None assert settings.crisis_alert_push_tags is None assert settings.crisis_alert_push_body_max_chars is None + assert settings.crisis_alert_telegram_chat_ids == () + assert settings.crisis_alert_telegram_bot_token is None + assert settings.crisis_alert_telegram_api_base_url is None + assert settings.crisis_alert_telegram_parse_mode is None + assert settings.crisis_alert_telegram_disable_web_page_preview is None + assert settings.crisis_alert_telegram_body_max_chars is None def test_load_platform_runtime_settings_prefers_runtime_target_json(monkeypatch): @@ -276,7 +282,7 @@ def test_load_platform_runtime_settings_reads_crisis_alert_channels_and_push_con monkeypatch.setenv("RUNTIME_TARGET_JSON", runtime_target_json(SAMPLE_STRATEGY_PROFILE)) monkeypatch.setenv("ACCOUNT_GROUP", "paper") monkeypatch.setenv("IB_ACCOUNT_GROUP_CONFIG_JSON", MINIMAL_GROUP_JSON) - monkeypatch.setenv("CRISIS_ALERT_CHANNELS", "email;push") + monkeypatch.setenv("CRISIS_ALERT_CHANNELS", "email;push;telegram") monkeypatch.setenv("CRISIS_ALERT_PUSH_RECIPIENTS", "risk-topic; backup-topic") monkeypatch.setenv("CRISIS_ALERT_PUSH_PROVIDER", "ntfy") monkeypatch.setenv("CRISIS_ALERT_PUSH_APP_TOKEN", "app-token") @@ -286,10 +292,16 @@ def test_load_platform_runtime_settings_reads_crisis_alert_channels_and_push_con monkeypatch.setenv("CRISIS_ALERT_PUSH_PRIORITY", "5") monkeypatch.setenv("CRISIS_ALERT_PUSH_TAGS", "warning") monkeypatch.setenv("CRISIS_ALERT_PUSH_BODY_MAX_CHARS", "300") + monkeypatch.setenv("CRISIS_ALERT_TELEGRAM_CHAT_IDS", "12345; @risk_channel") + monkeypatch.setenv("CRISIS_ALERT_TELEGRAM_BOT_TOKEN", "telegram-token") + monkeypatch.setenv("CRISIS_ALERT_TELEGRAM_API_BASE_URL", "https://telegram.example.test") + monkeypatch.setenv("CRISIS_ALERT_TELEGRAM_PARSE_MODE", "HTML") + monkeypatch.setenv("CRISIS_ALERT_TELEGRAM_DISABLE_WEB_PAGE_PREVIEW", "false") + monkeypatch.setenv("CRISIS_ALERT_TELEGRAM_BODY_MAX_CHARS", "900") settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1") - assert settings.crisis_alert_channels == ("email", "push") + assert settings.crisis_alert_channels == ("email", "push", "telegram") assert settings.crisis_alert_push_recipients == ("risk-topic", "backup-topic") assert settings.crisis_alert_push_provider == "ntfy" assert settings.crisis_alert_push_app_token == "app-token" @@ -299,6 +311,12 @@ def test_load_platform_runtime_settings_reads_crisis_alert_channels_and_push_con assert settings.crisis_alert_push_priority == "5" assert settings.crisis_alert_push_tags == "warning" assert settings.crisis_alert_push_body_max_chars == "300" + assert settings.crisis_alert_telegram_chat_ids == ("12345", "@risk_channel") + assert settings.crisis_alert_telegram_bot_token == "telegram-token" + assert settings.crisis_alert_telegram_api_base_url == "https://telegram.example.test" + assert settings.crisis_alert_telegram_parse_mode == "HTML" + assert settings.crisis_alert_telegram_disable_web_page_preview == "false" + assert settings.crisis_alert_telegram_body_max_chars == "900" def test_load_platform_runtime_settings_uses_whole_share_quantity_step(monkeypatch): diff --git a/tests/test_sync_cloud_run_env_workflow.sh b/tests/test_sync_cloud_run_env_workflow.sh index 1a787e0..34f3efc 100644 --- a/tests/test_sync_cloud_run_env_workflow.sh +++ b/tests/test_sync_cloud_run_env_workflow.sh @@ -25,8 +25,11 @@ grep -Fq 'IBKR_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.IBKR_STRATEGY_PLUGIN_MOUNTS grep -Fq 'CRISIS_ALERT_CHANNELS: ${{ vars.CRISIS_ALERT_CHANNELS }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_RECIPIENTS: ${{ vars.CRISIS_ALERT_EMAIL_RECIPIENTS }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_TELEGRAM_CHAT_IDS: ${{ vars.CRISIS_ALERT_TELEGRAM_CHAT_IDS }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME: ${{ vars.CRISIS_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME }}' "$workflow_file" grep -Fq 'TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_SENDER_PASSWORD: ${{ secrets.CRISIS_ALERT_EMAIL_SENDER_PASSWORD }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_TELEGRAM_BOT_TOKEN: ${{ secrets.CRISIS_ALERT_TELEGRAM_BOT_TOKEN }}' "$workflow_file" grep -Fq 'Skipping Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true.' "$workflow_file" grep -Fq 'scripts/build_cloud_run_env_sync_plan.py --json' "$workflow_file" @@ -50,8 +53,11 @@ grep -Fq 'shared_remove_env_vars=(' "$workflow_file" grep -Fq 'shared_remove_secret_vars=(' "$workflow_file" grep -Fq 'shared_secret_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'shared_secret_pairs+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD=${CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME}:latest")' "$workflow_file" +grep -Fq 'shared_secret_pairs+=("CRISIS_ALERT_TELEGRAM_BOT_TOKEN=${CRISIS_ALERT_TELEGRAM_BOT_TOKEN_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'shared_env_pairs+=("CRISIS_ALERT_CHANNELS=${CRISIS_ALERT_CHANNELS}")' "$workflow_file" grep -Fq 'shared_remove_env_vars+=("CRISIS_ALERT_CHANNELS")' "$workflow_file" +grep -Fq 'shared_env_pairs+=("CRISIS_ALERT_TELEGRAM_CHAT_IDS=${CRISIS_ALERT_TELEGRAM_CHAT_IDS}")' "$workflow_file" +grep -Fq 'shared_remove_env_vars+=("CRISIS_ALERT_TELEGRAM_BOT_TOKEN")' "$workflow_file" grep -Fq 'mapfile -t target_env_pairs < <(emit_target_env_pairs "${target_index}")' "$workflow_file" grep -Fq 'mapfile -t target_remove_env_vars < <(emit_target_remove_env_vars "${target_index}")' "$workflow_file" grep -Fq 'env_pairs=("${target_env_pairs[@]}" "${shared_env_pairs[@]}")' "$workflow_file"