Skip to content

Commit b520bca

Browse files
authored
Add strategy plugin Telegram alert channel (#62)
1 parent 2fdb7c3 commit b520bca

12 files changed

Lines changed: 831 additions & 31 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ It contains:
1414
- narrow ports for market data, portfolio snapshots, order execution, notifications, and state
1515
- reusable broker adapter utilities
1616
- strategy loading, strategy-plugin, and alert-message contracts
17-
- optional strategy-plugin alert channels for email, SMS, and push providers
17+
- optional strategy-plugin alert channels for email, SMS, push, and Telegram providers
1818
- synthetic-data tests for public behavior
1919

2020
It does not contain private runtime wiring or generated strategy outputs.
@@ -49,7 +49,7 @@ Strategy plugins are sidecar artifacts that platform repositories may read when
4949

5050
Generated plugin artifacts and platform-specific notification routing stay with the producing pipeline or consuming platform repository. Tests in this repository use synthetic price history and synthetic payloads only.
5151

52-
Plugin alert delivery is provider-neutral at the platform boundary. Platform repositories pass runtime settings into `publish_strategy_plugin_alerts`; this repository handles configured `email`, `sms`, and `push` channels without coupling plugin logic to a broker platform.
52+
Plugin alert delivery is provider-neutral at the platform boundary. Platform repositories pass runtime settings into `publish_strategy_plugin_alerts`; this repository handles configured `email`, `sms`, `push`, and `telegram` channels without coupling plugin logic to a broker platform.
5353

5454
## Package Layout
5555

README.zh-CN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
- 市场数据、持仓快照、订单执行、通知、状态存储等窄接口
1515
- 可复用的券商适配工具
1616
- 策略加载、策略插件、告警消息契约
17-
- 可选的策略插件 email、SMS 和 push 告警通道
17+
- 可选的策略插件 email、SMS、pushTelegram 告警通道
1818
- 使用合成数据的公开测试
1919

2020
它不包含私有运行时接线和生成的策略输出。
@@ -49,7 +49,7 @@ QuantPlatformKit
4949

5050
生成的插件 artifact 和平台专属通知路由由生成它的 pipeline 或消费它的平台仓库管理。这个仓库的测试只使用合成价格历史和合成 payload。
5151

52-
插件告警发送在平台边界保持 provider-neutral。平台仓库只把 runtime settings 传入 `publish_strategy_plugin_alerts`;这个仓库负责按配置发送 `email``sms``push`,不让插件逻辑耦合某个券商平台。
52+
插件告警发送在平台边界保持 provider-neutral。平台仓库只把 runtime settings 传入 `publish_strategy_plugin_alerts`;这个仓库负责按配置发送 `email``sms``push``telegram`,不让插件逻辑耦合某个券商平台。
5353

5454
## 目录结构
5555

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "quant-platform-kit"
7-
version = "0.7.29"
7+
version = "0.7.30"
88
description = "Shared broker adapters, domain models, execution ports, and notification utilities for QuantStrategyLab strategies."
99
readme = "README.md"
1010
requires-python = ">=3.9"

src/quant_platform_kit/common/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL,
4747
STRATEGY_PLUGIN_ALERT_CHANNEL_PUSH,
4848
STRATEGY_PLUGIN_ALERT_CHANNEL_SMS,
49+
STRATEGY_PLUGIN_ALERT_CHANNEL_TELEGRAM,
4950
STRATEGY_PLUGIN_ALERT_ACTIONS,
5051
STRATEGY_PLUGIN_NON_ALERT_ROUTES,
5152
SUPPORTED_STRATEGY_PLUGIN_MODES,
@@ -89,6 +90,7 @@
8990
"STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL",
9091
"STRATEGY_PLUGIN_ALERT_CHANNEL_PUSH",
9192
"STRATEGY_PLUGIN_ALERT_CHANNEL_SMS",
93+
"STRATEGY_PLUGIN_ALERT_CHANNEL_TELEGRAM",
9294
"STRATEGY_PLUGIN_ALERT_ACTIONS",
9395
"STRATEGY_PLUGIN_NON_ALERT_ROUTES",
9496
"SUPPORTED_STRATEGY_PLUGIN_MODES",

src/quant_platform_kit/common/strategy_plugins.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL = "email"
1616
STRATEGY_PLUGIN_ALERT_CHANNEL_SMS = "sms"
1717
STRATEGY_PLUGIN_ALERT_CHANNEL_PUSH = "push"
18+
STRATEGY_PLUGIN_ALERT_CHANNEL_TELEGRAM = "telegram"
1819
SUPPORTED_STRATEGY_PLUGIN_MODES = frozenset({PLUGIN_MODE_SHADOW})
1920
DEFAULT_PLUGIN_ARTIFACT_CACHE_DIR = Path(tempfile.gettempdir()) / "quant_strategy_plugin_artifacts"
2021
STRATEGY_PLUGIN_NON_ALERT_ROUTES = frozenset({"no_action"})
@@ -73,6 +74,7 @@ def supports_strategy(self, strategy: str) -> bool:
7374
STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL,
7475
STRATEGY_PLUGIN_ALERT_CHANNEL_SMS,
7576
STRATEGY_PLUGIN_ALERT_CHANNEL_PUSH,
77+
STRATEGY_PLUGIN_ALERT_CHANNEL_TELEGRAM,
7678
),
7779
)
7880
}

src/quant_platform_kit/notifications/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .events import NotificationPublisher, RenderedNotification, publish_rendered_notification
55
from .push import parse_push_recipients, send_ntfy_push, send_pushover_push, send_strategy_plugin_push
66
from .sms import normalize_sms_recipient, parse_sms_recipients, send_twilio_sms
7+
from .telegram import parse_telegram_chat_ids, send_strategy_plugin_telegram, send_telegram_message
78
from .strategy_plugin_alerts import (
89
StrategyPluginAlertChannelStores,
910
StrategyPluginAlertPublishResult,
@@ -32,6 +33,13 @@
3233
StrategyPluginPushSettings,
3334
publish_strategy_plugin_push_alerts,
3435
)
36+
from .strategy_plugin_telegram import (
37+
StrategyPluginTelegramAlertDelivery,
38+
StrategyPluginTelegramAlertMarkerStore,
39+
StrategyPluginTelegramAlertPublishResult,
40+
StrategyPluginTelegramSettings,
41+
publish_strategy_plugin_telegram_alerts,
42+
)
3543

3644
__all__ = [
3745
"NotificationPublisher",
@@ -47,6 +55,10 @@
4755
"StrategyPluginPushAlertMarkerStore",
4856
"StrategyPluginPushAlertPublishResult",
4957
"StrategyPluginPushSettings",
58+
"StrategyPluginTelegramAlertDelivery",
59+
"StrategyPluginTelegramAlertMarkerStore",
60+
"StrategyPluginTelegramAlertPublishResult",
61+
"StrategyPluginTelegramSettings",
5062
"StrategyPluginSmsAlertDelivery",
5163
"StrategyPluginSmsAlertMarkerStore",
5264
"StrategyPluginSmsAlertPublishResult",
@@ -56,14 +68,18 @@
5668
"parse_email_recipients",
5769
"parse_push_recipients",
5870
"parse_sms_recipients",
71+
"parse_telegram_chat_ids",
5972
"publish_rendered_notification",
6073
"publish_strategy_plugin_alerts",
6174
"publish_strategy_plugin_email_alerts",
6275
"publish_strategy_plugin_push_alerts",
6376
"publish_strategy_plugin_sms_alerts",
77+
"publish_strategy_plugin_telegram_alerts",
6478
"send_ntfy_push",
6579
"send_pushover_push",
6680
"send_smtp_email",
6781
"send_strategy_plugin_push",
82+
"send_strategy_plugin_telegram",
83+
"send_telegram_message",
6884
"send_twilio_sms",
6985
]

src/quant_platform_kit/notifications/strategy_plugin_alerts.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .email import send_smtp_email
1212
from .push import send_strategy_plugin_push
1313
from .sms import send_twilio_sms
14+
from .telegram import send_strategy_plugin_telegram
1415
from .strategy_plugin_email import (
1516
StrategyPluginEmailAlertMarkerStore,
1617
StrategyPluginEmailAlertPublishResult,
@@ -30,12 +31,19 @@
3031
StrategyPluginPushSettings,
3132
publish_strategy_plugin_push_alerts,
3233
)
34+
from .strategy_plugin_telegram import (
35+
StrategyPluginTelegramAlertMarkerStore,
36+
StrategyPluginTelegramAlertPublishResult,
37+
StrategyPluginTelegramSettings,
38+
publish_strategy_plugin_telegram_alerts,
39+
)
3340

3441
_DEFAULT_ALERT_STATE_DIR = "/tmp/quant_strategy_plugin_alerts"
3542
_CHANNEL_EMAIL = "email"
3643
_CHANNEL_SMS = "sms"
3744
_CHANNEL_PUSH = "push"
38-
_SUPPORTED_CHANNELS = frozenset({_CHANNEL_EMAIL, _CHANNEL_SMS, _CHANNEL_PUSH})
45+
_CHANNEL_TELEGRAM = "telegram"
46+
_SUPPORTED_CHANNELS = frozenset({_CHANNEL_EMAIL, _CHANNEL_SMS, _CHANNEL_PUSH, _CHANNEL_TELEGRAM})
3947

4048

4149
@dataclass(frozen=True)
@@ -45,6 +53,7 @@ class StrategyPluginAlertChannelStores:
4553
email: StrategyPluginEmailAlertMarkerStore | object | None = None
4654
sms: StrategyPluginSmsAlertMarkerStore | object | None = None
4755
push: StrategyPluginPushAlertMarkerStore | object | None = None
56+
telegram: StrategyPluginTelegramAlertMarkerStore | object | None = None
4857

4958
@classmethod
5059
def from_mapping(
@@ -57,6 +66,7 @@ def from_mapping(
5766
email=value.get(_CHANNEL_EMAIL),
5867
sms=value.get(_CHANNEL_SMS),
5968
push=value.get(_CHANNEL_PUSH),
69+
telegram=value.get(_CHANNEL_TELEGRAM),
6070
)
6171

6272

@@ -107,6 +117,12 @@ def build_channel_stores(self) -> StrategyPluginAlertChannelStores:
107117
gcp_project_id=self.gcp_project_id,
108118
client_factory=self.client_factory,
109119
),
120+
telegram=StrategyPluginTelegramAlertMarkerStore(
121+
local_dir=self.local_dir,
122+
gcs_prefix_uri=self.gcs_prefix_uri,
123+
gcp_project_id=self.gcp_project_id,
124+
client_factory=self.client_factory,
125+
),
110126
)
111127

112128

@@ -117,6 +133,7 @@ class StrategyPluginAlertPublishResult:
117133
email_result: StrategyPluginEmailAlertPublishResult | None = None
118134
sms_result: StrategyPluginSmsAlertPublishResult | None = None
119135
push_result: StrategyPluginPushAlertPublishResult | None = None
136+
telegram_result: StrategyPluginTelegramAlertPublishResult | None = None
120137

121138
@property
122139
def attempted_count(self) -> int:
@@ -147,6 +164,8 @@ def to_report_fields(self) -> dict[str, Any]:
147164
fields.update(self.sms_result.to_report_fields())
148165
if self.push_result is not None:
149166
fields.update(self.push_result.to_report_fields())
167+
if self.telegram_result is not None:
168+
fields.update(self.telegram_result.to_report_fields())
150169
return fields
151170

152171
def to_summary_fields(self) -> dict[str, int]:
@@ -159,6 +178,8 @@ def to_summary_fields(self) -> dict[str, int]:
159178
fields["strategy_plugin_alert_sms_sent_count"] = self.sms_result.sent_count
160179
if self.push_result is not None:
161180
fields["strategy_plugin_alert_push_sent_count"] = self.push_result.sent_count
181+
if self.telegram_result is not None:
182+
fields["strategy_plugin_alert_telegram_sent_count"] = self.telegram_result.sent_count
162183
return fields
163184

164185
def attach_to_report(self, report: dict[str, Any]) -> None:
@@ -170,12 +191,18 @@ def _results(
170191
) -> tuple[
171192
StrategyPluginEmailAlertPublishResult
172193
| StrategyPluginSmsAlertPublishResult
173-
| StrategyPluginPushAlertPublishResult,
194+
| StrategyPluginPushAlertPublishResult
195+
| StrategyPluginTelegramAlertPublishResult,
174196
...,
175197
]:
176198
return tuple(
177199
result
178-
for result in (self.email_result, self.sms_result, self.push_result)
200+
for result in (
201+
self.email_result,
202+
self.sms_result,
203+
self.push_result,
204+
self.telegram_result,
205+
)
179206
if result is not None
180207
)
181208

@@ -187,6 +214,7 @@ def publish_strategy_plugin_alerts(
187214
StrategyPluginEmailSettings
188215
| StrategyPluginSmsSettings
189216
| StrategyPluginPushSettings
217+
| StrategyPluginTelegramSettings
190218
| object
191219
),
192220
translator: Callable[..., str] | None = None,
@@ -198,6 +226,7 @@ def publish_strategy_plugin_alerts(
198226
send_email_notification: Callable[..., bool] = send_smtp_email,
199227
send_sms_notification: Callable[..., bool] = send_twilio_sms,
200228
send_push_notification: Callable[..., bool] = send_strategy_plugin_push,
229+
send_telegram_notification: Callable[..., bool] = send_strategy_plugin_telegram,
201230
log_message: Callable[..., Any] = print,
202231
) -> StrategyPluginAlertPublishResult:
203232
"""Publish strategy plugin alerts through the configured notification channels."""
@@ -207,6 +236,7 @@ def publish_strategy_plugin_alerts(
207236
email_result = None
208237
sms_result = None
209238
push_result = None
239+
telegram_result = None
210240
if _CHANNEL_EMAIL in selected_channels:
211241
email_result = publish_strategy_plugin_email_alerts(
212242
signals,
@@ -240,10 +270,22 @@ def publish_strategy_plugin_alerts(
240270
send_notification=send_push_notification,
241271
log_message=log_message,
242272
)
273+
if _CHANNEL_TELEGRAM in selected_channels:
274+
telegram_result = publish_strategy_plugin_telegram_alerts(
275+
signals,
276+
telegram_settings=notification_settings,
277+
translator=translator,
278+
strategy_label=strategy_label,
279+
context_label=context_label,
280+
alert_store=stores.telegram,
281+
send_notification=send_telegram_notification,
282+
log_message=log_message,
283+
)
243284
return StrategyPluginAlertPublishResult(
244285
email_result=email_result,
245286
sms_result=sms_result,
246287
push_result=push_result,
288+
telegram_result=telegram_result,
247289
)
248290

249291

@@ -287,7 +329,7 @@ def _resolve_channels(
287329
if raw_channels is None:
288330
raw_channels = _get_value(notification_settings, "crisis_alert_channels", None)
289331
if raw_channels in (None, "", (), []):
290-
raw_channels = (_CHANNEL_EMAIL, _CHANNEL_SMS, _CHANNEL_PUSH)
332+
raw_channels = (_CHANNEL_EMAIL, _CHANNEL_SMS, _CHANNEL_PUSH, _CHANNEL_TELEGRAM)
291333
return _normalize_channels(raw_channels)
292334

293335

0 commit comments

Comments
 (0)