diff --git a/.github/workflows/sync-cloud-run-env.yml b/.github/workflows/sync-cloud-run-env.yml index a497a3e..1481ca2 100644 --- a/.github/workflows/sync-cloud-run-env.yml +++ b/.github/workflows/sync-cloud-run-env.yml @@ -34,6 +34,7 @@ jobs: IBKR_MIN_RESERVED_CASH_USD: ${{ vars.IBKR_MIN_RESERVED_CASH_USD }} IBKR_RESERVED_CASH_RATIO: ${{ vars.IBKR_RESERVED_CASH_RATIO }} IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD: ${{ vars.IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD }} + CRISIS_ALERT_CHANNELS: ${{ vars.CRISIS_ALERT_CHANNELS }} CRISIS_ALERT_EMAIL_RECIPIENTS: ${{ vars.CRISIS_ALERT_EMAIL_RECIPIENTS }} CRISIS_ALERT_EMAIL_SENDER_EMAIL: ${{ vars.CRISIS_ALERT_EMAIL_SENDER_EMAIL }} CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME }} @@ -48,6 +49,15 @@ jobs: CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID: ${{ vars.CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID }} CRISIS_ALERT_SMS_API_BASE_URL: ${{ vars.CRISIS_ALERT_SMS_API_BASE_URL }} CRISIS_ALERT_SMS_BODY_MAX_CHARS: ${{ vars.CRISIS_ALERT_SMS_BODY_MAX_CHARS }} + CRISIS_ALERT_PUSH_RECIPIENTS: ${{ vars.CRISIS_ALERT_PUSH_RECIPIENTS }} + CRISIS_ALERT_PUSH_PROVIDER: ${{ vars.CRISIS_ALERT_PUSH_PROVIDER }} + CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME: ${{ vars.CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME }} + CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME: ${{ vars.CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME }} + CRISIS_ALERT_PUSH_API_BASE_URL: ${{ vars.CRISIS_ALERT_PUSH_API_BASE_URL }} + CRISIS_ALERT_PUSH_DEVICE: ${{ vars.CRISIS_ALERT_PUSH_DEVICE }} + 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 }} # 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 }} @@ -57,6 +67,8 @@ jobs: TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} CRISIS_ALERT_EMAIL_SENDER_PASSWORD: ${{ secrets.CRISIS_ALERT_EMAIL_SENDER_PASSWORD }} 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 }} steps: - name: Check whether env sync is enabled id: config @@ -332,6 +344,28 @@ jobs: remove_secret_vars+=("CRISIS_ALERT_SMS_AUTH_TOKEN") fi + if [ -n "${CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME:-}" ]; then + secret_pairs+=("CRISIS_ALERT_PUSH_APP_TOKEN=${CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME}:latest") + remove_env_vars+=("CRISIS_ALERT_PUSH_APP_TOKEN") + elif [ -n "${CRISIS_ALERT_PUSH_APP_TOKEN:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_APP_TOKEN=${CRISIS_ALERT_PUSH_APP_TOKEN}") + remove_secret_vars+=("CRISIS_ALERT_PUSH_APP_TOKEN") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_APP_TOKEN") + remove_secret_vars+=("CRISIS_ALERT_PUSH_APP_TOKEN") + fi + + if [ -n "${CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME:-}" ]; then + secret_pairs+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN=${CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME}:latest") + remove_env_vars+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN") + elif [ -n "${CRISIS_ALERT_PUSH_ACCESS_TOKEN:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN=${CRISIS_ALERT_PUSH_ACCESS_TOKEN}") + remove_secret_vars+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN") + remove_secret_vars+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN") + fi + if [ -n "${IB_GATEWAY_ZONE:-}" ]; then env_pairs+=("IB_GATEWAY_ZONE=${IB_GATEWAY_ZONE}") else @@ -404,6 +438,12 @@ jobs: remove_env_vars+=("IBKR_RESERVED_CASH_RATIO") fi + if [ -n "${CRISIS_ALERT_CHANNELS:-}" ]; then + env_pairs+=("CRISIS_ALERT_CHANNELS=${CRISIS_ALERT_CHANNELS}") + else + remove_env_vars+=("CRISIS_ALERT_CHANNELS") + fi + if [ -n "${CRISIS_ALERT_EMAIL_RECIPIENTS:-}" ]; then env_pairs+=("CRISIS_ALERT_EMAIL_RECIPIENTS=${CRISIS_ALERT_EMAIL_RECIPIENTS}") else @@ -476,6 +516,48 @@ jobs: remove_env_vars+=("CRISIS_ALERT_SMS_BODY_MAX_CHARS") fi + if [ -n "${CRISIS_ALERT_PUSH_RECIPIENTS:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_RECIPIENTS=${CRISIS_ALERT_PUSH_RECIPIENTS}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_RECIPIENTS") + fi + + if [ -n "${CRISIS_ALERT_PUSH_PROVIDER:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_PROVIDER=${CRISIS_ALERT_PUSH_PROVIDER}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_PROVIDER") + fi + + if [ -n "${CRISIS_ALERT_PUSH_API_BASE_URL:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_API_BASE_URL=${CRISIS_ALERT_PUSH_API_BASE_URL}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_API_BASE_URL") + fi + + if [ -n "${CRISIS_ALERT_PUSH_DEVICE:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_DEVICE=${CRISIS_ALERT_PUSH_DEVICE}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_DEVICE") + fi + + if [ -n "${CRISIS_ALERT_PUSH_PRIORITY:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_PRIORITY=${CRISIS_ALERT_PUSH_PRIORITY}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_PRIORITY") + fi + + if [ -n "${CRISIS_ALERT_PUSH_TAGS:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_TAGS=${CRISIS_ALERT_PUSH_TAGS}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_TAGS") + fi + + if [ -n "${CRISIS_ALERT_PUSH_BODY_MAX_CHARS:-}" ]; then + env_pairs+=("CRISIS_ALERT_PUSH_BODY_MAX_CHARS=${CRISIS_ALERT_PUSH_BODY_MAX_CHARS}") + else + remove_env_vars+=("CRISIS_ALERT_PUSH_BODY_MAX_CHARS") + fi + if [ -n "${EXECUTION_REPORT_GCS_URI:-}" ]; then env_pairs+=("EXECUTION_REPORT_GCS_URI=${EXECUTION_REPORT_GCS_URI}") else diff --git a/README.md b/README.md index a6b7217..5646731 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ 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_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. | @@ -182,7 +183,7 @@ GLOBAL_TELEGRAM_CHAT_ID= NOTIFY_LANG=zh ``` -This shared-config mode is only for the **IBKR pair** (`InteractiveBrokersPlatform` + `IBKRGatewayManager`). It is not meant to become a global secret bundle for unrelated quant repos. Across multiple quant projects, broadly reusable runtime settings are usually limited to `GLOBAL_TELEGRAM_CHAT_ID`, `NOTIFY_LANG`, and the generic `CRISIS_ALERT_EMAIL_*` crisis mail contract when the same alert policy applies. +This shared-config mode is only for the **IBKR pair** (`InteractiveBrokersPlatform` + `IBKRGatewayManager`). It is not meant to become a global secret bundle for unrelated quant repos. Across multiple quant projects, broadly reusable runtime settings are usually limited to `GLOBAL_TELEGRAM_CHAT_ID`, `NOTIFY_LANG`, `CRISIS_ALERT_CHANNELS`, and generic `CRISIS_ALERT_EMAIL_*`/`CRISIS_ALERT_PUSH_*` alert policy when the same alert policy applies. Recommended account-group config payload: @@ -213,8 +214,8 @@ Current behavior is fail-fast: - missing account-group config source → startup error - missing key fields in the selected group (`ib_gateway_instance_name`, `ib_gateway_mode`, `ib_client_id`) → startup error -When `IBKR_STRATEGY_PLUGIN_MOUNTS_JSON` includes the `crisis_response_shadow` plugin, the normal strategy-cycle Telegram message still includes the compact plugin line. If the plugin signal escalates beyond `no_action` (for example `canonical_route=true_crisis`, `suggested_action=defend`/`blocked`, or `would_trade_if_enabled=true`), the service also sends an independent crisis email notification when the `CRISIS_ALERT_EMAIL_*` settings are complete. -Email alert results are written into the runtime report. Duplicate suppression uses stable plugin alert keys and stores markers under `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` when set, otherwise `EXECUTION_REPORT_GCS_URI`, with a local `/tmp` marker fallback. +When `IBKR_STRATEGY_PLUGIN_MOUNTS_JSON` includes the `crisis_response_shadow` plugin, the normal strategy-cycle Telegram message still includes the compact plugin line. If the plugin signal escalates beyond `no_action` (for example `canonical_route=true_crisis`, `suggested_action=defend`/`blocked`, or `would_trade_if_enabled=true`), the service also sends independent crisis alerts through configured `CRISIS_ALERT_CHANNELS` channels. +Alert results are written into the runtime report. Duplicate suppression uses stable plugin alert keys and stores markers under `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` when set, otherwise `EXECUTION_REPORT_GCS_URI`, with a local `/tmp` marker fallback. ### GitHub-managed Cloud Run env sync @@ -249,7 +250,7 @@ On every push to `main`, the workflow updates the existing Cloud Run service wit Important: - The workflow only becomes strict when `ENABLE_GITHUB_ENV_SYNC=true`. If this variable is unset, the sync job is skipped. When enabled, it resolves the selected profile's snapshot/config requirements from `scripts/print_strategy_profile_status.py --json` instead of a hard-coded strategy-name list. -- Here "shared config" still only means the **IBKR pair** (`InteractiveBrokersPlatform` + `IBKRGatewayManager`). `TELEGRAM_TOKEN` and `TELEGRAM_TOKEN_SECRET_NAME` remain repository-specific. If crisis mail recipients and sender policy are shared across platforms, manage `CRISIS_ALERT_EMAIL_*` with GitHub Organization Variables/Secrets or GCP Secret Manager. +- Here "shared config" still only means the **IBKR pair** (`InteractiveBrokersPlatform` + `IBKRGatewayManager`). `TELEGRAM_TOKEN` and `TELEGRAM_TOKEN_SECRET_NAME` remain repository-specific. If crisis alert recipients and sender policy are shared across platforms, manage `CRISIS_ALERT_CHANNELS`, `CRISIS_ALERT_EMAIL_*`, and `CRISIS_ALERT_PUSH_*` with GitHub Organization Variables/Secrets or GCP Secret Manager. - If `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` is set, the Cloud Run runtime needs Secret Manager access to that secret. - GitHub now authenticates to Google Cloud with OIDC + Workload Identity Federation, so `GCP_SA_KEY` is no longer required for this workflow. - If you deploy with `gcloud run deploy --source` or a Cloud Run source trigger, the staging bucket `gs://run-sources--` also needs `roles/storage.objectViewer` for the build service account, the deploy service account, and the default compute service account. Missing this binding fails the deploy before Cloud Build starts with `storage.objects.get` denied. @@ -363,6 +364,7 @@ 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_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`。 | @@ -399,7 +401,7 @@ IB_GATEWAY_ZONE=us-central1-c IB_GATEWAY_IP_MODE=internal ``` -这里说的“共享配置”只针对 **IBKR 这一组系统**,也就是 `InteractiveBrokersPlatform` 和 `IBKRGatewayManager` 之间共享。它不是让所有 quant 仓库都共用一套 secrets。对多个量化仓库来说,`GLOBAL_TELEGRAM_CHAT_ID`、`NOTIFY_LANG`,以及同一套危机邮件策略下的 `CRISIS_ALERT_EMAIL_*` 适合提升到组织级配置;发送方密码仍应放在 GitHub Secret 或 GCP Secret Manager。 +这里说的“共享配置”只针对 **IBKR 这一组系统**,也就是 `InteractiveBrokersPlatform` 和 `IBKRGatewayManager` 之间共享。它不是让所有 quant 仓库都共用一套 secrets。对多个量化仓库来说,`GLOBAL_TELEGRAM_CHAT_ID`、`NOTIFY_LANG`、`CRISIS_ALERT_CHANNELS`,以及同一套危机告警策略下的 `CRISIS_ALERT_EMAIL_*`/`CRISIS_ALERT_PUSH_*` 适合提升到组织级配置;告警 token 和密码仍应放在 GitHub Secret 或 GCP Secret Manager。 推荐的账号组配置 JSON: @@ -430,8 +432,8 @@ IB_GATEWAY_IP_MODE=internal - 没有账号组配置来源 → 启动直接报错 - 选中的账号组缺少关键字段(`ib_gateway_instance_name`、`ib_gateway_mode`、`ib_client_id`)→ 启动直接报错 -如果 `IBKR_STRATEGY_PLUGIN_MOUNTS_JSON` 挂载了 `crisis_response_shadow` 插件,常规策略周期 Telegram 仍会包含插件摘要行。当插件信号升级到非 `no_action`(例如 `canonical_route=true_crisis`、`suggested_action=defend`/`blocked`,或 `would_trade_if_enabled=true`)时,只要 `CRISIS_ALERT_EMAIL_*` 配置完整,服务还会额外发送一条独立邮件危机通知。 -邮件告警结果会写入 runtime report。重复发送抑制使用稳定的插件告警 key;如配置了 `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` 则写入该前缀,否则复用 `EXECUTION_REPORT_GCS_URI`,并有本地 `/tmp` marker fallback。 +如果 `IBKR_STRATEGY_PLUGIN_MOUNTS_JSON` 挂载了 `crisis_response_shadow` 插件,常规策略周期 Telegram 仍会包含插件摘要行。当插件信号升级到非 `no_action`(例如 `canonical_route=true_crisis`、`suggested_action=defend`/`blocked`,或 `would_trade_if_enabled=true`)时,服务还会按 `CRISIS_ALERT_CHANNELS` 配置额外发送独立危机通知。 +告警结果会写入 runtime report。重复发送抑制使用稳定的插件告警 key;如配置了 `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` 则写入该前缀,否则复用 `EXECUTION_REPORT_GCS_URI`,并有本地 `/tmp` marker fallback。 ### GitHub 统一管理 Cloud Run 环境变量 @@ -466,7 +468,7 @@ IB_GATEWAY_IP_MODE=internal 注意: - 只有在 `ENABLE_GITHUB_ENV_SYNC=true` 时,这个 workflow 才会严格校验并执行同步。没打开时会直接跳过。打开后,它会通过 `scripts/print_strategy_profile_status.py --json` 动态解析目标策略需要的 snapshot/config 输入,不再维护硬编码策略名列表。 -- 这里说的“共享配置”仍然只针对 **IBKR 这一组系统**。`TELEGRAM_TOKEN` 和 `TELEGRAM_TOKEN_SECRET_NAME` 都还是这个仓库自己的配置,不建议提升成所有 quant 共用的全局配置。危机邮件通知如果确实跨平台共用同一套收件人和发送方,可以用 GitHub Organization Variables/Secrets 管理 `CRISIS_ALERT_EMAIL_*`。 +- 这里说的“共享配置”仍然只针对 **IBKR 这一组系统**。`TELEGRAM_TOKEN` 和 `TELEGRAM_TOKEN_SECRET_NAME` 都还是这个仓库自己的配置,不建议提升成所有 quant 共用的全局配置。危机告警如果确实跨平台共用同一套收件人和发送方,可以用 GitHub Organization Variables/Secrets 管理 `CRISIS_ALERT_CHANNELS`、`CRISIS_ALERT_EMAIL_*` 和 `CRISIS_ALERT_PUSH_*`。 - 如果设置了 `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME`,Cloud Run 运行时还需要有对应 Secret 的访问权限。 - GitHub 现在通过 OIDC + Workload Identity Federation 登录 Google Cloud,这个 workflow 不再需要 `GCP_SA_KEY`。 - 如果你用 `gcloud run deploy --source` 或 Cloud Run source trigger 部署,还要给 staging bucket `gs://run-sources--` 补 `roles/storage.objectViewer`,对象包括 build service account、deploy service account、默认 compute service account。少了这层权限,会在 Cloud Build 启动前直接报 `storage.objects.get denied`。 diff --git a/requirements.txt b/requirements.txt index 77f74c9..6737806 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ flask gunicorn -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@7183145b06f8b9292e1c8f3adba0a4f5dc0a5834 -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@d13b248d279887a57eef5b7692c2c7bd33906348 +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@1050a678a17a997bf4776c2a4c4bef59de29e1c1 +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@48f0267e3128eb7c6a8344ddefa08e0e80dc9d27 pandas numpy requests diff --git a/runtime_config_support.py b/runtime_config_support.py index 332c9c2..54458b6 100644 --- a/runtime_config_support.py +++ b/runtime_config_support.py @@ -72,6 +72,7 @@ class PlatformRuntimeSettings: tg_chat_id: str | None = None notify_lang: str = "en" strategy_plugin_mounts_json: str | None = None + crisis_alert_channels: tuple[str, ...] = () crisis_alert_email_recipients: tuple[str, ...] = () crisis_alert_email_sender_email: str | None = None crisis_alert_email_sender_password: str | None = None @@ -86,6 +87,15 @@ class PlatformRuntimeSettings: crisis_alert_sms_messaging_service_id: str | None = None crisis_alert_sms_api_base_url: str | None = None crisis_alert_sms_body_max_chars: str | None = None + crisis_alert_push_recipients: tuple[str, ...] = () + crisis_alert_push_provider: str | None = None + crisis_alert_push_app_token: str | None = None + crisis_alert_push_access_token: str | None = None + crisis_alert_push_api_base_url: str | None = None + crisis_alert_push_device: str | None = None + crisis_alert_push_priority: str | None = None + crisis_alert_push_tags: str | None = None + crisis_alert_push_body_max_chars: str | None = None runtime_target: RuntimeTarget | None = None @@ -194,6 +204,7 @@ def load_platform_runtime_settings( os.getenv("IBKR_STRATEGY_PLUGIN_MOUNTS_JSON") or os.getenv("STRATEGY_PLUGIN_MOUNTS_JSON") ), + crisis_alert_channels=split_env_list(os.getenv("CRISIS_ALERT_CHANNELS")), crisis_alert_email_recipients=split_env_list( os.getenv("CRISIS_ALERT_EMAIL_RECIPIENTS") ), @@ -236,6 +247,33 @@ def load_platform_runtime_settings( crisis_alert_sms_body_max_chars=first_non_empty( os.getenv("CRISIS_ALERT_SMS_BODY_MAX_CHARS") ), + crisis_alert_push_recipients=split_env_list( + os.getenv("CRISIS_ALERT_PUSH_RECIPIENTS") + ), + crisis_alert_push_provider=first_non_empty( + os.getenv("CRISIS_ALERT_PUSH_PROVIDER") + ), + crisis_alert_push_app_token=first_non_empty( + os.getenv("CRISIS_ALERT_PUSH_APP_TOKEN") + ), + crisis_alert_push_access_token=first_non_empty( + os.getenv("CRISIS_ALERT_PUSH_ACCESS_TOKEN") + ), + crisis_alert_push_api_base_url=first_non_empty( + os.getenv("CRISIS_ALERT_PUSH_API_BASE_URL") + ), + crisis_alert_push_device=first_non_empty( + os.getenv("CRISIS_ALERT_PUSH_DEVICE") + ), + crisis_alert_push_priority=first_non_empty( + os.getenv("CRISIS_ALERT_PUSH_PRIORITY") + ), + crisis_alert_push_tags=first_non_empty( + os.getenv("CRISIS_ALERT_PUSH_TAGS") + ), + crisis_alert_push_body_max_chars=first_non_empty( + os.getenv("CRISIS_ALERT_PUSH_BODY_MAX_CHARS") + ), runtime_target=runtime_target, ) diff --git a/tests/conftest.py b/tests/conftest.py index faa73f6..ceab2f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,12 +30,22 @@ def load_strategy_module(**env_overrides): ), "IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME": None, "GLOBAL_TELEGRAM_CHAT_ID": None, + "CRISIS_ALERT_CHANNELS": None, "CRISIS_ALERT_EMAIL_RECIPIENTS": None, "CRISIS_ALERT_EMAIL_SENDER_EMAIL": None, "CRISIS_ALERT_EMAIL_SENDER_PASSWORD": None, "CRISIS_ALERT_EMAIL_SMTP_HOST": None, "CRISIS_ALERT_EMAIL_SMTP_PORT": None, "CRISIS_ALERT_EMAIL_SMTP_SECURITY": None, + "CRISIS_ALERT_PUSH_RECIPIENTS": None, + "CRISIS_ALERT_PUSH_PROVIDER": None, + "CRISIS_ALERT_PUSH_APP_TOKEN": None, + "CRISIS_ALERT_PUSH_ACCESS_TOKEN": None, + "CRISIS_ALERT_PUSH_API_BASE_URL": None, + "CRISIS_ALERT_PUSH_DEVICE": None, + "CRISIS_ALERT_PUSH_PRIORITY": None, + "CRISIS_ALERT_PUSH_TAGS": None, + "CRISIS_ALERT_PUSH_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 390a9a0..1ce552c 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -135,6 +135,7 @@ def test_load_platform_runtime_settings_uses_minimal_group_config(monkeypatch): assert settings.tg_token is None assert settings.tg_chat_id is None assert settings.strategy_plugin_mounts_json is None + assert settings.crisis_alert_channels == () assert settings.crisis_alert_email_recipients == () assert settings.crisis_alert_email_sender_email is None assert settings.crisis_alert_email_sender_password is None @@ -149,6 +150,15 @@ def test_load_platform_runtime_settings_uses_minimal_group_config(monkeypatch): assert settings.crisis_alert_sms_messaging_service_id is None assert settings.crisis_alert_sms_api_base_url is None assert settings.crisis_alert_sms_body_max_chars is None + assert settings.crisis_alert_push_recipients == () + assert settings.crisis_alert_push_provider is None + assert settings.crisis_alert_push_app_token is None + assert settings.crisis_alert_push_access_token is None + assert settings.crisis_alert_push_api_base_url is None + assert settings.crisis_alert_push_device is None + 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 def test_load_platform_runtime_settings_prefers_runtime_target_json(monkeypatch): @@ -260,6 +270,35 @@ def test_load_platform_runtime_settings_reads_crisis_alert_sms_config(monkeypatc assert settings.crisis_alert_sms_body_max_chars == "160" +def test_load_platform_runtime_settings_reads_crisis_alert_channels_and_push_config(monkeypatch): + 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_PUSH_RECIPIENTS", "risk-topic; backup-topic") + monkeypatch.setenv("CRISIS_ALERT_PUSH_PROVIDER", "ntfy") + monkeypatch.setenv("CRISIS_ALERT_PUSH_APP_TOKEN", "app-token") + monkeypatch.setenv("CRISIS_ALERT_PUSH_ACCESS_TOKEN", "access-token") + monkeypatch.setenv("CRISIS_ALERT_PUSH_API_BASE_URL", "https://ntfy.example.test") + monkeypatch.setenv("CRISIS_ALERT_PUSH_DEVICE", "iphone") + monkeypatch.setenv("CRISIS_ALERT_PUSH_PRIORITY", "5") + monkeypatch.setenv("CRISIS_ALERT_PUSH_TAGS", "warning") + monkeypatch.setenv("CRISIS_ALERT_PUSH_BODY_MAX_CHARS", "300") + + settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1") + + assert settings.crisis_alert_channels == ("email", "push") + 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" + assert settings.crisis_alert_push_access_token == "access-token" + assert settings.crisis_alert_push_api_base_url == "https://ntfy.example.test" + assert settings.crisis_alert_push_device == "iphone" + assert settings.crisis_alert_push_priority == "5" + assert settings.crisis_alert_push_tags == "warning" + assert settings.crisis_alert_push_body_max_chars == "300" + + def test_load_platform_runtime_settings_uses_whole_share_quantity_step(monkeypatch): monkeypatch.setenv("RUNTIME_TARGET_JSON", runtime_target_json(SAMPLE_STRATEGY_PROFILE)) monkeypatch.setenv("ACCOUNT_GROUP", "paper") diff --git a/tests/test_sync_cloud_run_env_workflow.sh b/tests/test_sync_cloud_run_env_workflow.sh index 3419d08..4df8c6f 100644 --- a/tests/test_sync_cloud_run_env_workflow.sh +++ b/tests/test_sync_cloud_run_env_workflow.sh @@ -45,6 +45,7 @@ grep -Fq 'IBKR_PAPER_LIQUIDATE_ONLY: ${{ vars.IBKR_PAPER_LIQUIDATE_ONLY }}' "$wo grep -Fq 'IBKR_MIN_RESERVED_CASH_USD: ${{ vars.IBKR_MIN_RESERVED_CASH_USD }}' "$workflow_file" grep -Fq 'IBKR_RESERVED_CASH_RATIO: ${{ vars.IBKR_RESERVED_CASH_RATIO }}' "$workflow_file" grep -Fq 'IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD: ${{ vars.IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD }}' "$workflow_file" +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_EMAIL: ${{ vars.CRISIS_ALERT_EMAIL_SENDER_EMAIL }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME }}' "$workflow_file" @@ -59,6 +60,15 @@ grep -Fq 'CRISIS_ALERT_SMS_SENDER: ${{ vars.CRISIS_ALERT_SMS_SENDER }}' "$workfl grep -Fq 'CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID: ${{ vars.CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_SMS_API_BASE_URL: ${{ vars.CRISIS_ALERT_SMS_API_BASE_URL }}' "$workflow_file" grep -Fq 'CRISIS_ALERT_SMS_BODY_MAX_CHARS: ${{ vars.CRISIS_ALERT_SMS_BODY_MAX_CHARS }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_RECIPIENTS: ${{ vars.CRISIS_ALERT_PUSH_RECIPIENTS }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_PROVIDER: ${{ vars.CRISIS_ALERT_PUSH_PROVIDER }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME: ${{ vars.CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME: ${{ vars.CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_API_BASE_URL: ${{ vars.CRISIS_ALERT_PUSH_API_BASE_URL }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_DEVICE: ${{ vars.CRISIS_ALERT_PUSH_DEVICE }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_PRIORITY: ${{ vars.CRISIS_ALERT_PUSH_PRIORITY }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_TAGS: ${{ vars.CRISIS_ALERT_PUSH_TAGS }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_BODY_MAX_CHARS: ${{ vars.CRISIS_ALERT_PUSH_BODY_MAX_CHARS }}' "$workflow_file" grep -Fq 'IB_GATEWAY_ZONE: ${{ vars.IB_GATEWAY_ZONE }}' "$workflow_file" grep -Fq 'IB_GATEWAY_IP_MODE: ${{ vars.IB_GATEWAY_IP_MODE }}' "$workflow_file" grep -Fq 'GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}' "$workflow_file" @@ -66,6 +76,8 @@ grep -Fq 'NOTIFY_LANG: ${{ vars.NOTIFY_LANG }}' "$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_SMS_AUTH_TOKEN: ${{ secrets.CRISIS_ALERT_SMS_AUTH_TOKEN }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_APP_TOKEN: ${{ secrets.CRISIS_ALERT_PUSH_APP_TOKEN }}' "$workflow_file" +grep -Fq 'CRISIS_ALERT_PUSH_ACCESS_TOKEN: ${{ secrets.CRISIS_ALERT_PUSH_ACCESS_TOKEN }}' "$workflow_file" grep -Fq "Skipping Cloud Run env sync because ENABLE_GITHUB_ENV_SYNC is not set to true." "$workflow_file" grep -Fq "Cloud Run env sync is enabled, but these values are missing:" "$workflow_file" grep -Fq 'TELEGRAM_TOKEN_SECRET_NAME or TELEGRAM_TOKEN' "$workflow_file" @@ -103,6 +115,7 @@ grep -Fq 'env_pairs+=("IBKR_PAPER_LIQUIDATE_ONLY=${IBKR_PAPER_LIQUIDATE_ONLY}")' grep -Fq 'env_pairs+=("IBKR_MIN_RESERVED_CASH_USD=${IBKR_MIN_RESERVED_CASH_USD}")' "$workflow_file" grep -Fq 'env_pairs+=("IBKR_RESERVED_CASH_RATIO=${IBKR_RESERVED_CASH_RATIO}")' "$workflow_file" grep -Fq 'env_pairs+=("IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD=${IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_CHANNELS=${CRISIS_ALERT_CHANNELS}")' "$workflow_file" grep -Fq 'env_pairs+=("CRISIS_ALERT_EMAIL_RECIPIENTS=${CRISIS_ALERT_EMAIL_RECIPIENTS}")' "$workflow_file" grep -Fq 'env_pairs+=("CRISIS_ALERT_EMAIL_SENDER_EMAIL=${CRISIS_ALERT_EMAIL_SENDER_EMAIL}")' "$workflow_file" grep -Fq 'env_pairs+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD=${CRISIS_ALERT_EMAIL_SENDER_PASSWORD}")' "$workflow_file" @@ -116,6 +129,13 @@ grep -Fq 'env_pairs+=("CRISIS_ALERT_SMS_SENDER=${CRISIS_ALERT_SMS_SENDER}")' "$w grep -Fq 'env_pairs+=("CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID=${CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID}")' "$workflow_file" grep -Fq 'env_pairs+=("CRISIS_ALERT_SMS_API_BASE_URL=${CRISIS_ALERT_SMS_API_BASE_URL}")' "$workflow_file" grep -Fq 'env_pairs+=("CRISIS_ALERT_SMS_BODY_MAX_CHARS=${CRISIS_ALERT_SMS_BODY_MAX_CHARS}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_PUSH_RECIPIENTS=${CRISIS_ALERT_PUSH_RECIPIENTS}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_PUSH_PROVIDER=${CRISIS_ALERT_PUSH_PROVIDER}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_PUSH_API_BASE_URL=${CRISIS_ALERT_PUSH_API_BASE_URL}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_PUSH_DEVICE=${CRISIS_ALERT_PUSH_DEVICE}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_PUSH_PRIORITY=${CRISIS_ALERT_PUSH_PRIORITY}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_PUSH_TAGS=${CRISIS_ALERT_PUSH_TAGS}")' "$workflow_file" +grep -Fq 'env_pairs+=("CRISIS_ALERT_PUSH_BODY_MAX_CHARS=${CRISIS_ALERT_PUSH_BODY_MAX_CHARS}")' "$workflow_file" grep -Fq 'remove_env_vars+=("IBKR_FEATURE_SNAPSHOT_PATH")' "$workflow_file" grep -Fq 'remove_env_vars+=("IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH")' "$workflow_file" grep -Fq 'remove_env_vars+=("IBKR_STRATEGY_CONFIG_PATH")' "$workflow_file" @@ -126,6 +146,7 @@ grep -Fq 'remove_env_vars+=("IBKR_PAPER_LIQUIDATE_ONLY")' "$workflow_file" grep -Fq 'remove_env_vars+=("IBKR_MIN_RESERVED_CASH_USD")' "$workflow_file" grep -Fq 'remove_env_vars+=("IBKR_RESERVED_CASH_RATIO")' "$workflow_file" grep -Fq 'remove_env_vars+=("IBKR_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_CHANNELS")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_EMAIL_RECIPIENTS")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_EMAIL_SENDER_EMAIL")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD")' "$workflow_file" @@ -140,6 +161,15 @@ grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMS_SENDER")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMS_MESSAGING_SERVICE_ID")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMS_API_BASE_URL")' "$workflow_file" grep -Fq 'remove_env_vars+=("CRISIS_ALERT_SMS_BODY_MAX_CHARS")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_RECIPIENTS")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_PROVIDER")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_APP_TOKEN")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_API_BASE_URL")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_DEVICE")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_PRIORITY")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_TAGS")' "$workflow_file" +grep -Fq 'remove_env_vars+=("CRISIS_ALERT_PUSH_BODY_MAX_CHARS")' "$workflow_file" grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_TO"' "$workflow_file" grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_GATEWAY"' "$workflow_file" grep -Fq '"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER"' "$workflow_file" @@ -154,6 +184,8 @@ grep -Fq '"CRISIS_ALERT_SMTP_HOST"' "$workflow_file" grep -Fq 'secret_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'secret_pairs+=("CRISIS_ALERT_EMAIL_SENDER_PASSWORD=${CRISIS_ALERT_EMAIL_SENDER_PASSWORD_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'secret_pairs+=("CRISIS_ALERT_SMS_AUTH_TOKEN=${CRISIS_ALERT_SMS_AUTH_TOKEN_SECRET_NAME}:latest")' "$workflow_file" +grep -Fq 'secret_pairs+=("CRISIS_ALERT_PUSH_APP_TOKEN=${CRISIS_ALERT_PUSH_APP_TOKEN_SECRET_NAME}:latest")' "$workflow_file" +grep -Fq 'secret_pairs+=("CRISIS_ALERT_PUSH_ACCESS_TOKEN=${CRISIS_ALERT_PUSH_ACCESS_TOKEN_SECRET_NAME}:latest")' "$workflow_file" grep -Fq 'env_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN}")' "$workflow_file" grep -Fq 'remove_env_vars+=("IB_GATEWAY_ZONE")' "$workflow_file" grep -Fq 'remove_env_vars+=("IB_GATEWAY_IP_MODE")' "$workflow_file"