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
48 changes: 48 additions & 0 deletions .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}")
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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`。 |

选中的账号组配置里,至少要有:

Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
24 changes: 24 additions & 0 deletions runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
)

Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 20 additions & 2 deletions tests/test_runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Expand All @@ -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"
Expand All @@ -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):
Expand Down
6 changes: 6 additions & 0 deletions tests/test_sync_cloud_run_env_workflow.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down