Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ It contains:
- narrow ports for market data, portfolio snapshots, order execution, notifications, and state
- reusable broker adapter utilities
- strategy loading, strategy-plugin, and alert-message contracts
- optional strategy-plugin alert channels for email, SMS, and push providers
- optional strategy-plugin alert channels for email, SMS, push, and Telegram providers
- synthetic-data tests for public behavior

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

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.

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.
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.

## Package Layout

Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
- 市场数据、持仓快照、订单执行、通知、状态存储等窄接口
- 可复用的券商适配工具
- 策略加载、策略插件、告警消息契约
- 可选的策略插件 email、SMS 和 push 告警通道
- 可选的策略插件 email、SMS、pushTelegram 告警通道
- 使用合成数据的公开测试

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

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

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

## 目录结构

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "quant-platform-kit"
version = "0.7.29"
version = "0.7.30"
description = "Shared broker adapters, domain models, execution ports, and notification utilities for QuantStrategyLab strategies."
readme = "README.md"
requires-python = ">=3.9"
Expand Down
2 changes: 2 additions & 0 deletions src/quant_platform_kit/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL,
STRATEGY_PLUGIN_ALERT_CHANNEL_PUSH,
STRATEGY_PLUGIN_ALERT_CHANNEL_SMS,
STRATEGY_PLUGIN_ALERT_CHANNEL_TELEGRAM,
STRATEGY_PLUGIN_ALERT_ACTIONS,
STRATEGY_PLUGIN_NON_ALERT_ROUTES,
SUPPORTED_STRATEGY_PLUGIN_MODES,
Expand Down Expand Up @@ -89,6 +90,7 @@
"STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL",
"STRATEGY_PLUGIN_ALERT_CHANNEL_PUSH",
"STRATEGY_PLUGIN_ALERT_CHANNEL_SMS",
"STRATEGY_PLUGIN_ALERT_CHANNEL_TELEGRAM",
"STRATEGY_PLUGIN_ALERT_ACTIONS",
"STRATEGY_PLUGIN_NON_ALERT_ROUTES",
"SUPPORTED_STRATEGY_PLUGIN_MODES",
Expand Down
2 changes: 2 additions & 0 deletions src/quant_platform_kit/common/strategy_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL = "email"
STRATEGY_PLUGIN_ALERT_CHANNEL_SMS = "sms"
STRATEGY_PLUGIN_ALERT_CHANNEL_PUSH = "push"
STRATEGY_PLUGIN_ALERT_CHANNEL_TELEGRAM = "telegram"
SUPPORTED_STRATEGY_PLUGIN_MODES = frozenset({PLUGIN_MODE_SHADOW})
DEFAULT_PLUGIN_ARTIFACT_CACHE_DIR = Path(tempfile.gettempdir()) / "quant_strategy_plugin_artifacts"
STRATEGY_PLUGIN_NON_ALERT_ROUTES = frozenset({"no_action"})
Expand Down Expand Up @@ -73,6 +74,7 @@ def supports_strategy(self, strategy: str) -> bool:
STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL,
STRATEGY_PLUGIN_ALERT_CHANNEL_SMS,
STRATEGY_PLUGIN_ALERT_CHANNEL_PUSH,
STRATEGY_PLUGIN_ALERT_CHANNEL_TELEGRAM,
),
)
}
Expand Down
16 changes: 16 additions & 0 deletions src/quant_platform_kit/notifications/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .events import NotificationPublisher, RenderedNotification, publish_rendered_notification
from .push import parse_push_recipients, send_ntfy_push, send_pushover_push, send_strategy_plugin_push
from .sms import normalize_sms_recipient, parse_sms_recipients, send_twilio_sms
from .telegram import parse_telegram_chat_ids, send_strategy_plugin_telegram, send_telegram_message
from .strategy_plugin_alerts import (
StrategyPluginAlertChannelStores,
StrategyPluginAlertPublishResult,
Expand Down Expand Up @@ -32,6 +33,13 @@
StrategyPluginPushSettings,
publish_strategy_plugin_push_alerts,
)
from .strategy_plugin_telegram import (
StrategyPluginTelegramAlertDelivery,
StrategyPluginTelegramAlertMarkerStore,
StrategyPluginTelegramAlertPublishResult,
StrategyPluginTelegramSettings,
publish_strategy_plugin_telegram_alerts,
)

__all__ = [
"NotificationPublisher",
Expand All @@ -47,6 +55,10 @@
"StrategyPluginPushAlertMarkerStore",
"StrategyPluginPushAlertPublishResult",
"StrategyPluginPushSettings",
"StrategyPluginTelegramAlertDelivery",
"StrategyPluginTelegramAlertMarkerStore",
"StrategyPluginTelegramAlertPublishResult",
"StrategyPluginTelegramSettings",
"StrategyPluginSmsAlertDelivery",
"StrategyPluginSmsAlertMarkerStore",
"StrategyPluginSmsAlertPublishResult",
Expand All @@ -56,14 +68,18 @@
"parse_email_recipients",
"parse_push_recipients",
"parse_sms_recipients",
"parse_telegram_chat_ids",
"publish_rendered_notification",
"publish_strategy_plugin_alerts",
"publish_strategy_plugin_email_alerts",
"publish_strategy_plugin_push_alerts",
"publish_strategy_plugin_sms_alerts",
"publish_strategy_plugin_telegram_alerts",
"send_ntfy_push",
"send_pushover_push",
"send_smtp_email",
"send_strategy_plugin_push",
"send_strategy_plugin_telegram",
"send_telegram_message",
"send_twilio_sms",
]
50 changes: 46 additions & 4 deletions src/quant_platform_kit/notifications/strategy_plugin_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .email import send_smtp_email
from .push import send_strategy_plugin_push
from .sms import send_twilio_sms
from .telegram import send_strategy_plugin_telegram
from .strategy_plugin_email import (
StrategyPluginEmailAlertMarkerStore,
StrategyPluginEmailAlertPublishResult,
Expand All @@ -30,12 +31,19 @@
StrategyPluginPushSettings,
publish_strategy_plugin_push_alerts,
)
from .strategy_plugin_telegram import (
StrategyPluginTelegramAlertMarkerStore,
StrategyPluginTelegramAlertPublishResult,
StrategyPluginTelegramSettings,
publish_strategy_plugin_telegram_alerts,
)

_DEFAULT_ALERT_STATE_DIR = "/tmp/quant_strategy_plugin_alerts"
_CHANNEL_EMAIL = "email"
_CHANNEL_SMS = "sms"
_CHANNEL_PUSH = "push"
_SUPPORTED_CHANNELS = frozenset({_CHANNEL_EMAIL, _CHANNEL_SMS, _CHANNEL_PUSH})
_CHANNEL_TELEGRAM = "telegram"
_SUPPORTED_CHANNELS = frozenset({_CHANNEL_EMAIL, _CHANNEL_SMS, _CHANNEL_PUSH, _CHANNEL_TELEGRAM})


@dataclass(frozen=True)
Expand All @@ -45,6 +53,7 @@ class StrategyPluginAlertChannelStores:
email: StrategyPluginEmailAlertMarkerStore | object | None = None
sms: StrategyPluginSmsAlertMarkerStore | object | None = None
push: StrategyPluginPushAlertMarkerStore | object | None = None
telegram: StrategyPluginTelegramAlertMarkerStore | object | None = None

@classmethod
def from_mapping(
Expand All @@ -57,6 +66,7 @@ def from_mapping(
email=value.get(_CHANNEL_EMAIL),
sms=value.get(_CHANNEL_SMS),
push=value.get(_CHANNEL_PUSH),
telegram=value.get(_CHANNEL_TELEGRAM),
)


Expand Down Expand Up @@ -107,6 +117,12 @@ def build_channel_stores(self) -> StrategyPluginAlertChannelStores:
gcp_project_id=self.gcp_project_id,
client_factory=self.client_factory,
),
telegram=StrategyPluginTelegramAlertMarkerStore(
local_dir=self.local_dir,
gcs_prefix_uri=self.gcs_prefix_uri,
gcp_project_id=self.gcp_project_id,
client_factory=self.client_factory,
),
)


Expand All @@ -117,6 +133,7 @@ class StrategyPluginAlertPublishResult:
email_result: StrategyPluginEmailAlertPublishResult | None = None
sms_result: StrategyPluginSmsAlertPublishResult | None = None
push_result: StrategyPluginPushAlertPublishResult | None = None
telegram_result: StrategyPluginTelegramAlertPublishResult | None = None

@property
def attempted_count(self) -> int:
Expand Down Expand Up @@ -147,6 +164,8 @@ def to_report_fields(self) -> dict[str, Any]:
fields.update(self.sms_result.to_report_fields())
if self.push_result is not None:
fields.update(self.push_result.to_report_fields())
if self.telegram_result is not None:
fields.update(self.telegram_result.to_report_fields())
return fields

def to_summary_fields(self) -> dict[str, int]:
Expand All @@ -159,6 +178,8 @@ def to_summary_fields(self) -> dict[str, int]:
fields["strategy_plugin_alert_sms_sent_count"] = self.sms_result.sent_count
if self.push_result is not None:
fields["strategy_plugin_alert_push_sent_count"] = self.push_result.sent_count
if self.telegram_result is not None:
fields["strategy_plugin_alert_telegram_sent_count"] = self.telegram_result.sent_count
return fields

def attach_to_report(self, report: dict[str, Any]) -> None:
Expand All @@ -170,12 +191,18 @@ def _results(
) -> tuple[
StrategyPluginEmailAlertPublishResult
| StrategyPluginSmsAlertPublishResult
| StrategyPluginPushAlertPublishResult,
| StrategyPluginPushAlertPublishResult
| StrategyPluginTelegramAlertPublishResult,
...,
]:
return tuple(
result
for result in (self.email_result, self.sms_result, self.push_result)
for result in (
self.email_result,
self.sms_result,
self.push_result,
self.telegram_result,
)
if result is not None
)

Expand All @@ -187,6 +214,7 @@ def publish_strategy_plugin_alerts(
StrategyPluginEmailSettings
| StrategyPluginSmsSettings
| StrategyPluginPushSettings
| StrategyPluginTelegramSettings
| object
),
translator: Callable[..., str] | None = None,
Expand All @@ -198,6 +226,7 @@ def publish_strategy_plugin_alerts(
send_email_notification: Callable[..., bool] = send_smtp_email,
send_sms_notification: Callable[..., bool] = send_twilio_sms,
send_push_notification: Callable[..., bool] = send_strategy_plugin_push,
send_telegram_notification: Callable[..., bool] = send_strategy_plugin_telegram,
log_message: Callable[..., Any] = print,
) -> StrategyPluginAlertPublishResult:
"""Publish strategy plugin alerts through the configured notification channels."""
Expand All @@ -207,6 +236,7 @@ def publish_strategy_plugin_alerts(
email_result = None
sms_result = None
push_result = None
telegram_result = None
if _CHANNEL_EMAIL in selected_channels:
email_result = publish_strategy_plugin_email_alerts(
signals,
Expand Down Expand Up @@ -240,10 +270,22 @@ def publish_strategy_plugin_alerts(
send_notification=send_push_notification,
log_message=log_message,
)
if _CHANNEL_TELEGRAM in selected_channels:
telegram_result = publish_strategy_plugin_telegram_alerts(
signals,
telegram_settings=notification_settings,
translator=translator,
strategy_label=strategy_label,
context_label=context_label,
alert_store=stores.telegram,
send_notification=send_telegram_notification,
log_message=log_message,
)
return StrategyPluginAlertPublishResult(
email_result=email_result,
sms_result=sms_result,
push_result=push_result,
telegram_result=telegram_result,
)


Expand Down Expand Up @@ -287,7 +329,7 @@ def _resolve_channels(
if raw_channels is None:
raw_channels = _get_value(notification_settings, "crisis_alert_channels", None)
if raw_channels in (None, "", (), []):
raw_channels = (_CHANNEL_EMAIL, _CHANNEL_SMS, _CHANNEL_PUSH)
raw_channels = (_CHANNEL_EMAIL, _CHANNEL_SMS, _CHANNEL_PUSH, _CHANNEL_TELEGRAM)
return _normalize_channels(raw_channels)


Expand Down
Loading