Skip to content

Commit 4443fec

Browse files
authored
Use shared strategy plugin alert dispatcher (#34)
1 parent a53733b commit 4443fec

4 files changed

Lines changed: 58 additions & 114 deletions

File tree

application/rebalance_service.py

Lines changed: 41 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +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,
58-
)
59-
from quant_platform_kit.notifications.strategy_plugin_sms import (
60-
StrategyPluginSmsAlertMarkerStore,
61-
publish_strategy_plugin_sms_alerts,
54+
from quant_platform_kit.notifications.strategy_plugin_alerts import (
55+
StrategyPluginAlertStateSettings,
56+
build_strategy_plugin_alert_context_label as build_alert_context_label,
57+
publish_strategy_plugin_alerts as dispatch_strategy_plugin_alerts,
6258
)
6359
from quant_platform_kit.strategy_contracts import build_strategy_evaluation_inputs
6460
from runtime_config_support import PlatformRuntimeSettings, load_platform_runtime_settings
@@ -216,7 +212,7 @@ def attach_strategy_plugin_result(
216212

217213

218214
def build_strategy_plugin_alert_context_label(settings: PlatformRuntimeSettings) -> str:
219-
return build_email_alert_context_label(
215+
return build_alert_context_label(
220216
platform_id="firstrade",
221217
strategy_profile=settings.strategy_profile,
222218
account_scope=settings.account_region or settings.account_prefix,
@@ -225,52 +221,21 @@ def build_strategy_plugin_alert_context_label(settings: PlatformRuntimeSettings)
225221
)
226222

227223

228-
def build_strategy_plugin_alert_store(
224+
def build_strategy_plugin_alert_state_settings(
229225
settings: PlatformRuntimeSettings,
230226
*,
231227
env_reader: Callable[[str, str | None], str | None] = os.getenv,
232228
):
233-
explicit_gcs_uri = env_reader("STRATEGY_PLUGIN_ALERT_STATE_GCS_URI", None)
234-
report_gcs_uri = env_reader("EXECUTION_REPORT_GCS_URI", None)
235229
state_bucket = env_reader("FIRSTRADE_GCS_STATE_BUCKET", None)
236230
state_prefix = env_reader("FIRSTRADE_STATE_PREFIX", "firstrade-platform") or "firstrade-platform"
237231
state_gcs_uri = f"gs://{state_bucket}/{state_prefix}" if state_bucket else None
238-
return StrategyPluginEmailAlertMarkerStore(
239-
local_dir=env_reader("STRATEGY_PLUGIN_ALERT_STATE_DIR", None) or "/tmp/quant_strategy_plugin_alerts",
240-
gcs_prefix_uri=explicit_gcs_uri or report_gcs_uri or state_gcs_uri,
232+
return StrategyPluginAlertStateSettings.from_env(
233+
env_reader=env_reader,
241234
gcp_project_id=settings.project_id,
235+
fallback_gcs_prefix_uri=state_gcs_uri,
242236
)
243237

244238

245-
def build_strategy_plugin_sms_alert_store(
246-
settings: PlatformRuntimeSettings,
247-
*,
248-
env_reader: Callable[[str, str | None], str | None] = os.getenv,
249-
):
250-
explicit_gcs_uri = env_reader("STRATEGY_PLUGIN_ALERT_STATE_GCS_URI", None)
251-
report_gcs_uri = env_reader("EXECUTION_REPORT_GCS_URI", None)
252-
state_bucket = env_reader("FIRSTRADE_GCS_STATE_BUCKET", None)
253-
state_prefix = env_reader("FIRSTRADE_STATE_PREFIX", "firstrade-platform") or "firstrade-platform"
254-
state_gcs_uri = f"gs://{state_bucket}/{state_prefix}" if state_bucket else None
255-
return StrategyPluginSmsAlertMarkerStore(
256-
local_dir=env_reader("STRATEGY_PLUGIN_ALERT_STATE_DIR", None) or "/tmp/quant_strategy_plugin_alerts",
257-
gcs_prefix_uri=explicit_gcs_uri or report_gcs_uri or state_gcs_uri,
258-
gcp_project_id=settings.project_id,
259-
)
260-
261-
262-
class StrategyPluginAlertPublishResults:
263-
def __init__(self, *, email_result, sms_result):
264-
self.email_result = email_result
265-
self.sms_result = sms_result
266-
267-
def to_report_fields(self) -> dict[str, Any]:
268-
fields: dict[str, Any] = {}
269-
fields.update(self.email_result.to_report_fields())
270-
fields.update(self.sms_result.to_report_fields())
271-
return fields
272-
273-
274239
def publish_strategy_plugin_alerts(
275240
signals,
276241
*,
@@ -279,25 +244,34 @@ def publish_strategy_plugin_alerts(
279244
log_message: Callable[..., Any] = print,
280245
env_reader: Callable[[str, str | None], str | None] = os.getenv,
281246
):
282-
email_result = publish_strategy_plugin_email_alerts(
247+
return dispatch_strategy_plugin_alerts(
283248
signals,
284-
email_settings=settings,
249+
notification_settings=settings,
285250
translator=translator,
286251
strategy_label=settings.strategy_profile,
287252
context_label=build_strategy_plugin_alert_context_label(settings),
288-
alert_store=build_strategy_plugin_alert_store(settings, env_reader=env_reader),
253+
state_settings=build_strategy_plugin_alert_state_settings(settings, env_reader=env_reader),
289254
log_message=log_message,
290255
)
291-
sms_result = publish_strategy_plugin_sms_alerts(
292-
signals,
293-
sms_settings=settings,
294-
translator=translator,
295-
strategy_label=settings.strategy_profile,
296-
context_label=build_strategy_plugin_alert_context_label(settings),
297-
alert_store=build_strategy_plugin_sms_alert_store(settings, env_reader=env_reader),
298-
log_message=log_message,
299-
)
300-
return StrategyPluginAlertPublishResults(email_result=email_result, sms_result=sms_result)
256+
257+
258+
def empty_strategy_plugin_alert_report_fields() -> dict[str, Any]:
259+
return {
260+
"strategy_plugin_alert_attempted_count": 0,
261+
"strategy_plugin_alert_sent_count": 0,
262+
"strategy_plugin_alert_skipped_count": 0,
263+
"strategy_plugin_alert_failed_count": 0,
264+
"strategy_plugin_alert_email_attempted_count": 0,
265+
"strategy_plugin_alert_email_sent_count": 0,
266+
"strategy_plugin_alert_email_skipped_count": 0,
267+
"strategy_plugin_alert_email_failed_count": 0,
268+
"strategy_plugin_alert_email_deliveries": [],
269+
"strategy_plugin_alert_sms_attempted_count": 0,
270+
"strategy_plugin_alert_sms_sent_count": 0,
271+
"strategy_plugin_alert_sms_skipped_count": 0,
272+
"strategy_plugin_alert_sms_failed_count": 0,
273+
"strategy_plugin_alert_sms_deliveries": [],
274+
}
301275

302276

303277
def _runtime_metadata_with_execution_policy(
@@ -423,34 +397,25 @@ def run_strategy_cycle(
423397
}
424398
],
425399
"action_done": False,
426-
"strategy_plugin_alert_email_attempted_count": 0,
427-
"strategy_plugin_alert_email_sent_count": 0,
428-
"strategy_plugin_alert_email_skipped_count": 0,
429-
"strategy_plugin_alert_email_failed_count": 0,
430-
"strategy_plugin_alert_email_deliveries": [],
431-
"strategy_plugin_alert_sms_attempted_count": 0,
432-
"strategy_plugin_alert_sms_sent_count": 0,
433-
"strategy_plugin_alert_sms_skipped_count": 0,
434-
"strategy_plugin_alert_sms_failed_count": 0,
435-
"strategy_plugin_alert_sms_deliveries": [],
400+
**empty_strategy_plugin_alert_report_fields(),
436401
}
437402
return attach_strategy_plugin_result(
438403
result,
439404
signals=strategy_plugin_signals,
440405
error=strategy_plugin_error,
441406
translator=translator,
442407
)
443-
strategy_plugin_alert_email_result = None
444-
strategy_plugin_alert_email_error = None
408+
strategy_plugin_alert_result = None
409+
strategy_plugin_alert_error = None
445410
try:
446-
strategy_plugin_alert_email_result = publish_strategy_plugin_alerts(
411+
strategy_plugin_alert_result = publish_strategy_plugin_alerts(
447412
strategy_plugin_signals,
448413
settings=settings,
449414
translator=translator,
450415
env_reader=env_reader,
451416
)
452417
except Exception as exc:
453-
strategy_plugin_alert_email_error = f"{type(exc).__name__}: {exc}"
418+
strategy_plugin_alert_error = f"{type(exc).__name__}: {exc}"
454419
strategy_run_persisted = False
455420
strategy_run_persistence_error = None
456421
if persist_strategy_runs:
@@ -527,25 +492,12 @@ def run_strategy_cycle(
527492
result["funding_blocked"] = True
528493
if strategy_run_persistence_error:
529494
result["strategy_run_persistence_error"] = strategy_run_persistence_error
530-
if strategy_plugin_alert_email_result is not None:
531-
result.update(strategy_plugin_alert_email_result.to_report_fields())
495+
if strategy_plugin_alert_result is not None:
496+
result.update(strategy_plugin_alert_result.to_report_fields())
532497
else:
533-
result.update(
534-
{
535-
"strategy_plugin_alert_email_attempted_count": 0,
536-
"strategy_plugin_alert_email_sent_count": 0,
537-
"strategy_plugin_alert_email_skipped_count": 0,
538-
"strategy_plugin_alert_email_failed_count": 0,
539-
"strategy_plugin_alert_email_deliveries": [],
540-
"strategy_plugin_alert_sms_attempted_count": 0,
541-
"strategy_plugin_alert_sms_sent_count": 0,
542-
"strategy_plugin_alert_sms_skipped_count": 0,
543-
"strategy_plugin_alert_sms_failed_count": 0,
544-
"strategy_plugin_alert_sms_deliveries": [],
545-
}
546-
)
547-
if strategy_plugin_alert_email_error:
548-
result["strategy_plugin_alert_email_error"] = strategy_plugin_alert_email_error
498+
result.update(empty_strategy_plugin_alert_report_fields())
499+
if strategy_plugin_alert_error:
500+
result["strategy_plugin_alert_error"] = strategy_plugin_alert_error
549501
attach_strategy_plugin_result(
550502
result,
551503
signals=strategy_plugin_signals,

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ authors = [
1414
]
1515
dependencies = [
1616
"firstrade==0.0.38",
17-
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@d43800180aae1c7fe7051496a6af5d76f2c65879",
18-
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@9661d8bb74e33466fa0ec1efef168b1d1bae8875",
17+
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@532346b6e22d9f9c0a25c5e859f29084d7b27f6c",
18+
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@7ccc46038e6f0fe84a6910a6fa0515252bf15b79",
1919
"google-cloud-storage",
2020
"requests",
2121
]

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
flask
22
gunicorn
33
firstrade==0.0.38
4-
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@d43800180aae1c7fe7051496a6af5d76f2c65879
5-
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@9661d8bb74e33466fa0ec1efef168b1d1bae8875
4+
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@532346b6e22d9f9c0a25c5e859f29084d7b27f6c
5+
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@7ccc46038e6f0fe84a6910a6fa0515252bf15b79
66
google-cloud-storage
77
requests
88
pytest

tests/test_rebalance_service.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -233,34 +233,28 @@ def test_run_strategy_cycle_loads_strategy_plugin_report_and_sends_email(
233233
crisis_alert_email_sender_password="app-password",
234234
)
235235
messages = []
236-
observed_email_alerts = []
237-
observed_sms_alerts = []
236+
observed_alerts = []
238237

239238
monkeypatch.setattr(
240239
"application.rebalance_service.load_strategy_runtime",
241240
lambda *_args, **_kwargs: FakeStrategyRuntime(),
242241
)
243242

244-
def fake_email_publish(signals, **kwargs):
245-
observed_email_alerts.append((tuple(signals), kwargs))
243+
def fake_dispatch(signals, **kwargs):
244+
observed_alerts.append((tuple(signals), kwargs))
246245
return SimpleNamespace(
247-
sent_count=1,
248246
to_report_fields=lambda: {
247+
"strategy_plugin_alert_attempted_count": 2,
248+
"strategy_plugin_alert_sent_count": 2,
249+
"strategy_plugin_alert_skipped_count": 0,
250+
"strategy_plugin_alert_failed_count": 0,
249251
"strategy_plugin_alert_email_attempted_count": 1,
250252
"strategy_plugin_alert_email_sent_count": 1,
251253
"strategy_plugin_alert_email_skipped_count": 0,
252254
"strategy_plugin_alert_email_failed_count": 0,
253255
"strategy_plugin_alert_email_deliveries": [
254256
{"subject": "Crisis plugin alert", "status": "sent"}
255257
],
256-
},
257-
)
258-
259-
def fake_sms_publish(signals, **kwargs):
260-
observed_sms_alerts.append((tuple(signals), kwargs))
261-
return SimpleNamespace(
262-
sent_count=1,
263-
to_report_fields=lambda: {
264258
"strategy_plugin_alert_sms_attempted_count": 1,
265259
"strategy_plugin_alert_sms_sent_count": 1,
266260
"strategy_plugin_alert_sms_skipped_count": 0,
@@ -271,8 +265,7 @@ def fake_sms_publish(signals, **kwargs):
271265
},
272266
)
273267

274-
monkeypatch.setattr("application.rebalance_service.publish_strategy_plugin_email_alerts", fake_email_publish)
275-
monkeypatch.setattr("application.rebalance_service.publish_strategy_plugin_sms_alerts", fake_sms_publish)
268+
monkeypatch.setattr("application.rebalance_service.dispatch_strategy_plugin_alerts", fake_dispatch)
276269

277270
result = run_strategy_cycle(
278271
runtime_settings=settings,
@@ -288,12 +281,11 @@ def fake_sms_publish(signals, **kwargs):
288281
assert result["strategy_plugin_lines"] == (
289282
"🧩 Plugin: Crisis Watch Notice | status: true crisis | notice: defend",
290283
)
291-
assert len(observed_email_alerts) == 1
292-
assert len(observed_sms_alerts) == 1
293-
assert observed_email_alerts[0][0][0].canonical_route == "true_crisis"
294-
assert observed_sms_alerts[0][0][0].canonical_route == "true_crisis"
295-
assert "firstrade" in observed_email_alerts[0][1]["context_label"]
296-
assert "firstrade" in observed_sms_alerts[0][1]["context_label"]
284+
assert len(observed_alerts) == 1
285+
assert observed_alerts[0][0][0].canonical_route == "true_crisis"
286+
assert "firstrade" in observed_alerts[0][1]["context_label"]
287+
assert observed_alerts[0][1]["notification_settings"] is settings
288+
assert observed_alerts[0][1]["state_settings"] is not None
297289
assert result["strategy_plugin_alert_email_deliveries"][0]["status"] == "sent"
298290
assert result["strategy_plugin_alert_sms_deliveries"][0]["status"] == "sent"
299291
assert "🧩 Plugin: Crisis Watch Notice | status: true crisis | notice: defend" in messages[0]

0 commit comments

Comments
 (0)