Skip to content

Commit 3c8f52b

Browse files
authored
Rename crisis alert notification contract to email (#55)
1 parent 43b2ecb commit 3c8f52b

7 files changed

Lines changed: 117 additions & 120 deletions

File tree

docs/strategy_plugin_runtime_contract.md

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ The default registry currently defines:
6868

6969
| Plugin | Supported strategies | Supported mode | Escalated alert channel |
7070
| --- | --- | --- | --- |
71-
| `crisis_response_shadow` | `tqqq_growth_income`, `soxl_soxx_trend_income` | `shadow` | `google_voice` |
71+
| `crisis_response_shadow` | `tqqq_growth_income`, `soxl_soxx_trend_income` | `shadow` | `email` |
7272

7373
To expand a plugin later, update the shared definition or pass an explicit
7474
definition registry into the parser/loader. This keeps future plugin eligibility
@@ -124,33 +124,30 @@ when any of the following is true:
124124
- `suggested_action` is `defend` or `blocked`
125125
- `would_trade_if_enabled` is `true`
126126

127-
Platforms may still choose their delivery sinks, but Google Voice escalation via
128-
the Gmail-to-Google-Voice SMS gateway should use
129-
`quant_platform_kit.notifications.strategy_plugin_google_voice.publish_strategy_plugin_google_voice_alerts()`.
127+
Platforms may still choose their delivery sinks, but email escalation should use
128+
`quant_platform_kit.notifications.strategy_plugin_email.publish_strategy_plugin_email_alerts()`.
130129
The publisher builds the shared subject/body, prefixes platform context, returns
131130
structured sent/skipped/failed diagnostics, and can use
132-
`StrategyPluginGoogleVoiceAlertMarkerStore` to skip alert keys that were already
131+
`StrategyPluginEmailAlertMarkerStore` to skip alert keys that were already
133132
sent.
134133

135-
Platforms should expose this as Google Voice notification config, not as a
136-
generic email alert surface. The recipient value is still an email-form address:
137-
a normal mailbox receives an email, while a Google Voice mailbox/address can
138-
also surface the Google Voice prompt. The public configuration names should be
139-
channel specific:
134+
Platforms should expose this as crisis email notification config. The recipient
135+
value is an email address list: a normal mailbox receives an email, while a
136+
Google Voice-associated mailbox/address can also surface a Google Voice prompt
137+
through Google's own forwarding behavior. The public configuration names should
138+
be channel-neutral:
140139

141-
- `CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS`
142-
- `CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL`
143-
- `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD`
140+
- `CRISIS_ALERT_EMAIL_RECIPIENTS`
141+
- `CRISIS_ALERT_EMAIL_SENDER_EMAIL`
142+
- `CRISIS_ALERT_EMAIL_SENDER_PASSWORD`
144143

145144
By default the transport uses Gmail SMTP (`smtp.gmail.com`, port `465`, SSL),
146-
but the sender is not part of the Google Voice channel contract. Non-Gmail
147-
senders can override:
145+
but the sender provider is not part of the email channel contract. Non-Gmail
146+
senders can override the transport:
148147

149-
- `CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST`
150-
- `CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT`
151-
- `CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY` (`ssl`, `starttls`, or `none`)
148+
- `CRISIS_ALERT_EMAIL_SMTP_HOST`
149+
- `CRISIS_ALERT_EMAIL_SMTP_PORT`
150+
- `CRISIS_ALERT_EMAIL_SMTP_SECURITY` (`ssl`, `starttls`, or `none`)
152151

153-
Future direct email notifications should use a separate namespace such as
154-
`CRISIS_ALERT_EMAIL_*`.
155152
This keeps the Crisis Response plugin behavior consistent across IBKR, Schwab,
156153
LongBridge, Firstrade, and future platform runtimes.

src/quant_platform_kit/common/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
DEFAULT_STRATEGY_PLUGIN_DEFINITIONS,
4444
PLUGIN_CRISIS_RESPONSE_SHADOW,
4545
PLUGIN_MODE_SHADOW,
46-
STRATEGY_PLUGIN_ALERT_CHANNEL_GOOGLE_VOICE,
46+
STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL,
4747
STRATEGY_PLUGIN_ALERT_ACTIONS,
4848
STRATEGY_PLUGIN_NON_ALERT_ROUTES,
4949
SUPPORTED_STRATEGY_PLUGIN_MODES,
@@ -84,7 +84,7 @@
8484
"STAGE_PARTIAL_SUBMITTED",
8585
"STAGE_RECONCILED",
8686
"STAGE_SUBMITTED",
87-
"STRATEGY_PLUGIN_ALERT_CHANNEL_GOOGLE_VOICE",
87+
"STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL",
8888
"STRATEGY_PLUGIN_ALERT_ACTIONS",
8989
"STRATEGY_PLUGIN_NON_ALERT_ROUTES",
9090
"SUPPORTED_STRATEGY_PLUGIN_MODES",

src/quant_platform_kit/common/strategy_plugins.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
PLUGIN_CRISIS_RESPONSE_SHADOW = "crisis_response_shadow"
1414
PLUGIN_MODE_SHADOW = "shadow"
15-
STRATEGY_PLUGIN_ALERT_CHANNEL_GOOGLE_VOICE = "google_voice"
15+
STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL = "email"
1616
SUPPORTED_STRATEGY_PLUGIN_MODES = frozenset({PLUGIN_MODE_SHADOW})
1717
DEFAULT_PLUGIN_ARTIFACT_CACHE_DIR = Path(tempfile.gettempdir()) / "quant_strategy_plugin_artifacts"
1818
STRATEGY_PLUGIN_NON_ALERT_ROUTES = frozenset({"no_action"})
@@ -67,7 +67,7 @@ def supports_strategy(self, strategy: str) -> bool:
6767
plugin=PLUGIN_CRISIS_RESPONSE_SHADOW,
6868
supported_strategies=CRISIS_RESPONSE_SHADOW_SUPPORTED_STRATEGIES,
6969
supported_modes=SUPPORTED_STRATEGY_PLUGIN_MODES,
70-
alert_channels=(STRATEGY_PLUGIN_ALERT_CHANNEL_GOOGLE_VOICE,),
70+
alert_channels=(STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL,),
7171
)
7272
}
7373

src/quant_platform_kit/notifications/__init__.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@
22

33
from .email import parse_email_recipients, send_smtp_email
44
from .events import NotificationPublisher, RenderedNotification, publish_rendered_notification
5-
from .strategy_plugin_google_voice import (
6-
StrategyPluginGoogleVoiceAlertDelivery,
7-
StrategyPluginGoogleVoiceAlertMarkerStore,
8-
StrategyPluginGoogleVoiceAlertPublishResult,
9-
StrategyPluginGoogleVoiceSettings,
5+
from .strategy_plugin_email import (
6+
StrategyPluginEmailAlertDelivery,
7+
StrategyPluginEmailAlertMarkerStore,
8+
StrategyPluginEmailAlertPublishResult,
9+
StrategyPluginEmailSettings,
1010
build_strategy_plugin_alert_context_label,
11-
publish_strategy_plugin_google_voice_alerts,
11+
publish_strategy_plugin_email_alerts,
1212
)
1313

1414
__all__ = [
1515
"NotificationPublisher",
1616
"RenderedNotification",
17-
"StrategyPluginGoogleVoiceAlertDelivery",
18-
"StrategyPluginGoogleVoiceAlertMarkerStore",
19-
"StrategyPluginGoogleVoiceAlertPublishResult",
20-
"StrategyPluginGoogleVoiceSettings",
17+
"StrategyPluginEmailAlertDelivery",
18+
"StrategyPluginEmailAlertMarkerStore",
19+
"StrategyPluginEmailAlertPublishResult",
20+
"StrategyPluginEmailSettings",
2121
"build_strategy_plugin_alert_context_label",
2222
"parse_email_recipients",
2323
"publish_rendered_notification",
24-
"publish_strategy_plugin_google_voice_alerts",
24+
"publish_strategy_plugin_email_alerts",
2525
"send_smtp_email",
2626
]

src/quant_platform_kit/notifications/strategy_plugin_google_voice.py renamed to src/quant_platform_kit/notifications/strategy_plugin_email.py

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Google Voice notification helpers for strategy plugin alerts."""
1+
"""Email notification helpers for strategy plugin alerts."""
22

33
from __future__ import annotations
44

@@ -18,9 +18,9 @@
1818
from .email import parse_email_recipients, send_smtp_email
1919

2020

21-
_DEFAULT_GOOGLE_VOICE_SMTP_HOST = "smtp.gmail.com"
22-
_DEFAULT_GOOGLE_VOICE_SMTP_PORT = 465
23-
_DEFAULT_GOOGLE_VOICE_SMTP_SECURITY = "ssl"
21+
_DEFAULT_EMAIL_SMTP_HOST = "smtp.gmail.com"
22+
_DEFAULT_EMAIL_SMTP_PORT = 465
23+
_DEFAULT_EMAIL_SMTP_SECURITY = "ssl"
2424
_SMTP_SECURITY_NONE = "none"
2525
_SMTP_SECURITY_SSL = "ssl"
2626
_SMTP_SECURITY_STARTTLS = "starttls"
@@ -32,46 +32,46 @@
3232

3333

3434
@dataclass(frozen=True)
35-
class StrategyPluginGoogleVoiceSettings:
35+
class StrategyPluginEmailSettings:
3636
recipients: tuple[str, ...] = ()
3737
sender_email: str | None = None
3838
sender_password: str | None = field(default=None, repr=False)
39-
smtp_host: str = _DEFAULT_GOOGLE_VOICE_SMTP_HOST
40-
smtp_port: int = _DEFAULT_GOOGLE_VOICE_SMTP_PORT
41-
smtp_security: str = _DEFAULT_GOOGLE_VOICE_SMTP_SECURITY
39+
smtp_host: str = _DEFAULT_EMAIL_SMTP_HOST
40+
smtp_port: int = _DEFAULT_EMAIL_SMTP_PORT
41+
smtp_security: str = _DEFAULT_EMAIL_SMTP_SECURITY
4242
timeout: float = 10.0
4343

4444
@classmethod
45-
def from_object(cls, value: object) -> "StrategyPluginGoogleVoiceSettings":
45+
def from_object(cls, value: object) -> "StrategyPluginEmailSettings":
4646
if isinstance(value, cls):
4747
return value
4848
return cls(
4949
recipients=tuple(
50-
parse_email_recipients(_get_value(value, "crisis_alert_google_voice_recipients", ()))
50+
parse_email_recipients(_get_value(value, "crisis_alert_email_recipients", ()))
5151
),
52-
sender_email=_first_non_empty(_get_value(value, "crisis_alert_google_voice_sender_email")),
53-
sender_password=_get_value(value, "crisis_alert_google_voice_sender_password"),
52+
sender_email=_first_non_empty(_get_value(value, "crisis_alert_email_sender_email")),
53+
sender_password=_get_value(value, "crisis_alert_email_sender_password"),
5454
smtp_host=_first_non_empty(
55-
_get_value(value, "crisis_alert_google_voice_smtp_host")
55+
_get_value(value, "crisis_alert_email_smtp_host")
5656
)
57-
or _DEFAULT_GOOGLE_VOICE_SMTP_HOST,
57+
or _DEFAULT_EMAIL_SMTP_HOST,
5858
smtp_port=_coerce_int(
59-
_get_value(value, "crisis_alert_google_voice_smtp_port"),
60-
_DEFAULT_GOOGLE_VOICE_SMTP_PORT,
59+
_get_value(value, "crisis_alert_email_smtp_port"),
60+
_DEFAULT_EMAIL_SMTP_PORT,
6161
),
6262
smtp_security=_coerce_smtp_security(
63-
_get_value(value, "crisis_alert_google_voice_smtp_security")
63+
_get_value(value, "crisis_alert_email_smtp_security")
6464
),
6565
)
6666

6767
def missing_fields(self) -> tuple[str, ...]:
6868
missing: list[str] = []
6969
if not parse_email_recipients(self.recipients):
70-
missing.append("CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS")
70+
missing.append("CRISIS_ALERT_EMAIL_RECIPIENTS")
7171
if not str(self.sender_email or "").strip():
72-
missing.append("CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL")
72+
missing.append("CRISIS_ALERT_EMAIL_SENDER_EMAIL")
7373
if not str(self.sender_password or "").strip():
74-
missing.append("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD")
74+
missing.append("CRISIS_ALERT_EMAIL_SENDER_PASSWORD")
7575
return tuple(missing)
7676

7777
@property
@@ -80,7 +80,7 @@ def is_configured(self) -> bool:
8080

8181

8282
@dataclass(frozen=True)
83-
class StrategyPluginGoogleVoiceAlertDelivery:
83+
class StrategyPluginEmailAlertDelivery:
8484
alert_key: str
8585
subject: str
8686
status: str
@@ -101,8 +101,8 @@ def to_dict(self) -> dict[str, Any]:
101101

102102

103103
@dataclass(frozen=True)
104-
class StrategyPluginGoogleVoiceAlertPublishResult:
105-
deliveries: tuple[StrategyPluginGoogleVoiceAlertDelivery, ...] = ()
104+
class StrategyPluginEmailAlertPublishResult:
105+
deliveries: tuple[StrategyPluginEmailAlertDelivery, ...] = ()
106106

107107
@property
108108
def attempted_count(self) -> int:
@@ -120,7 +120,7 @@ def skipped_count(self) -> int:
120120
def failed_count(self) -> int:
121121
return sum(1 for delivery in self.deliveries if delivery.status == "failed")
122122

123-
def to_report_fields(self, *, prefix: str = "strategy_plugin_alert_google_voice") -> dict[str, Any]:
123+
def to_report_fields(self, *, prefix: str = "strategy_plugin_alert_email") -> dict[str, Any]:
124124
return {
125125
f"{prefix}_attempted_count": self.attempted_count,
126126
f"{prefix}_sent_count": self.sent_count,
@@ -131,11 +131,11 @@ def to_report_fields(self, *, prefix: str = "strategy_plugin_alert_google_voice"
131131

132132

133133
@dataclass(frozen=True)
134-
class StrategyPluginGoogleVoiceAlertMarkerStore:
134+
class StrategyPluginEmailAlertMarkerStore:
135135
local_dir: str | Path | None = None
136136
gcs_prefix_uri: str | None = None
137137
gcp_project_id: str | None = None
138-
namespace: str = "strategy_plugin_google_voice_alerts"
138+
namespace: str = "strategy_plugin_email_alerts"
139139
client_factory: Any = None
140140

141141
def has_alert(self, alert_key: str) -> bool:
@@ -152,7 +152,7 @@ def record_alert(
152152
metadata: Mapping[str, Any] | None = None,
153153
) -> None:
154154
payload = {
155-
"schema_version": "strategy_plugin_google_voice_alert_marker.v1",
155+
"schema_version": "strategy_plugin_email_alert_marker.v1",
156156
"alert_key": str(alert_key),
157157
"recorded_at": datetime.now(timezone.utc).isoformat(),
158158
"metadata": dict(metadata or {}),
@@ -215,38 +215,38 @@ def build_strategy_plugin_alert_context_label(
215215
return " / ".join(str(part).strip() for part in parts if str(part or "").strip())
216216

217217

218-
def publish_strategy_plugin_google_voice_alerts(
218+
def publish_strategy_plugin_email_alerts(
219219
signals: Sequence[object],
220220
*,
221-
google_voice_settings: StrategyPluginGoogleVoiceSettings | object,
221+
email_settings: StrategyPluginEmailSettings | object,
222222
translator: Callable[..., str] | None = None,
223223
strategy_label: str | None = None,
224224
context_label: str | None = None,
225-
alert_store: StrategyPluginGoogleVoiceAlertMarkerStore | object | None = None,
225+
alert_store: StrategyPluginEmailAlertMarkerStore | object | None = None,
226226
send_notification: Callable[..., bool] = send_smtp_email,
227227
log_message: Callable[..., Any] = print,
228-
) -> StrategyPluginGoogleVoiceAlertPublishResult:
229-
settings = StrategyPluginGoogleVoiceSettings.from_object(google_voice_settings)
228+
) -> StrategyPluginEmailAlertPublishResult:
229+
settings = StrategyPluginEmailSettings.from_object(email_settings)
230230
messages = build_strategy_plugin_alert_messages(
231231
signals,
232232
translator=translator,
233233
strategy_label=strategy_label,
234234
context_label=context_label,
235-
alert_namespace="strategy_plugin_google_voice_alert",
235+
alert_namespace="strategy_plugin_email_alert",
236236
)
237-
deliveries: list[StrategyPluginGoogleVoiceAlertDelivery] = []
237+
deliveries: list[StrategyPluginEmailAlertDelivery] = []
238238
missing_fields = settings.missing_fields()
239239
if missing_fields:
240240
for message in messages:
241241
deliveries.append(
242242
_delivery(
243243
message,
244244
status="skipped",
245-
reason="missing_google_voice_config",
245+
reason="missing_email_config",
246246
error=",".join(missing_fields),
247247
)
248248
)
249-
result = StrategyPluginGoogleVoiceAlertPublishResult(tuple(deliveries))
249+
result = StrategyPluginEmailAlertPublishResult(tuple(deliveries))
250250
_log_publish_result(result, log_message=log_message)
251251
return result
252252

@@ -268,7 +268,7 @@ def publish_strategy_plugin_google_voice_alerts(
268268
record_error = _store_record_error(alert_store, alert_key, message)
269269
combined_error = "; ".join(error for error in (store_error, record_error) if error)
270270
deliveries.append(_delivery(message, status="sent", error=combined_error or None))
271-
result = StrategyPluginGoogleVoiceAlertPublishResult(tuple(deliveries))
271+
result = StrategyPluginEmailAlertPublishResult(tuple(deliveries))
272272
_log_publish_result(result, log_message=log_message)
273273
return result
274274

@@ -279,8 +279,8 @@ def _delivery(
279279
status: str,
280280
reason: str | None = None,
281281
error: str | None = None,
282-
) -> StrategyPluginGoogleVoiceAlertDelivery:
283-
return StrategyPluginGoogleVoiceAlertDelivery(
282+
) -> StrategyPluginEmailAlertDelivery:
283+
return StrategyPluginEmailAlertDelivery(
284284
alert_key=message.alert_key or _fallback_alert_key(message),
285285
subject=message.subject,
286286
status=status,
@@ -293,7 +293,7 @@ def _delivery(
293293
def _send_message(
294294
send_notification: Callable[..., bool],
295295
message: StrategyPluginAlertMessage,
296-
settings: StrategyPluginGoogleVoiceSettings,
296+
settings: StrategyPluginEmailSettings,
297297
) -> tuple[bool, str | None]:
298298
try:
299299
sent = send_notification(
@@ -347,7 +347,7 @@ def _store_record_error(
347347

348348

349349
def _log_publish_result(
350-
result: StrategyPluginGoogleVoiceAlertPublishResult,
350+
result: StrategyPluginEmailAlertPublishResult,
351351
*,
352352
log_message: Callable[..., Any],
353353
) -> None:
@@ -356,7 +356,7 @@ def _log_publish_result(
356356
_call_log_message(
357357
log_message,
358358
(
359-
"strategy_plugin_alert_google_voice_result "
359+
"strategy_plugin_alert_email_result "
360360
f"attempted={result.attempted_count} "
361361
f"sent={result.sent_count} "
362362
f"skipped={result.skipped_count} "
@@ -400,11 +400,11 @@ def _coerce_smtp_security(value: Any) -> str:
400400
security = str(value or "").strip().lower()
401401
if security in _SMTP_SECURITY_VALUES:
402402
return security
403-
return _DEFAULT_GOOGLE_VOICE_SMTP_SECURITY
403+
return _DEFAULT_EMAIL_SMTP_SECURITY
404404

405405

406406
def _fallback_alert_key(message: StrategyPluginAlertMessage) -> str:
407-
return "strategy_plugin_google_voice_alert/" + _clean_relative_key(message.subject or "unknown")
407+
return "strategy_plugin_email_alert/" + _clean_relative_key(message.subject or "unknown")
408408

409409

410410
def _clean_relative_key(value: str) -> str:

0 commit comments

Comments
 (0)