Skip to content

Commit 2aa1116

Browse files
committed
Use shared plugin email alert publisher
1 parent 81a7fbc commit 2aa1116

4 files changed

Lines changed: 79 additions & 52 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ full guarded strategy cycle:
181181
`GLOBAL_TELEGRAM_CHAT_ID` are configured
182182
- send independent SMTP email alerts for escalated strategy plugin signals when
183183
`CRISIS_ALERT_*` is configured
184+
- write email alert results into the response and suppress duplicate plugin
185+
alert keys through `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI`, `EXECUTION_REPORT_GCS_URI`,
186+
or the configured Firstrade state bucket
184187

185188
The default mode remains dry-run. A live HTTP-triggered strategy order requires
186189
all of these gates:
@@ -313,6 +316,8 @@ Firstrade 登录、账户/行情读取、下单转换、安全闸和部署 wirin
313316
- 配置 `TELEGRAM_TOKEN``GLOBAL_TELEGRAM_CHAT_ID` 后发送运行摘要
314317
- 读取通用策略插件信号,并在危机类插件触发时通过 `CRISIS_ALERT_*`
315318
配置发送独立邮件告警
319+
- 在响应中写入邮件告警结果,并通过 `STRATEGY_PLUGIN_ALERT_STATE_GCS_URI`
320+
`EXECUTION_REPORT_GCS_URI` 或已配置的 Firstrade state bucket 抑制重复插件告警 key
316321
- 在你再次确认后,才允许极小金额实盘验证
317322
- 通用 `us_equity` 策略 profile 的平台层接入
318323

application/rebalance_service.py

Lines changed: 53 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,17 @@
4545
required_semiconductor_rotation_history_lookback,
4646
)
4747
from quant_platform_kit.common.strategy_plugins import (
48-
build_strategy_plugin_alert_messages,
4948
build_strategy_plugin_notification_lines,
5049
build_strategy_plugin_report_payload,
5150
load_configured_strategy_plugin_signals,
5251
parse_strategy_plugin_mounts,
5352
)
54-
from quant_platform_kit.notifications.email import send_smtp_email
5553
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,
58+
)
5659
from quant_platform_kit.strategy_contracts import build_strategy_evaluation_inputs
5760
from runtime_config_support import PlatformRuntimeSettings, load_platform_runtime_settings
5861
from strategy_runtime import load_strategy_runtime
@@ -208,36 +211,30 @@ def attach_strategy_plugin_result(
208211
return result
209212

210213

211-
def _call_log_message(log_message: Callable[..., Any], text: str) -> None:
212-
try:
213-
log_message(text, flush=True)
214-
except TypeError:
215-
log_message(text)
214+
def build_strategy_plugin_alert_context_label(settings: PlatformRuntimeSettings) -> str:
215+
return build_email_alert_context_label(
216+
platform_id="firstrade",
217+
strategy_profile=settings.strategy_profile,
218+
account_scope=settings.account_region or settings.account_prefix,
219+
service_name=settings.account_prefix,
220+
runtime_target=settings.runtime_target,
221+
)
216222

217223

218-
def send_crisis_alert_email(
219-
alert_message,
220-
*,
224+
def build_strategy_plugin_alert_store(
221225
settings: PlatformRuntimeSettings,
222-
smtp_module=None,
223-
log_message: Callable[..., Any] = print,
224-
) -> bool:
225-
send_kwargs: dict[str, Any] = {}
226-
if smtp_module is not None:
227-
send_kwargs["smtp_module"] = smtp_module
228-
return send_smtp_email(
229-
subject=alert_message.subject,
230-
body=alert_message.body,
231-
smtp_host=getattr(settings, "crisis_alert_smtp_host", None),
232-
smtp_port=getattr(settings, "crisis_alert_smtp_port", 587),
233-
sender=getattr(settings, "crisis_alert_email_from", None),
234-
recipients=getattr(settings, "crisis_alert_email_to", ()),
235-
username=getattr(settings, "crisis_alert_smtp_username", None),
236-
password=getattr(settings, "crisis_alert_smtp_password", None),
237-
use_starttls=getattr(settings, "crisis_alert_smtp_starttls", True),
238-
use_ssl=getattr(settings, "crisis_alert_smtp_ssl", False),
239-
printer=lambda text, **_kwargs: _call_log_message(log_message, text),
240-
**send_kwargs,
226+
*,
227+
env_reader: Callable[[str, str | None], str | None] = os.getenv,
228+
):
229+
explicit_gcs_uri = env_reader("STRATEGY_PLUGIN_ALERT_STATE_GCS_URI", None)
230+
report_gcs_uri = env_reader("EXECUTION_REPORT_GCS_URI", None)
231+
state_bucket = env_reader("FIRSTRADE_GCS_STATE_BUCKET", None)
232+
state_prefix = env_reader("FIRSTRADE_STATE_PREFIX", "firstrade-platform") or "firstrade-platform"
233+
state_gcs_uri = f"gs://{state_bucket}/{state_prefix}" if state_bucket else None
234+
return StrategyPluginEmailAlertMarkerStore(
235+
local_dir=env_reader("STRATEGY_PLUGIN_ALERT_STATE_DIR", None) or "/tmp/quant_strategy_plugin_alerts",
236+
gcs_prefix_uri=explicit_gcs_uri or report_gcs_uri or state_gcs_uri,
237+
gcp_project_id=settings.project_id,
241238
)
242239

243240

@@ -247,22 +244,17 @@ def publish_strategy_plugin_alerts(
247244
settings: PlatformRuntimeSettings,
248245
translator: Callable[..., str],
249246
log_message: Callable[..., Any] = print,
250-
) -> int:
251-
sent_count = 0
252-
for alert_message in build_strategy_plugin_alert_messages(
247+
env_reader: Callable[[str, str | None], str | None] = os.getenv,
248+
):
249+
return publish_strategy_plugin_email_alerts(
253250
signals,
251+
email_settings=settings,
254252
translator=translator,
255253
strategy_label=settings.strategy_profile,
256-
):
257-
if send_crisis_alert_email(
258-
alert_message,
259-
settings=settings,
260-
log_message=log_message,
261-
):
262-
sent_count += 1
263-
if sent_count:
264-
_call_log_message(log_message, f"strategy_plugin_alert_email_sent count={sent_count}")
265-
return sent_count
254+
context_label=build_strategy_plugin_alert_context_label(settings),
255+
alert_store=build_strategy_plugin_alert_store(settings, env_reader=env_reader),
256+
log_message=log_message,
257+
)
266258

267259

268260
def _runtime_metadata_with_execution_policy(
@@ -388,21 +380,26 @@ def run_strategy_cycle(
388380
}
389381
],
390382
"action_done": False,
383+
"strategy_plugin_alert_email_attempted_count": 0,
391384
"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": [],
392388
}
393389
return attach_strategy_plugin_result(
394390
result,
395391
signals=strategy_plugin_signals,
396392
error=strategy_plugin_error,
397393
translator=translator,
398394
)
399-
strategy_plugin_alert_email_sent_count = 0
395+
strategy_plugin_alert_email_result = None
400396
strategy_plugin_alert_email_error = None
401397
try:
402-
strategy_plugin_alert_email_sent_count = publish_strategy_plugin_alerts(
398+
strategy_plugin_alert_email_result = publish_strategy_plugin_alerts(
403399
strategy_plugin_signals,
404400
settings=settings,
405401
translator=translator,
402+
env_reader=env_reader,
406403
)
407404
except Exception as exc:
408405
strategy_plugin_alert_email_error = f"{type(exc).__name__}: {exc}"
@@ -482,7 +479,18 @@ def run_strategy_cycle(
482479
result["funding_blocked"] = True
483480
if strategy_run_persistence_error:
484481
result["strategy_run_persistence_error"] = strategy_run_persistence_error
485-
result["strategy_plugin_alert_email_sent_count"] = strategy_plugin_alert_email_sent_count
482+
if strategy_plugin_alert_email_result is not None:
483+
result.update(strategy_plugin_alert_email_result.to_report_fields())
484+
else:
485+
result.update(
486+
{
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": [],
492+
}
493+
)
486494
if strategy_plugin_alert_email_error:
487495
result["strategy_plugin_alert_email_error"] = strategy_plugin_alert_email_error
488496
attach_strategy_plugin_result(

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@1b6febbba7df81179ad7579f430c26a811c0e1a8
4+
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@ba67541711228f5a72a294def0e5cc24cc5479f3
55
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@1636271a3e0c17fc0c5da363f67eabe114eeff48
66
google-cloud-storage
77
requests

tests/test_rebalance_service.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,23 @@ def test_run_strategy_cycle_loads_strategy_plugin_report_and_sends_email(
239239
"application.rebalance_service.load_strategy_runtime",
240240
lambda *_args, **_kwargs: FakeStrategyRuntime(),
241241
)
242-
monkeypatch.setattr(
243-
"application.rebalance_service.send_crisis_alert_email",
244-
lambda alert_message, **_kwargs: observed_alerts.append(alert_message) or True,
245-
)
242+
243+
def fake_publish(signals, **kwargs):
244+
observed_alerts.append((tuple(signals), kwargs))
245+
return SimpleNamespace(
246+
sent_count=1,
247+
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": [
253+
{"subject": "Crisis plugin alert", "status": "sent"}
254+
],
255+
},
256+
)
257+
258+
monkeypatch.setattr("application.rebalance_service.publish_strategy_plugin_email_alerts", fake_publish)
246259

247260
result = run_strategy_cycle(
248261
runtime_settings=settings,
@@ -258,8 +271,9 @@ def test_run_strategy_cycle_loads_strategy_plugin_report_and_sends_email(
258271
"🧩 Plugin: Crisis Watch Notice | status: true crisis | notice: defend",
259272
)
260273
assert len(observed_alerts) == 1
261-
assert observed_alerts[0].subject == "🚨 Crisis plugin alert: Crisis Watch Notice | true crisis"
262-
assert "Would trade if enabled: true" in observed_alerts[0].body
274+
assert observed_alerts[0][0][0].canonical_route == "true_crisis"
275+
assert "firstrade" in observed_alerts[0][1]["context_label"]
276+
assert result["strategy_plugin_alert_email_deliveries"][0]["status"] == "sent"
263277
assert "🧩 Plugin: Crisis Watch Notice | status: true crisis | notice: defend" in messages[0]
264278

265279

0 commit comments

Comments
 (0)