Skip to content

Commit 5253532

Browse files
committed
Split plugin alerts from strategy position notices
1 parent 0d23179 commit 5253532

4 files changed

Lines changed: 111 additions & 1 deletion

File tree

docs/strategy_plugin_runtime_contract.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,22 @@ when any of the following is true:
177177
- `suggested_action` is `defend` or `blocked`
178178
- `would_trade_if_enabled` is `true`
179179

180+
Strategy-mounted artifacts that are automation-approved, expose
181+
`position_control_allowed = true`, and request an automatic `defend` or
182+
`delever` action are intentionally excluded from the dedicated plugin-alert
183+
stream. Those position-impacting events should be reported by the strategy run
184+
that consumed the artifact. The plugin-alert stream remains for manual-review
185+
or notification-only cases, including `notification_targets`, `blocked`,
186+
`watch_only`, and `notify_manual_review` routes.
187+
180188
Platforms may still choose their delivery sinks, but shared escalation helpers
181-
are available for email and SMS:
189+
are available for email, SMS, push, and Telegram:
182190

191+
- `quant_platform_kit.notifications.strategy_plugin_alerts.publish_strategy_plugin_alerts()`
183192
- `quant_platform_kit.notifications.strategy_plugin_email.publish_strategy_plugin_email_alerts()`
184193
- `quant_platform_kit.notifications.strategy_plugin_sms.publish_strategy_plugin_sms_alerts()`
194+
- `quant_platform_kit.notifications.strategy_plugin_push.publish_strategy_plugin_push_alerts()`
195+
- `quant_platform_kit.notifications.strategy_plugin_telegram.publish_strategy_plugin_telegram_alerts()`
185196

186197
The publishers build the shared subject/body, prefix platform context, return
187198
structured sent/skipped/failed diagnostics, and can use marker stores to skip

docs/strategy_plugin_runtime_contract.zh-CN.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,21 @@ sidecar 路径维护插件账本或执行插件驱动的 allocation 变更。
162162
- `suggested_action``defend``blocked`
163163
- `would_trade_if_enabled``true`
164164

165+
如果 strategy-mounted artifact 已经是 `automation_approved`、暴露
166+
`position_control_allowed = true`,并且请求自动 `defend``delever`
167+
动作,则专用插件告警流会刻意跳过它。这类会影响仓位的事件应由实际消费该
168+
artifact 的策略运行结果通知。插件告警流只保留给人工复核或 notification-only
169+
场景,包括 `notification_targets``blocked``watch_only`
170+
`notify_manual_review` 路线。
171+
165172
平台仍可选择自己的投递 sink;共享 helper 已提供 email、SMS、push 和
166173
Telegram 的聚合入口:
167174

168175
- `quant_platform_kit.notifications.strategy_plugin_alerts.publish_strategy_plugin_alerts()`
169176
- `quant_platform_kit.notifications.strategy_plugin_email.publish_strategy_plugin_email_alerts()`
170177
- `quant_platform_kit.notifications.strategy_plugin_sms.publish_strategy_plugin_sms_alerts()`
178+
- `quant_platform_kit.notifications.strategy_plugin_push.publish_strategy_plugin_push_alerts()`
179+
- `quant_platform_kit.notifications.strategy_plugin_telegram.publish_strategy_plugin_telegram_alerts()`
171180

172181
publisher 会构造共享 subject/body、追加平台上下文、返回结构化
173182
sent/skipped/failed diagnostics,并可使用 marker store 跳过某个通道已发送过的

src/quant_platform_kit/common/strategy_plugins.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
DEFAULT_PLUGIN_ARTIFACT_CACHE_DIR = Path(tempfile.gettempdir()) / "quant_strategy_plugin_artifacts"
3333
STRATEGY_PLUGIN_NON_ALERT_ROUTES = frozenset({"no_action"})
3434
STRATEGY_PLUGIN_ALERT_ACTIONS = frozenset({"defend", "blocked"})
35+
STRATEGY_PLUGIN_AUTOMATED_POSITION_ACTIONS = frozenset({"defend", "delever"})
3536
CRISIS_RESPONSE_SHADOW_SUPPORTED_STRATEGIES = frozenset(
3637
{
3738
"tqqq_growth_income",
@@ -930,13 +931,37 @@ def build_strategy_plugin_notification_lines(
930931
def should_alert_strategy_plugin_signal(signal: StrategyPluginSignal) -> bool:
931932
route = _normalize_strategy_plugin_field(getattr(signal, "canonical_route", None))
932933
action = _normalize_strategy_plugin_field(getattr(signal, "suggested_action", None))
934+
if _is_strategy_position_control_notice(signal, action=action):
935+
return False
933936
return (
934937
bool(getattr(signal, "would_trade_if_enabled", False))
935938
or route not in STRATEGY_PLUGIN_NON_ALERT_ROUTES
936939
or action in STRATEGY_PLUGIN_ALERT_ACTIONS
937940
)
938941

939942

943+
def _is_strategy_position_control_notice(signal: StrategyPluginSignal, *, action: str | None = None) -> bool:
944+
"""Return true when strategy runtime should carry the alert instead of the plugin bot."""
945+
946+
target_type = _normalize_strategy_plugin_field(getattr(signal, "target_type", None)) or "strategy"
947+
if target_type != "strategy":
948+
return False
949+
normalized_action = action if action is not None else _normalize_strategy_plugin_field(
950+
getattr(signal, "suggested_action", None)
951+
)
952+
if normalized_action not in STRATEGY_PLUGIN_AUTOMATED_POSITION_ACTIONS:
953+
return False
954+
controls = getattr(signal, "execution_controls", {}) or {}
955+
if not isinstance(controls, Mapping):
956+
return False
957+
if not _as_bool(controls.get("strategy_runtime_metadata_allowed"), default=False):
958+
return False
959+
if not _as_bool(controls.get("position_control_allowed"), default=False):
960+
return False
961+
evidence_status = _normalize_strategy_plugin_field(controls.get("consumption_evidence_status"))
962+
return evidence_status == "automation_approved"
963+
964+
940965
def build_strategy_plugin_alert_guidance(
941966
signal: StrategyPluginSignal,
942967
*,

tests/test_strategy_plugins.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,71 @@ def test_strategy_plugin_no_action_signal_does_not_escalate_alert(self):
663663
self.assertFalse(should_alert_strategy_plugin_signal(signal))
664664
self.assertEqual(build_strategy_plugin_alert_messages([signal]), ())
665665

666+
def test_strategy_plugin_auto_position_control_signal_stays_with_strategy_notification(self):
667+
signal = validate_strategy_plugin_signal_payload(
668+
{
669+
**_signal_payload(plugin=PLUGIN_MARKET_REGIME_CONTROL),
670+
"canonical_route": "risk_off",
671+
"suggested_action": "defend",
672+
"would_trade_if_enabled": True,
673+
"execution_controls": {
674+
**_signal_payload()["execution_controls"],
675+
"strategy_runtime_metadata_allowed": True,
676+
"position_control_allowed": True,
677+
"consumption_evidence_status": "automation_approved",
678+
},
679+
}
680+
)
681+
682+
self.assertFalse(should_alert_strategy_plugin_signal(signal))
683+
self.assertEqual(build_strategy_plugin_alert_messages([signal]), ())
684+
685+
def test_strategy_plugin_notification_target_still_alerts_plugin_bot(self):
686+
signal = validate_strategy_plugin_signal_payload(
687+
{
688+
**_signal_payload(plugin=PLUGIN_MARKET_REGIME_CONTROL),
689+
"target_type": "notification_target",
690+
"strategy": "",
691+
"notification_target": GENERAL_MARKET_REGIME_NOTIFICATION_TARGET,
692+
"canonical_route": "risk_off",
693+
"suggested_action": "defend",
694+
"would_trade_if_enabled": True,
695+
"execution_controls": {
696+
**_signal_payload()["execution_controls"],
697+
"strategy_runtime_metadata_allowed": False,
698+
"position_control_allowed": False,
699+
"consumption_evidence_status": "notification_only",
700+
"capital_impact": "notification_only",
701+
},
702+
}
703+
)
704+
705+
self.assertTrue(should_alert_strategy_plugin_signal(signal))
706+
alerts = build_strategy_plugin_alert_messages([signal])
707+
self.assertEqual(len(alerts), 1)
708+
self.assertEqual(alerts[0].metadata["target_type"], "notification_target")
709+
710+
def test_strategy_plugin_manual_review_strategy_signal_still_alerts_plugin_bot(self):
711+
signal = validate_strategy_plugin_signal_payload(
712+
{
713+
**_signal_payload(plugin=PLUGIN_MARKET_REGIME_CONTROL),
714+
"canonical_route": "opportunity_watch",
715+
"suggested_action": "notify_manual_review",
716+
"would_trade_if_enabled": False,
717+
"execution_controls": {
718+
**_signal_payload()["execution_controls"],
719+
"strategy_runtime_metadata_allowed": True,
720+
"position_control_allowed": True,
721+
"consumption_evidence_status": "automation_approved",
722+
},
723+
}
724+
)
725+
726+
self.assertTrue(should_alert_strategy_plugin_signal(signal))
727+
alerts = build_strategy_plugin_alert_messages([signal])
728+
self.assertEqual(len(alerts), 1)
729+
self.assertIn("Manual review only", alerts[0].body)
730+
666731
def test_strategy_plugin_true_crisis_builds_generic_alert_message(self):
667732
signal = validate_strategy_plugin_signal_payload(
668733
{

0 commit comments

Comments
 (0)