Skip to content

Commit 07ee816

Browse files
committed
Rename crisis alert channel to Google Voice
1 parent fe02262 commit 07ee816

10 files changed

Lines changed: 75 additions & 49 deletions

.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ TELEGRAM_TOKEN=
2424
GLOBAL_TELEGRAM_CHAT_ID=
2525
FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON=
2626

27-
# Optional independent email channel for escalated strategy plugin alerts.
27+
# Optional Google Voice/SMS channel for escalated strategy plugin alerts.
28+
CRISIS_ALERT_GOOGLE_VOICE_TO=
2829
CRISIS_ALERT_EMAIL_TO=
30+
CRISIS_ALERT_SMTP_FROM=
2931
CRISIS_ALERT_EMAIL_FROM=
3032
CRISIS_ALERT_SMTP_HOST=
3133
CRISIS_ALERT_SMTP_PORT=587

.github/workflows/sync-cloud-run-env.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ jobs:
6161
FIRSTRADE_STATE_PREFIX: ${{ vars.FIRSTRADE_STATE_PREFIX }}
6262
FIRSTRADE_STRATEGY_CONFIG_PATH: ${{ vars.FIRSTRADE_STRATEGY_CONFIG_PATH }}
6363
FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON: ${{ vars.FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON }}
64+
CRISIS_ALERT_GOOGLE_VOICE_TO: ${{ vars.CRISIS_ALERT_GOOGLE_VOICE_TO }}
6465
CRISIS_ALERT_EMAIL_TO: ${{ vars.CRISIS_ALERT_EMAIL_TO }}
66+
CRISIS_ALERT_SMTP_FROM: ${{ vars.CRISIS_ALERT_SMTP_FROM }}
6567
CRISIS_ALERT_EMAIL_FROM: ${{ vars.CRISIS_ALERT_EMAIL_FROM }}
6668
CRISIS_ALERT_SMTP_HOST: ${{ vars.CRISIS_ALERT_SMTP_HOST }}
6769
CRISIS_ALERT_SMTP_PORT: ${{ vars.CRISIS_ALERT_SMTP_PORT }}
@@ -433,7 +435,9 @@ jobs:
433435
add_optional_env FIRSTRADE_FEATURE_SNAPSHOT_MANIFEST_PATH
434436
add_optional_env FIRSTRADE_STRATEGY_CONFIG_PATH
435437
add_optional_env FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON
438+
add_optional_env CRISIS_ALERT_GOOGLE_VOICE_TO
436439
add_optional_env CRISIS_ALERT_EMAIL_TO
440+
add_optional_env CRISIS_ALERT_SMTP_FROM
437441
add_optional_env CRISIS_ALERT_EMAIL_FROM
438442
add_optional_env CRISIS_ALERT_SMTP_HOST
439443
add_optional_env CRISIS_ALERT_SMTP_PORT

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,11 @@ commit credentials.
8585
| `TELEGRAM_TOKEN` | Optional | Telegram bot token for strategy-cycle summaries |
8686
| `GLOBAL_TELEGRAM_CHAT_ID` | Optional | Telegram chat ID for strategy-cycle summaries |
8787
| `FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON` | Optional | JSON sidecar plugin mount config. Overrides global `STRATEGY_PLUGIN_MOUNTS_JSON` for this platform |
88-
| `CRISIS_ALERT_EMAIL_TO` | Optional | Comma, semicolon, or newline separated recipients for escalated strategy plugin email alerts |
89-
| `CRISIS_ALERT_EMAIL_FROM` | Optional | SMTP sender address for escalated strategy plugin email alerts |
90-
| `CRISIS_ALERT_SMTP_HOST` | Optional | SMTP host for escalated strategy plugin email alerts |
88+
| `CRISIS_ALERT_GOOGLE_VOICE_TO` | Optional | Google Voice SMS gateway recipients, usually ending in `@txt.voice.google.com` |
89+
| `CRISIS_ALERT_EMAIL_TO` | Optional | Ordinary email recipients that receive the same escalated alert; also accepted as a legacy recipient list |
90+
| `CRISIS_ALERT_SMTP_FROM` | Optional | SMTP sender address for Google Voice alerts; falls back to `CRISIS_ALERT_EMAIL_FROM` |
91+
| `CRISIS_ALERT_EMAIL_FROM` | Optional | Legacy SMTP sender alias; prefer `CRISIS_ALERT_SMTP_FROM` |
92+
| `CRISIS_ALERT_SMTP_HOST` | Optional | SMTP host for Google Voice alerts |
9193
| `CRISIS_ALERT_SMTP_PORT` | Optional | SMTP port. Defaults to `587` |
9294
| `CRISIS_ALERT_SMTP_USERNAME` | Optional | SMTP username when authentication is required |
9395
| `CRISIS_ALERT_SMTP_PASSWORD` | Optional | SMTP password, preferably supplied from Secret Manager in Cloud Run |
@@ -179,9 +181,9 @@ full guarded strategy cycle:
179181
- route generated orders through the local safety layer
180182
- publish a compact Telegram summary when `TELEGRAM_TOKEN` and
181183
`GLOBAL_TELEGRAM_CHAT_ID` are configured
182-
- send independent SMTP email alerts for escalated strategy plugin signals when
184+
- send independent Google Voice alerts for escalated strategy plugin signals when
183185
`CRISIS_ALERT_*` is configured
184-
- write email alert results into the response and suppress duplicate plugin
186+
- write Google Voice alert results into the response and suppress duplicate plugin
185187
alert keys through `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI`, `EXECUTION_REPORT_GCS_URI`,
186188
or the configured Firstrade state bucket
187189

@@ -315,8 +317,8 @@ Firstrade 登录、账户/行情读取、下单转换、安全闸和部署 wirin
315317
- `/run` 执行通用美股策略的 dry-run 调仓闭环
316318
- 配置 `TELEGRAM_TOKEN``GLOBAL_TELEGRAM_CHAT_ID` 后发送运行摘要
317319
- 读取通用策略插件信号,并在危机类插件触发时通过 `CRISIS_ALERT_*`
318-
配置发送独立邮件告警
319-
- 在响应中写入邮件告警结果,并通过 `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI`
320+
配置发送独立 Google Voice 告警
321+
- 在响应中写入 Google Voice 告警结果,并通过 `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI`
320322
`EXECUTION_REPORT_GCS_URI` 或已配置的 Firstrade state bucket 抑制重复插件告警 key
321323
- 在你再次确认后,才允许极小金额实盘验证
322324
- 通用 `us_equity` 策略 profile 的平台层接入

application/rebalance_service.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@
5151
parse_strategy_plugin_mounts,
5252
)
5353
from quant_platform_kit.notifications.events import NotificationPublisher, RenderedNotification
54-
from quant_platform_kit.notifications.strategy_plugin_email import (
55-
StrategyPluginEmailAlertMarkerStore,
56-
build_strategy_plugin_alert_context_label as build_email_alert_context_label,
57-
publish_strategy_plugin_email_alerts,
54+
from quant_platform_kit.notifications.strategy_plugin_google_voice import (
55+
StrategyPluginGoogleVoiceAlertMarkerStore,
56+
build_strategy_plugin_alert_context_label as build_google_voice_alert_context_label,
57+
publish_strategy_plugin_google_voice_alerts,
5858
)
5959
from quant_platform_kit.strategy_contracts import build_strategy_evaluation_inputs
6060
from runtime_config_support import PlatformRuntimeSettings, load_platform_runtime_settings
@@ -212,7 +212,7 @@ def attach_strategy_plugin_result(
212212

213213

214214
def build_strategy_plugin_alert_context_label(settings: PlatformRuntimeSettings) -> str:
215-
return build_email_alert_context_label(
215+
return build_google_voice_alert_context_label(
216216
platform_id="firstrade",
217217
strategy_profile=settings.strategy_profile,
218218
account_scope=settings.account_region or settings.account_prefix,
@@ -231,7 +231,7 @@ def build_strategy_plugin_alert_store(
231231
state_bucket = env_reader("FIRSTRADE_GCS_STATE_BUCKET", None)
232232
state_prefix = env_reader("FIRSTRADE_STATE_PREFIX", "firstrade-platform") or "firstrade-platform"
233233
state_gcs_uri = f"gs://{state_bucket}/{state_prefix}" if state_bucket else None
234-
return StrategyPluginEmailAlertMarkerStore(
234+
return StrategyPluginGoogleVoiceAlertMarkerStore(
235235
local_dir=env_reader("STRATEGY_PLUGIN_ALERT_STATE_DIR", None) or "/tmp/quant_strategy_plugin_alerts",
236236
gcs_prefix_uri=explicit_gcs_uri or report_gcs_uri or state_gcs_uri,
237237
gcp_project_id=settings.project_id,
@@ -246,9 +246,9 @@ def publish_strategy_plugin_alerts(
246246
log_message: Callable[..., Any] = print,
247247
env_reader: Callable[[str, str | None], str | None] = os.getenv,
248248
):
249-
return publish_strategy_plugin_email_alerts(
249+
return publish_strategy_plugin_google_voice_alerts(
250250
signals,
251-
email_settings=settings,
251+
google_voice_settings=settings,
252252
translator=translator,
253253
strategy_label=settings.strategy_profile,
254254
context_label=build_strategy_plugin_alert_context_label(settings),
@@ -380,29 +380,29 @@ def run_strategy_cycle(
380380
}
381381
],
382382
"action_done": False,
383-
"strategy_plugin_alert_email_attempted_count": 0,
384-
"strategy_plugin_alert_email_sent_count": 0,
385-
"strategy_plugin_alert_email_skipped_count": 0,
386-
"strategy_plugin_alert_email_failed_count": 0,
387-
"strategy_plugin_alert_email_deliveries": [],
383+
"strategy_plugin_alert_google_voice_attempted_count": 0,
384+
"strategy_plugin_alert_google_voice_sent_count": 0,
385+
"strategy_plugin_alert_google_voice_skipped_count": 0,
386+
"strategy_plugin_alert_google_voice_failed_count": 0,
387+
"strategy_plugin_alert_google_voice_deliveries": [],
388388
}
389389
return attach_strategy_plugin_result(
390390
result,
391391
signals=strategy_plugin_signals,
392392
error=strategy_plugin_error,
393393
translator=translator,
394394
)
395-
strategy_plugin_alert_email_result = None
396-
strategy_plugin_alert_email_error = None
395+
strategy_plugin_alert_google_voice_result = None
396+
strategy_plugin_alert_google_voice_error = None
397397
try:
398-
strategy_plugin_alert_email_result = publish_strategy_plugin_alerts(
398+
strategy_plugin_alert_google_voice_result = publish_strategy_plugin_alerts(
399399
strategy_plugin_signals,
400400
settings=settings,
401401
translator=translator,
402402
env_reader=env_reader,
403403
)
404404
except Exception as exc:
405-
strategy_plugin_alert_email_error = f"{type(exc).__name__}: {exc}"
405+
strategy_plugin_alert_google_voice_error = f"{type(exc).__name__}: {exc}"
406406
strategy_run_persisted = False
407407
strategy_run_persistence_error = None
408408
if persist_strategy_runs:
@@ -479,20 +479,20 @@ def run_strategy_cycle(
479479
result["funding_blocked"] = True
480480
if strategy_run_persistence_error:
481481
result["strategy_run_persistence_error"] = strategy_run_persistence_error
482-
if strategy_plugin_alert_email_result is not None:
483-
result.update(strategy_plugin_alert_email_result.to_report_fields())
482+
if strategy_plugin_alert_google_voice_result is not None:
483+
result.update(strategy_plugin_alert_google_voice_result.to_report_fields())
484484
else:
485485
result.update(
486486
{
487-
"strategy_plugin_alert_email_attempted_count": 0,
488-
"strategy_plugin_alert_email_sent_count": 0,
489-
"strategy_plugin_alert_email_skipped_count": 0,
490-
"strategy_plugin_alert_email_failed_count": 0,
491-
"strategy_plugin_alert_email_deliveries": [],
487+
"strategy_plugin_alert_google_voice_attempted_count": 0,
488+
"strategy_plugin_alert_google_voice_sent_count": 0,
489+
"strategy_plugin_alert_google_voice_skipped_count": 0,
490+
"strategy_plugin_alert_google_voice_failed_count": 0,
491+
"strategy_plugin_alert_google_voice_deliveries": [],
492492
}
493493
)
494-
if strategy_plugin_alert_email_error:
495-
result["strategy_plugin_alert_email_error"] = strategy_plugin_alert_email_error
494+
if strategy_plugin_alert_google_voice_error:
495+
result["strategy_plugin_alert_google_voice_error"] = strategy_plugin_alert_google_voice_error
496496
attach_strategy_plugin_result(
497497
result,
498498
signals=strategy_plugin_signals,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ authors = [
1414
]
1515
dependencies = [
1616
"firstrade==0.0.38",
17-
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@1b6febbba7df81179ad7579f430c26a811c0e1a8",
17+
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@3322c959408edbfdf95799417748c97818333317",
1818
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@1636271a3e0c17fc0c5da363f67eabe114eeff48",
1919
"google-cloud-storage",
2020
"requests",

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
flask
22
gunicorn
33
firstrade==0.0.38
4-
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@ba67541711228f5a72a294def0e5cc24cc5479f3
4+
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@3322c959408edbfdf95799417748c97818333317
55
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@305f2cc0748ec08d001deabc3add6c4eff7fe7ba
66
google-cloud-storage
77
requests

runtime_config_support.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ class PlatformRuntimeSettings:
5858
strategy_config_path: str | None = None
5959
strategy_config_source: str | None = None
6060
strategy_plugin_mounts_json: str | None = None
61+
crisis_alert_google_voice_to: tuple[str, ...] = ()
62+
crisis_alert_smtp_from: str | None = None
6163
crisis_alert_email_to: tuple[str, ...] = ()
6264
crisis_alert_email_from: str | None = None
6365
crisis_alert_smtp_host: str | None = None
@@ -153,6 +155,11 @@ def load_platform_runtime_settings(
153155
os.getenv("FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON")
154156
or os.getenv("STRATEGY_PLUGIN_MOUNTS_JSON")
155157
),
158+
crisis_alert_google_voice_to=_split_env_list(os.getenv("CRISIS_ALERT_GOOGLE_VOICE_TO")),
159+
crisis_alert_smtp_from=_first_non_empty(
160+
os.getenv("CRISIS_ALERT_SMTP_FROM"),
161+
os.getenv("CRISIS_ALERT_EMAIL_FROM"),
162+
),
156163
crisis_alert_email_to=_split_env_list(os.getenv("CRISIS_ALERT_EMAIL_TO")),
157164
crisis_alert_email_from=_first_non_empty(os.getenv("CRISIS_ALERT_EMAIL_FROM")),
158165
crisis_alert_smtp_host=_first_non_empty(os.getenv("CRISIS_ALERT_SMTP_HOST")),
@@ -219,9 +226,12 @@ def _resolve_ratio_env(name: str, *, default: float) -> float:
219226
return value
220227

221228

222-
def _first_non_empty(raw_value: str | None) -> str | None:
223-
value = str(raw_value or "").strip()
224-
return value or None
229+
def _first_non_empty(*raw_values: str | None) -> str | None:
230+
for raw_value in raw_values:
231+
value = str(raw_value or "").strip()
232+
if value:
233+
return value
234+
return None
225235

226236

227237
def _resolve_bool_env(name: str, *, default: bool) -> bool:

tests/test_rebalance_service.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -245,17 +245,17 @@ def fake_publish(signals, **kwargs):
245245
return SimpleNamespace(
246246
sent_count=1,
247247
to_report_fields=lambda: {
248-
"strategy_plugin_alert_email_attempted_count": 1,
249-
"strategy_plugin_alert_email_sent_count": 1,
250-
"strategy_plugin_alert_email_skipped_count": 0,
251-
"strategy_plugin_alert_email_failed_count": 0,
252-
"strategy_plugin_alert_email_deliveries": [
248+
"strategy_plugin_alert_google_voice_attempted_count": 1,
249+
"strategy_plugin_alert_google_voice_sent_count": 1,
250+
"strategy_plugin_alert_google_voice_skipped_count": 0,
251+
"strategy_plugin_alert_google_voice_failed_count": 0,
252+
"strategy_plugin_alert_google_voice_deliveries": [
253253
{"subject": "Crisis plugin alert", "status": "sent"}
254254
],
255255
},
256256
)
257257

258-
monkeypatch.setattr("application.rebalance_service.publish_strategy_plugin_email_alerts", fake_publish)
258+
monkeypatch.setattr("application.rebalance_service.publish_strategy_plugin_google_voice_alerts", fake_publish)
259259

260260
result = run_strategy_cycle(
261261
runtime_settings=settings,
@@ -266,14 +266,14 @@ def fake_publish(signals, **kwargs):
266266
)
267267

268268
assert result["strategy_plugins"][0]["canonical_route"] == "true_crisis"
269-
assert result["strategy_plugin_alert_email_sent_count"] == 1
269+
assert result["strategy_plugin_alert_google_voice_sent_count"] == 1
270270
assert result["strategy_plugin_lines"] == (
271271
"🧩 Plugin: Crisis Watch Notice | status: true crisis | notice: defend",
272272
)
273273
assert len(observed_alerts) == 1
274274
assert observed_alerts[0][0][0].canonical_route == "true_crisis"
275275
assert "firstrade" in observed_alerts[0][1]["context_label"]
276-
assert result["strategy_plugin_alert_email_deliveries"][0]["status"] == "sent"
276+
assert result["strategy_plugin_alert_google_voice_deliveries"][0]["status"] == "sent"
277277
assert "🧩 Plugin: Crisis Watch Notice | status: true crisis | notice: defend" in messages[0]
278278

279279

@@ -295,7 +295,7 @@ def test_run_strategy_cycle_strategy_plugin_load_error_is_non_blocking(monkeypat
295295
assert result["ok"] is True
296296
assert result["action_done"] is True
297297
assert result["strategy_plugin_error"].startswith("JSONDecodeError:")
298-
assert result["strategy_plugin_alert_email_sent_count"] == 0
298+
assert result["strategy_plugin_alert_google_voice_sent_count"] == 0
299299

300300

301301
def test_run_strategy_cycle_persists_strategy_run_state(monkeypatch):

tests/test_runtime_config_support.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ def test_reserved_cash_policy_defaults_to_zero(monkeypatch):
5353

5454
assert settings.reserved_cash_floor_usd == 0.0
5555
assert settings.reserved_cash_ratio == 0.0
56+
assert settings.crisis_alert_google_voice_to == ()
57+
assert settings.crisis_alert_smtp_from is None
5658
assert settings.crisis_alert_email_to == ()
5759
assert settings.crisis_alert_smtp_port == 587
5860
assert settings.crisis_alert_smtp_starttls is True
@@ -70,9 +72,11 @@ def test_reserved_cash_policy_loads_from_env(monkeypatch):
7072
assert settings.reserved_cash_ratio == 0.025
7173

7274

73-
def test_crisis_alert_email_settings_load_from_env(monkeypatch):
75+
def test_crisis_alert_google_voice_settings_load_from_env(monkeypatch):
7476
monkeypatch.setenv("RUNTIME_TARGET_JSON", _target_json())
77+
monkeypatch.setenv("CRISIS_ALERT_GOOGLE_VOICE_TO", "gateway@txt.voice.google.com")
7578
monkeypatch.setenv("CRISIS_ALERT_EMAIL_TO", "risk@example.com;ops@example.com,risk@example.com")
79+
monkeypatch.setenv("CRISIS_ALERT_SMTP_FROM", "smtp-from@example.com")
7680
monkeypatch.setenv("CRISIS_ALERT_EMAIL_FROM", "bot@example.com")
7781
monkeypatch.setenv("CRISIS_ALERT_SMTP_HOST", "smtp.example.com")
7882
monkeypatch.setenv("CRISIS_ALERT_SMTP_PORT", "465")
@@ -83,6 +87,8 @@ def test_crisis_alert_email_settings_load_from_env(monkeypatch):
8387

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

90+
assert settings.crisis_alert_google_voice_to == ("gateway@txt.voice.google.com",)
91+
assert settings.crisis_alert_smtp_from == "smtp-from@example.com"
8692
assert settings.crisis_alert_email_to == ("risk@example.com", "ops@example.com")
8793
assert settings.crisis_alert_email_from == "bot@example.com"
8894
assert settings.crisis_alert_smtp_host == "smtp.example.com"

tests/test_sync_cloud_run_env_workflow.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ def test_sync_cloud_run_env_workflow_syncs_crisis_alert_settings():
88
workflow = workflow_path.read_text(encoding="utf-8")
99

1010
for name in (
11+
"CRISIS_ALERT_GOOGLE_VOICE_TO",
1112
"CRISIS_ALERT_EMAIL_TO",
13+
"CRISIS_ALERT_SMTP_FROM",
1214
"CRISIS_ALERT_EMAIL_FROM",
1315
"CRISIS_ALERT_SMTP_HOST",
1416
"CRISIS_ALERT_SMTP_PORT",

0 commit comments

Comments
 (0)