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
7 changes: 5 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON=

# Optional Google Voice/SMS channel for escalated strategy plugin alerts.
CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS=
CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER=
CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD=
CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL=
CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD=
CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST=
CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT=
CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY=

# Runtime safety controls.
FIRSTRADE_COOKIE_DIR=.runtime/firstrade-cookies
Expand Down
20 changes: 14 additions & 6 deletions .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@ jobs:
FIRSTRADE_STRATEGY_CONFIG_PATH: ${{ vars.FIRSTRADE_STRATEGY_CONFIG_PATH }}
FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON }}
CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS }}
CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER }}
CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME }}
CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL }}
CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME }}
CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST }}
CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT }}
CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY }}
FIRSTRADE_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS: ${{ vars.FIRSTRADE_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS }}
FIRSTRADE_TECH_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS: ${{ vars.FIRSTRADE_TECH_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS }}
INCOME_THRESHOLD_USD: ${{ vars.INCOME_THRESHOLD_USD }}
Expand All @@ -72,7 +75,7 @@ jobs:
GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}
NOTIFY_LANG: ${{ vars.NOTIFY_LANG }}
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD: ${{ secrets.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD }}
CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD: ${{ secrets.CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD }}
FIRSTRADE_USERNAME: ${{ secrets.FIRSTRADE_USERNAME }}
FIRSTRADE_PASSWORD: ${{ secrets.FIRSTRADE_PASSWORD }}
FIRSTRADE_MFA_SECRET: ${{ secrets.FIRSTRADE_MFA_SECRET }}
Expand Down Expand Up @@ -372,6 +375,8 @@ jobs:
"TELEGRAM_CHAT_ID"
"CRISIS_ALERT_GOOGLE_VOICE_TO"
"CRISIS_ALERT_GOOGLE_VOICE_GATEWAY"
"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER"
"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD"
"CRISIS_ALERT_SMTP_FROM"
"CRISIS_ALERT_SMTP_HOST"
"CRISIS_ALERT_SMTP_PORT"
Expand All @@ -380,7 +385,7 @@ jobs:
"CRISIS_ALERT_SMTP_STARTTLS"
"CRISIS_ALERT_SMTP_SSL"
)
remove_secret_vars=("CRISIS_ALERT_SMTP_PASSWORD")
remove_secret_vars=("CRISIS_ALERT_SMTP_PASSWORD" "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD")

add_optional_env() {
local name="$1"
Expand Down Expand Up @@ -438,7 +443,10 @@ jobs:
add_optional_env FIRSTRADE_STRATEGY_CONFIG_PATH
add_optional_env FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON
add_optional_env CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS
add_optional_env CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER
add_optional_env CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL
add_optional_env CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST
add_optional_env CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT
add_optional_env CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY
add_optional_env FIRSTRADE_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS
add_optional_env FIRSTRADE_TECH_RUNTIME_EXECUTION_WINDOW_TRADING_DAYS
add_optional_env INCOME_THRESHOLD_USD
Expand All @@ -448,7 +456,7 @@ jobs:
add_optional_env NOTIFY_LANG

add_optional_secret TELEGRAM_TOKEN TELEGRAM_TOKEN_SECRET_NAME TELEGRAM_TOKEN
add_optional_secret CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD
add_optional_secret CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD
add_optional_secret FIRSTRADE_USERNAME FIRSTRADE_USERNAME_SECRET_NAME FIRSTRADE_USERNAME
add_optional_secret FIRSTRADE_PASSWORD FIRSTRADE_PASSWORD_SECRET_NAME FIRSTRADE_PASSWORD
add_optional_secret FIRSTRADE_MFA_SECRET FIRSTRADE_MFA_SECRET_SECRET_NAME FIRSTRADE_MFA_SECRET
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,11 @@ commit credentials.
| `GLOBAL_TELEGRAM_CHAT_ID` | Optional | Telegram chat ID for strategy-cycle summaries |
| `FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON` | Optional | JSON sidecar plugin mount config. Overrides global `STRATEGY_PLUGIN_MOUNTS_JSON` for this platform |
| `CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS` | Optional | Email-form recipients. Use a normal mailbox for email-only delivery, or a Google Voice mailbox/address to also trigger GV prompts |
| `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER` | Optional | Gmail address used as the sender for Google Voice notification mail |
| `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD` | Optional | Gmail App Password for the sender account, preferably supplied from Secret Manager in Cloud Run |
| `CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL` | Optional | Sender email address used for Google Voice notification mail. Gmail is the default transport, but the sender naming is provider-neutral |
| `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD` | Optional | Sender SMTP password or app password, preferably supplied from Secret Manager in Cloud Run |
| `CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST` | Optional | SMTP host override. Defaults to Gmail SMTP when unset |
| `CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT` | Optional | SMTP port override. Defaults to `465` when unset |
| `CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY` | Optional | SMTP security override: `ssl`, `starttls`, or `none`. Defaults to `ssl` when unset |
| `FIRSTRADE_COOKIE_DIR` | Optional | Cookie cache directory, default `.runtime/firstrade-cookies` |
| `FIRSTRADE_ENABLE_LIVE_TRADING` | Optional | Must be `true` before any live order can be submitted |
| `FIRSTRADE_RUN_SMOKE_ON_HTTP` | Optional | Must be `true` before `/smoke` performs a real login/quote |
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ authors = [
]
dependencies = [
"firstrade==0.0.38",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@705667fbb88b743eb83e858b1cc42fe9ebc3a87a",
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@aca4e1449e9e87e0222b8a1a5e0dd84f822d516c",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@43b2ecb9dc7b1a70e52fe038ce321d79f5f7987a",
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@a3f1899079d8d6c11601dddc40cb6f3020b6fc82",
"google-cloud-storage",
"requests",
]
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
flask
gunicorn
firstrade==0.0.38
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@705667fbb88b743eb83e858b1cc42fe9ebc3a87a
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@aca4e1449e9e87e0222b8a1a5e0dd84f822d516c
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@43b2ecb9dc7b1a70e52fe038ce321d79f5f7987a
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@a3f1899079d8d6c11601dddc40cb6f3020b6fc82
google-cloud-storage
requests
pytest
18 changes: 13 additions & 5 deletions runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,11 @@ class PlatformRuntimeSettings:
strategy_config_source: str | None = None
strategy_plugin_mounts_json: str | None = None
crisis_alert_google_voice_recipients: tuple[str, ...] = ()
crisis_alert_google_voice_gmail_user: str | None = None
crisis_alert_google_voice_gmail_app_password: str | None = None
crisis_alert_google_voice_sender_email: str | None = None
crisis_alert_google_voice_sender_password: str | None = None
crisis_alert_google_voice_smtp_host: str | None = None
crisis_alert_google_voice_smtp_port: str | None = None
crisis_alert_google_voice_smtp_security: str | None = None
runtime_target: RuntimeTarget | None = None


Expand Down Expand Up @@ -149,9 +152,14 @@ def load_platform_runtime_settings(
or os.getenv("STRATEGY_PLUGIN_MOUNTS_JSON")
),
crisis_alert_google_voice_recipients=_split_env_list(os.getenv("CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS")),
crisis_alert_google_voice_gmail_user=_first_non_empty(os.getenv("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER")),
crisis_alert_google_voice_gmail_app_password=_first_non_empty(
os.getenv("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD")
crisis_alert_google_voice_sender_email=_first_non_empty(os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL")),
crisis_alert_google_voice_sender_password=_first_non_empty(
os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD")
Comment on lines +155 to +157

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve legacy Google Voice env fallback during rename

This change drops support for CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER/CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD without a fallback, so deployments that still have only the old vars configured will silently lose Google Voice sender credentials after this commit. That failure path is realistic because the workflow can skip env-sync when ENABLE_GITHUB_ENV_SYNC is not true (see .github/workflows/sync-cloud-run-env.yml), leaving old Cloud Run vars in place while the runtime now reads only the new names. Please keep reading the legacy names as a fallback until all environments are migrated.

Useful? React with 👍 / 👎.

),
crisis_alert_google_voice_smtp_host=_first_non_empty(os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST")),
crisis_alert_google_voice_smtp_port=_first_non_empty(os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT")),
crisis_alert_google_voice_smtp_security=_first_non_empty(
os.getenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY")
),
runtime_target=runtime_target,
)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ def test_run_strategy_cycle_loads_strategy_plugin_report_and_sends_email(
settings = _runtime_settings_with_persistence(
strategy_plugin_mounts_json=mount_config,
crisis_alert_google_voice_recipients=("voice@example.com",),
crisis_alert_google_voice_gmail_user="bot@example.com",
crisis_alert_google_voice_gmail_app_password="app-password",
crisis_alert_google_voice_sender_email="bot@example.com",
crisis_alert_google_voice_sender_password="app-password",
)
messages = []
observed_alerts = []
Expand Down
21 changes: 15 additions & 6 deletions tests/test_runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,11 @@ def test_reserved_cash_policy_defaults_to_zero(monkeypatch):
assert settings.reserved_cash_floor_usd == 0.0
assert settings.reserved_cash_ratio == 0.0
assert settings.crisis_alert_google_voice_recipients == ()
assert settings.crisis_alert_google_voice_gmail_user is None
assert settings.crisis_alert_google_voice_gmail_app_password is None
assert settings.crisis_alert_google_voice_sender_email is None
assert settings.crisis_alert_google_voice_sender_password is None
assert settings.crisis_alert_google_voice_smtp_host is None
assert settings.crisis_alert_google_voice_smtp_port is None
assert settings.crisis_alert_google_voice_smtp_security is None


def test_reserved_cash_policy_loads_from_env(monkeypatch):
Expand All @@ -72,14 +75,20 @@ def test_reserved_cash_policy_loads_from_env(monkeypatch):
def test_crisis_alert_google_voice_settings_load_from_env(monkeypatch):
monkeypatch.setenv("RUNTIME_TARGET_JSON", _target_json())
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS", "alerts@example.com; voice@example.com")
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER", "sender@gmail.com")
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD", "secret")
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL", "sender@example.com")
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD", "secret")
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST", "smtp.example.com")
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT", "587")
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY", "starttls")

settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1")

assert settings.crisis_alert_google_voice_recipients == ("alerts@example.com", "voice@example.com")
assert settings.crisis_alert_google_voice_gmail_user == "sender@gmail.com"
assert settings.crisis_alert_google_voice_gmail_app_password == "secret"
assert settings.crisis_alert_google_voice_sender_email == "sender@example.com"
assert settings.crisis_alert_google_voice_sender_password == "secret"
assert settings.crisis_alert_google_voice_smtp_host == "smtp.example.com"
assert settings.crisis_alert_google_voice_smtp_port == "587"
assert settings.crisis_alert_google_voice_smtp_security == "starttls"


def test_reserved_cash_ratio_rejects_invalid_env(monkeypatch):
Expand Down
17 changes: 11 additions & 6 deletions tests/test_sync_cloud_run_env_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,25 @@ def test_sync_cloud_run_env_workflow_syncs_crisis_alert_settings():

for name in (
"CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS",
"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER",
"CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL",
"CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST",
"CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT",
"CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY",
):
assert f"{name}: ${{{{ vars.{name} }}}}" in workflow
assert f"add_optional_env {name}" in workflow

assert (
"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME: "
"${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME }}"
"CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME: "
"${{ vars.CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME }}"
) in workflow
assert "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD: ${{ secrets.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD }}" in workflow
assert "CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD: ${{ secrets.CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD }}" in workflow
assert (
"add_optional_secret CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD "
"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD"
"add_optional_secret CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD "
"CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD_SECRET_NAME CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD"
) in workflow
assert '"CRISIS_ALERT_GOOGLE_VOICE_TO"' in workflow
assert '"CRISIS_ALERT_GOOGLE_VOICE_GATEWAY"' in workflow
assert '"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER"' in workflow
assert '"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD"' in workflow
assert '"CRISIS_ALERT_SMTP_HOST"' in workflow