Skip to content

Commit cf452d7

Browse files
committed
Use shared plugin email alert publisher
1 parent 4434cfe commit cf452d7

4 files changed

Lines changed: 59 additions & 37 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ Current behavior is fail-fast:
216216
- missing key fields in the selected group (`ib_gateway_instance_name`, `ib_gateway_mode`, `ib_client_id`) → startup error
217217

218218
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 when the `CRISIS_ALERT_*` SMTP settings are complete.
219+
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.
219220

220221
### GitHub-managed Cloud Run env sync
221222

@@ -433,6 +434,7 @@ IB_GATEWAY_IP_MODE=internal
433434
- 选中的账号组缺少关键字段(`ib_gateway_instance_name``ib_gateway_mode``ib_client_id`)→ 启动直接报错
434435

435436
如果 `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_*` SMTP 配置完整,服务还会额外发一封独立危机邮件。
437+
邮件告警结果会写入 runtime report。重复发送抑制使用稳定的插件告警 key;如配置了 `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI` 则写入该前缀,否则复用 `EXECUTION_REPORT_GCS_URI`,并有本地 `/tmp` marker fallback。
436438

437439
### GitHub 统一管理 Cloud Run 环境变量
438440

main.py

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525
from decision_mapper import map_strategy_decision
2626
from entrypoints.cloud_run import is_market_open_today
2727
from notifications.telegram import build_strategy_display_name, build_translator, send_telegram_message
28-
from quant_platform_kit.notifications.email import send_smtp_email
28+
from quant_platform_kit.notifications.strategy_plugin_email import (
29+
StrategyPluginEmailAlertMarkerStore,
30+
build_strategy_plugin_alert_context_label as build_email_alert_context_label,
31+
publish_strategy_plugin_email_alerts,
32+
)
2933
from quant_platform_kit.common.runtime_assembly import build_runtime_assembly
3034
from quant_platform_kit.common.runtime_reports import (
3135
append_runtime_report_error,
@@ -233,14 +237,6 @@ def _env_flag(name: str) -> bool:
233237
TG_TOKEN = RUNTIME_SETTINGS.tg_token
234238
TG_CHAT_ID = RUNTIME_SETTINGS.tg_chat_id
235239
NOTIFY_LANG = RUNTIME_SETTINGS.notify_lang
236-
CRISIS_ALERT_EMAIL_TO = getattr(RUNTIME_SETTINGS, "crisis_alert_email_to", ())
237-
CRISIS_ALERT_EMAIL_FROM = getattr(RUNTIME_SETTINGS, "crisis_alert_email_from", None)
238-
CRISIS_ALERT_SMTP_HOST = getattr(RUNTIME_SETTINGS, "crisis_alert_smtp_host", None)
239-
CRISIS_ALERT_SMTP_PORT = getattr(RUNTIME_SETTINGS, "crisis_alert_smtp_port", 587)
240-
CRISIS_ALERT_SMTP_USERNAME = getattr(RUNTIME_SETTINGS, "crisis_alert_smtp_username", None)
241-
CRISIS_ALERT_SMTP_PASSWORD = getattr(RUNTIME_SETTINGS, "crisis_alert_smtp_password", None)
242-
CRISIS_ALERT_SMTP_STARTTLS = getattr(RUNTIME_SETTINGS, "crisis_alert_smtp_starttls", True)
243-
CRISIS_ALERT_SMTP_SSL = getattr(RUNTIME_SETTINGS, "crisis_alert_smtp_ssl", False)
244240

245241
CASH_RESERVE_RATIO = STRATEGY_RUNTIME.cash_reserve_ratio
246242
CASH_RESERVE_FLOOR_USD = getattr(STRATEGY_RUNTIME, "cash_reserve_floor_usd", 0.0)
@@ -481,29 +477,42 @@ def build_strategy_plugin_alert_messages(signals):
481477
return build_strategy_adapters().build_strategy_plugin_alert_messages(signals)
482478

483479

484-
def send_crisis_alert_email(alert_message) -> bool:
485-
return send_smtp_email(
486-
subject=alert_message.subject,
487-
body=alert_message.body,
488-
smtp_host=CRISIS_ALERT_SMTP_HOST,
489-
smtp_port=CRISIS_ALERT_SMTP_PORT,
490-
sender=CRISIS_ALERT_EMAIL_FROM,
491-
recipients=CRISIS_ALERT_EMAIL_TO,
492-
username=CRISIS_ALERT_SMTP_USERNAME,
493-
password=CRISIS_ALERT_SMTP_PASSWORD,
494-
use_starttls=CRISIS_ALERT_SMTP_STARTTLS,
495-
use_ssl=CRISIS_ALERT_SMTP_SSL,
480+
def build_strategy_plugin_alert_store():
481+
return StrategyPluginEmailAlertMarkerStore(
482+
local_dir=os.getenv("STRATEGY_PLUGIN_ALERT_STATE_DIR") or "/tmp/quant_strategy_plugin_alerts",
483+
gcs_prefix_uri=os.getenv("STRATEGY_PLUGIN_ALERT_STATE_GCS_URI") or os.getenv("EXECUTION_REPORT_GCS_URI"),
484+
gcp_project_id=PROJECT_ID,
496485
)
497486

498487

499-
def publish_strategy_plugin_alerts(signals) -> int:
500-
sent_count = 0
501-
for alert_message in build_strategy_plugin_alert_messages(signals):
502-
if send_crisis_alert_email(alert_message):
503-
sent_count += 1
504-
if sent_count:
505-
print(f"strategy_plugin_alert_email_sent count={sent_count}", flush=True)
506-
return sent_count
488+
def build_strategy_plugin_alert_context_label() -> str:
489+
return build_email_alert_context_label(
490+
platform_id="ibkr",
491+
strategy_profile=STRATEGY_PROFILE,
492+
account_scope=ACCOUNT_GROUP,
493+
service_name=SERVICE_NAME,
494+
runtime_target=RUNTIME_SETTINGS.runtime_target,
495+
)
496+
497+
498+
def attach_strategy_plugin_alert_email_result(report, result) -> None:
499+
report.setdefault("summary", {})["strategy_plugin_alert_email_sent_count"] = result.sent_count
500+
report.setdefault("diagnostics", {}).update(result.to_report_fields())
501+
502+
503+
def publish_strategy_plugin_alerts(signals, *, report=None):
504+
result = publish_strategy_plugin_email_alerts(
505+
signals,
506+
email_settings=RUNTIME_SETTINGS,
507+
translator=t,
508+
strategy_label=STRATEGY_PROFILE,
509+
context_label=build_strategy_plugin_alert_context_label(),
510+
alert_store=build_strategy_plugin_alert_store(),
511+
log_message=print,
512+
)
513+
if report is not None:
514+
attach_strategy_plugin_alert_email_result(report, result)
515+
return result
507516

508517

509518
def build_account_notification_lines() -> tuple[str, ...]:
@@ -640,7 +649,7 @@ def _handle_request(*, dry_run_only_override: bool | None = None, response_body:
640649
execution_window="precheck" if dry_run_only_override else "execution",
641650
)
642651
if dry_run_only_override is None:
643-
publish_strategy_plugin_alerts(strategy_plugin_signals)
652+
publish_strategy_plugin_alerts(strategy_plugin_signals, report=report)
644653
cycle_result = coerce_strategy_cycle_result(
645654
run_strategy_core(
646655
strategy_plugin_signals=strategy_plugin_signals,

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
flask
22
gunicorn
3-
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@1b6febbba7df81179ad7579f430c26a811c0e1a8
3+
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@ba67541711228f5a72a294def0e5cc24cc5479f3
44
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@1636271a3e0c17fc0c5da363f67eabe114eeff48
55
pandas
66
numpy

tests/test_request_handling.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,21 @@ def test_handle_request_sends_escalated_strategy_plugin_alert(strategy_module, m
4848
monkeypatch.setattr(strategy_module, "is_market_open_today", lambda: True)
4949
monkeypatch.setattr(strategy_module, "load_strategy_plugin_signals", lambda: ((signal,), None))
5050
monkeypatch.setattr(strategy_module, "attach_strategy_plugin_report", lambda *args, **kwargs: None)
51-
monkeypatch.setattr(
52-
strategy_module,
53-
"send_crisis_alert_email",
54-
lambda alert_message: observed["alerts"].append(alert_message) or True,
55-
)
51+
52+
def fake_publish(signals, **kwargs):
53+
observed["alerts"].append((tuple(signals), kwargs))
54+
return types.SimpleNamespace(
55+
sent_count=1,
56+
to_report_fields=lambda: {
57+
"strategy_plugin_alert_email_attempted_count": 1,
58+
"strategy_plugin_alert_email_sent_count": 1,
59+
"strategy_plugin_alert_email_skipped_count": 0,
60+
"strategy_plugin_alert_email_failed_count": 0,
61+
"strategy_plugin_alert_email_deliveries": [],
62+
},
63+
)
64+
65+
monkeypatch.setattr(strategy_module, "publish_strategy_plugin_email_alerts", fake_publish)
5666
monkeypatch.setattr(strategy_module, "run_strategy_core", lambda **_kwargs: "OK - executed")
5767

5868
with strategy_module.app.test_request_context("/", method="POST"):
@@ -61,7 +71,8 @@ def test_handle_request_sends_escalated_strategy_plugin_alert(strategy_module, m
6171
assert status == 200
6272
assert body == "OK - executed"
6373
assert len(observed["alerts"]) == 1
64-
assert "Crisis" in observed["alerts"][0].subject
74+
assert observed["alerts"][0][0] == (signal,)
75+
assert "ibkr" in observed["alerts"][0][1]["context_label"]
6576

6677

6778
def test_handle_precheck_post_uses_dry_run_override(strategy_module, monkeypatch):

0 commit comments

Comments
 (0)