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
11 changes: 3 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,9 @@ GLOBAL_TELEGRAM_CHAT_ID=
FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON=

# Optional Google Voice/SMS channel for escalated strategy plugin alerts.
CRISIS_ALERT_GOOGLE_VOICE_TO=
CRISIS_ALERT_SMTP_FROM=
CRISIS_ALERT_SMTP_HOST=
CRISIS_ALERT_SMTP_PORT=587
CRISIS_ALERT_SMTP_USERNAME=
CRISIS_ALERT_SMTP_PASSWORD=
CRISIS_ALERT_SMTP_STARTTLS=true
CRISIS_ALERT_SMTP_SSL=false
CRISIS_ALERT_GOOGLE_VOICE_GATEWAY=
CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER=
CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD=

# Runtime safety controls.
FIRSTRADE_COOKIE_DIR=.runtime/firstrade-cookies
Expand Down
34 changes: 16 additions & 18 deletions .github/workflows/sync-cloud-run-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,9 @@ jobs:
FIRSTRADE_STATE_PREFIX: ${{ vars.FIRSTRADE_STATE_PREFIX }}
FIRSTRADE_STRATEGY_CONFIG_PATH: ${{ vars.FIRSTRADE_STRATEGY_CONFIG_PATH }}
FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON }}
CRISIS_ALERT_GOOGLE_VOICE_TO: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_TO }}
CRISIS_ALERT_SMTP_FROM: ${{ vars.CRISIS_ALERT_SMTP_FROM }}
CRISIS_ALERT_SMTP_HOST: ${{ vars.CRISIS_ALERT_SMTP_HOST }}
CRISIS_ALERT_SMTP_PORT: ${{ vars.CRISIS_ALERT_SMTP_PORT }}
CRISIS_ALERT_SMTP_USERNAME: ${{ vars.CRISIS_ALERT_SMTP_USERNAME }}
CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME: ${{ vars.CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME }}
CRISIS_ALERT_SMTP_STARTTLS: ${{ vars.CRISIS_ALERT_SMTP_STARTTLS }}
CRISIS_ALERT_SMTP_SSL: ${{ vars.CRISIS_ALERT_SMTP_SSL }}
CRISIS_ALERT_GOOGLE_VOICE_GATEWAY: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GATEWAY }}
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 }}
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 @@ -77,7 +72,7 @@ jobs:
GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}
NOTIFY_LANG: ${{ vars.NOTIFY_LANG }}
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
CRISIS_ALERT_SMTP_PASSWORD: ${{ secrets.CRISIS_ALERT_SMTP_PASSWORD }}
CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD: ${{ secrets.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD }}
FIRSTRADE_USERNAME: ${{ secrets.FIRSTRADE_USERNAME }}
FIRSTRADE_PASSWORD: ${{ secrets.FIRSTRADE_PASSWORD }}
FIRSTRADE_MFA_SECRET: ${{ secrets.FIRSTRADE_MFA_SECRET }}
Expand Down Expand Up @@ -375,8 +370,16 @@ jobs:
secret_pairs=()
remove_env_vars=(
"TELEGRAM_CHAT_ID"
"CRISIS_ALERT_GOOGLE_VOICE_TO"
"CRISIS_ALERT_SMTP_FROM"
"CRISIS_ALERT_SMTP_HOST"
"CRISIS_ALERT_SMTP_PORT"
"CRISIS_ALERT_SMTP_USERNAME"
"CRISIS_ALERT_SMTP_PASSWORD"
"CRISIS_ALERT_SMTP_STARTTLS"
"CRISIS_ALERT_SMTP_SSL"
)
remove_secret_vars=()
remove_secret_vars=("CRISIS_ALERT_SMTP_PASSWORD")

add_optional_env() {
local name="$1"
Expand Down Expand Up @@ -433,13 +436,8 @@ jobs:
add_optional_env FIRSTRADE_FEATURE_SNAPSHOT_MANIFEST_PATH
add_optional_env FIRSTRADE_STRATEGY_CONFIG_PATH
add_optional_env FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON
add_optional_env CRISIS_ALERT_GOOGLE_VOICE_TO
add_optional_env CRISIS_ALERT_SMTP_FROM
add_optional_env CRISIS_ALERT_SMTP_HOST
add_optional_env CRISIS_ALERT_SMTP_PORT
add_optional_env CRISIS_ALERT_SMTP_USERNAME
add_optional_env CRISIS_ALERT_SMTP_STARTTLS
add_optional_env CRISIS_ALERT_SMTP_SSL
add_optional_env CRISIS_ALERT_GOOGLE_VOICE_GATEWAY
add_optional_env CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER
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 @@ -449,7 +447,7 @@ jobs:
add_optional_env NOTIFY_LANG

add_optional_secret TELEGRAM_TOKEN TELEGRAM_TOKEN_SECRET_NAME TELEGRAM_TOKEN
add_optional_secret CRISIS_ALERT_SMTP_PASSWORD CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME CRISIS_ALERT_SMTP_PASSWORD
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 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
11 changes: 3 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,9 @@ commit credentials.
| `TELEGRAM_TOKEN` | Optional | Telegram bot token for strategy-cycle summaries |
| `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_TO` | Optional | Google Voice SMS gateway recipients, usually ending in `@txt.voice.google.com` |
| `CRISIS_ALERT_SMTP_FROM` | Optional | SMTP sender address for Google Voice alerts |
| `CRISIS_ALERT_SMTP_HOST` | Optional | SMTP host for Google Voice alerts |
| `CRISIS_ALERT_SMTP_PORT` | Optional | SMTP port. Defaults to `587` |
| `CRISIS_ALERT_SMTP_USERNAME` | Optional | SMTP username when authentication is required |
| `CRISIS_ALERT_SMTP_PASSWORD` | Optional | SMTP password, preferably supplied from Secret Manager in Cloud Run |
| `CRISIS_ALERT_SMTP_STARTTLS` | Optional | Enable STARTTLS for SMTP. Defaults to `true` |
| `CRISIS_ALERT_SMTP_SSL` | Optional | Use SMTP over SSL. Defaults to `false` |
| `CRISIS_ALERT_GOOGLE_VOICE_GATEWAY` | Optional | Google Voice SMS gateway recipients, usually ending in `@txt.voice.google.com` |
| `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER` | Optional | Gmail address used to send Google Voice gateway alerts |
| `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD` | Optional | Gmail App Password, preferably supplied from Secret Manager in Cloud Run |
| `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@4305a3c01151ced7e78b39519959444309326cd7",
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@03e23c5d6a620ca9ecb763550452f9ca3870481a",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@34a2c74deb7aafef0f6f4b278444d7b0efb76794",
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@861eaedc1caecf1e33ff12f55bbe9af87a221df1",
"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@4305a3c01151ced7e78b39519959444309326cd7
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@03e23c5d6a620ca9ecb763550452f9ca3870481a
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@34a2c74deb7aafef0f6f4b278444d7b0efb76794
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@861eaedc1caecf1e33ff12f55bbe9af87a221df1
google-cloud-storage
requests
pytest
44 changes: 8 additions & 36 deletions runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,9 @@ class PlatformRuntimeSettings:
strategy_config_path: str | None = None
strategy_config_source: str | None = None
strategy_plugin_mounts_json: str | None = None
crisis_alert_google_voice_to: tuple[str, ...] = ()
crisis_alert_smtp_from: str | None = None
crisis_alert_smtp_host: str | None = None
crisis_alert_smtp_port: int = 587
crisis_alert_smtp_username: str | None = None
crisis_alert_smtp_password: str | None = None
crisis_alert_smtp_starttls: bool = True
crisis_alert_smtp_ssl: bool = False
crisis_alert_google_voice_gateway: tuple[str, ...] = ()
crisis_alert_google_voice_gmail_user: str | None = None
crisis_alert_google_voice_gmail_app_password: str | None = None
runtime_target: RuntimeTarget | None = None


Expand Down Expand Up @@ -153,14 +148,11 @@ def load_platform_runtime_settings(
os.getenv("FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON")
or os.getenv("STRATEGY_PLUGIN_MOUNTS_JSON")
),
crisis_alert_google_voice_to=_split_env_list(os.getenv("CRISIS_ALERT_GOOGLE_VOICE_TO")),
crisis_alert_smtp_from=_first_non_empty(os.getenv("CRISIS_ALERT_SMTP_FROM")),
crisis_alert_smtp_host=_first_non_empty(os.getenv("CRISIS_ALERT_SMTP_HOST")),
crisis_alert_smtp_port=_resolve_positive_int_env("CRISIS_ALERT_SMTP_PORT", default=587),
crisis_alert_smtp_username=_first_non_empty(os.getenv("CRISIS_ALERT_SMTP_USERNAME")),
crisis_alert_smtp_password=_first_non_empty(os.getenv("CRISIS_ALERT_SMTP_PASSWORD")),
crisis_alert_smtp_starttls=_resolve_bool_env("CRISIS_ALERT_SMTP_STARTTLS", default=True),
crisis_alert_smtp_ssl=_resolve_bool_env("CRISIS_ALERT_SMTP_SSL", default=False),
crisis_alert_google_voice_gateway=_split_env_list(os.getenv("CRISIS_ALERT_GOOGLE_VOICE_GATEWAY")),
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")
),
runtime_target=runtime_target,
)

Expand Down Expand Up @@ -227,26 +219,6 @@ def _first_non_empty(*raw_values: str | None) -> str | None:
return None


def _resolve_bool_env(name: str, *, default: bool) -> bool:
raw_value = os.getenv(name)
if raw_value is None or str(raw_value).strip() == "":
return bool(default)
return resolve_bool_value(raw_value)


def _resolve_positive_int_env(name: str, *, default: int) -> int:
raw_value = os.getenv(name)
if raw_value is None or str(raw_value).strip() == "":
return int(default)
try:
value = int(raw_value)
except (TypeError, ValueError):
raise ValueError(f"{name} must be a positive integer, got {raw_value!r}") from None
if value <= 0:
raise ValueError(f"{name} must be a positive integer, got {raw_value!r}")
return value


def _split_env_list(raw_value: str | None) -> tuple[str, ...]:
if raw_value is None:
return ()
Expand Down
6 changes: 3 additions & 3 deletions tests/test_rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,9 @@ 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_to=("gateway@txt.voice.google.com",),
crisis_alert_smtp_from="bot@example.com",
crisis_alert_smtp_host="smtp.example.com",
crisis_alert_google_voice_gateway=("gateway@txt.voice.google.com",),
crisis_alert_google_voice_gmail_user="bot@example.com",
crisis_alert_google_voice_gmail_app_password="app-password",
)
messages = []
observed_alerts = []
Expand Down
30 changes: 9 additions & 21 deletions tests/test_runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,9 @@ 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_to == ()
assert settings.crisis_alert_smtp_from is None
assert settings.crisis_alert_smtp_port == 587
assert settings.crisis_alert_smtp_starttls is True
assert settings.crisis_alert_smtp_ssl is False
assert settings.crisis_alert_google_voice_gateway == ()
assert settings.crisis_alert_google_voice_gmail_user is None
assert settings.crisis_alert_google_voice_gmail_app_password is None


def test_reserved_cash_policy_loads_from_env(monkeypatch):
Expand All @@ -73,25 +71,15 @@ 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_TO", "gateway@txt.voice.google.com")
monkeypatch.setenv("CRISIS_ALERT_SMTP_FROM", "smtp-from@example.com")
monkeypatch.setenv("CRISIS_ALERT_SMTP_HOST", "smtp.example.com")
monkeypatch.setenv("CRISIS_ALERT_SMTP_PORT", "465")
monkeypatch.setenv("CRISIS_ALERT_SMTP_USERNAME", "bot")
monkeypatch.setenv("CRISIS_ALERT_SMTP_PASSWORD", "secret")
monkeypatch.setenv("CRISIS_ALERT_SMTP_STARTTLS", "false")
monkeypatch.setenv("CRISIS_ALERT_SMTP_SSL", "true")
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_GATEWAY", "gateway@txt.voice.google.com")
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER", "sender@gmail.com")
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD", "secret")

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

assert settings.crisis_alert_google_voice_to == ("gateway@txt.voice.google.com",)
assert settings.crisis_alert_smtp_from == "smtp-from@example.com"
assert settings.crisis_alert_smtp_host == "smtp.example.com"
assert settings.crisis_alert_smtp_port == 465
assert settings.crisis_alert_smtp_username == "bot"
assert settings.crisis_alert_smtp_password == "secret"
assert settings.crisis_alert_smtp_starttls is False
assert settings.crisis_alert_smtp_ssl is True
assert settings.crisis_alert_google_voice_gateway == ("gateway@txt.voice.google.com",)
assert settings.crisis_alert_google_voice_gmail_user == "sender@gmail.com"
assert settings.crisis_alert_google_voice_gmail_app_password == "secret"


def test_reserved_cash_ratio_rejects_invalid_env(monkeypatch):
Expand Down
21 changes: 9 additions & 12 deletions tests/test_sync_cloud_run_env_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,20 @@ def test_sync_cloud_run_env_workflow_syncs_crisis_alert_settings():
workflow = workflow_path.read_text(encoding="utf-8")

for name in (
"CRISIS_ALERT_GOOGLE_VOICE_TO",
"CRISIS_ALERT_SMTP_FROM",
"CRISIS_ALERT_SMTP_HOST",
"CRISIS_ALERT_SMTP_PORT",
"CRISIS_ALERT_SMTP_USERNAME",
"CRISIS_ALERT_SMTP_STARTTLS",
"CRISIS_ALERT_SMTP_SSL",
"CRISIS_ALERT_GOOGLE_VOICE_GATEWAY",
"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER",
):
assert f"{name}: ${{{{ vars.{name} }}}}" in workflow
assert f"add_optional_env {name}" in workflow

assert (
"CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME: "
"${{ vars.CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME }}"
"CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME: "
"${{ vars.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD_SECRET_NAME }}"
) in workflow
assert "CRISIS_ALERT_SMTP_PASSWORD: ${{ secrets.CRISIS_ALERT_SMTP_PASSWORD }}" in workflow
assert "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD: ${{ secrets.CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD }}" in workflow
assert (
"add_optional_secret CRISIS_ALERT_SMTP_PASSWORD "
"CRISIS_ALERT_SMTP_PASSWORD_SECRET_NAME CRISIS_ALERT_SMTP_PASSWORD"
"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"
) in workflow
assert '"CRISIS_ALERT_GOOGLE_VOICE_TO"' in workflow
assert '"CRISIS_ALERT_SMTP_HOST"' in workflow