From 3838bf8968f11217fd0687a1ba2ef2616059d935 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Wed, 17 Jun 2026 02:39:40 +0800 Subject: [PATCH] Delegate strategy plugin manual alerts --- docs/strategy_plugin_runtime_contract.md | 17 ++++- .../strategy_plugin_runtime_contract.zh-CN.md | 12 +++- .../common/notification_localization.py | 6 ++ .../common/strategy_plugins.py | 28 +++++++- tests/test_strategy_plugins.py | 72 +++++++++++++++++++ 5 files changed, 129 insertions(+), 6 deletions(-) diff --git a/docs/strategy_plugin_runtime_contract.md b/docs/strategy_plugin_runtime_contract.md index 9b04e51..00ed0a2 100644 --- a/docs/strategy_plugin_runtime_contract.md +++ b/docs/strategy_plugin_runtime_contract.md @@ -96,6 +96,12 @@ defaults may consume `risk_off` and deterministic default. Broad market-regime notifications may still be published through the separate `notification_targets.market_regime_notification` artifact for manual review; notification-target artifacts cannot affect position sizing. +When a strategy-mounted market-regime artifact carries +`execution_controls.manual_review_notification_delegated = true`, platform +strategy runners should treat manual-review plugin-bot delivery as delegated to +that notification target. They may still attach the strategy artifact to runtime +metadata and may still report any actual position effect in the strategy run +notification. SOXL retention profiles may include a deterministic SOXX price/volatility rebound context. That context is backtestable hard-data evidence only and must not promote TACO, panic reversal, AI audit, OSINT, or localized copy into @@ -190,9 +196,14 @@ Strategy-mounted artifacts that are automation-approved, expose `position_control_allowed = true`, and request an automatic `defend` or `delever` action are intentionally excluded from the dedicated plugin-alert stream. Those position-impacting events should be reported by the strategy run -that consumed the artifact. The plugin-alert stream remains for manual-review -or notification-only cases, including `notification_targets`, `blocked`, -`watch_only`, and `notify_manual_review` routes. +that consumed the artifact. Strategy artifacts can also explicitly delegate +manual-review plugin-bot delivery with +`execution_controls.manual_review_notification_delegated = true` plus +`manual_review_notification_target`; those delegated alerts are sent once from +the matching `notification_targets` artifact. The plugin-alert stream remains +for non-delegated manual-review or notification-only cases, including +`notification_targets`, `blocked`, `watch_only`, and `notify_manual_review` +routes. Platforms may still choose their delivery sinks, but shared escalation helpers are available for email, SMS, push, and Telegram: diff --git a/docs/strategy_plugin_runtime_contract.zh-CN.md b/docs/strategy_plugin_runtime_contract.zh-CN.md index 93071a6..cd1867e 100644 --- a/docs/strategy_plugin_runtime_contract.zh-CN.md +++ b/docs/strategy_plugin_runtime_contract.zh-CN.md @@ -85,6 +85,10 @@ SOXL/SOXX 已列入 `market_regime_control` 的运行时挂载清单。策略默 `risk_off` 和确定性的 `position_control.volatility_delever_context` retention profiles;`risk_reduced` 仓位影响仍在策略默认配置中关闭。广义市场状态通知仍可通过独立的 `notification_targets.market_regime_notification` artifact 分发给人工复核;notification-target artifact 不能影响仓位。 +当 strategy-mounted market-regime artifact 带有 +`execution_controls.manual_review_notification_delegated = true` 时,平台策略 +runner 应把人工复核插件 bot 通知视为已委托给该 notification target。策略 +runner 仍可把 strategy artifact 挂入 runtime metadata,并在策略运行通知中报告实际仓位影响。 SOXL retention profiles 可以包含确定性的 SOXX 价格/波动反弹上下文。该上下文只使用可回测硬数据, 不能把 TACO、panic reversal、AI audit、OSINT 或本地化文案升级成自动仓位权限。 @@ -171,8 +175,12 @@ sidecar 路径维护插件账本或执行插件驱动的 allocation 变更。 如果 strategy-mounted artifact 已经是 `automation_approved`、暴露 `position_control_allowed = true`,并且请求自动 `defend` 或 `delever` 动作,则专用插件告警流会刻意跳过它。这类会影响仓位的事件应由实际消费该 -artifact 的策略运行结果通知。插件告警流只保留给人工复核或 notification-only -场景,包括 `notification_targets`、`blocked`、`watch_only` 和 +artifact 的策略运行结果通知。strategy artifact 也可以通过 +`execution_controls.manual_review_notification_delegated = true` 和 +`manual_review_notification_target` 明确把人工复核插件 bot 通知委托给统一 +notification target;这类委托告警只从对应 `notification_targets` artifact +发送一次。插件告警流只保留给未委托的人工复核或 notification-only 场景, +包括 `notification_targets`、`blocked`、`watch_only` 和 `notify_manual_review` 路线。 平台仍可选择自己的投递 sink;共享 helper 已提供 email、SMS、push 和 diff --git a/src/quant_platform_kit/common/notification_localization.py b/src/quant_platform_kit/common/notification_localization.py index 0c5a243..d3c4406 100644 --- a/src/quant_platform_kit/common/notification_localization.py +++ b/src/quant_platform_kit/common/notification_localization.py @@ -30,6 +30,9 @@ "strategy_plugin_alert_subject": "🚨 策略插件告警:{plugin} | {route}", "strategy_plugin_alert_title": "🚨 【策略插件告警】", "strategy_plugin_alert_context": "运行环境:{context}", + "strategy_plugin_alert_target": "{target_name}:{target}", + "strategy_plugin_alert_target_name_strategy": "策略", + "strategy_plugin_alert_target_name_notification_target": "通知目标", "strategy_plugin_alert_strategy": "策略:{strategy}", "strategy_plugin_alert_plugin": "插件:{plugin}", "strategy_plugin_alert_status": "状态:{route}", @@ -80,6 +83,9 @@ "strategy_plugin_alert_subject": "🚨 Strategy plugin alert: {plugin} | {route}", "strategy_plugin_alert_title": "🚨 【Strategy Plugin Alert】", "strategy_plugin_alert_context": "Context: {context}", + "strategy_plugin_alert_target": "{target_name}: {target}", + "strategy_plugin_alert_target_name_strategy": "Strategy", + "strategy_plugin_alert_target_name_notification_target": "Notification target", "strategy_plugin_alert_strategy": "Strategy: {strategy}", "strategy_plugin_alert_plugin": "Plugin: {plugin}", "strategy_plugin_alert_status": "Status: {route}", diff --git a/src/quant_platform_kit/common/strategy_plugins.py b/src/quant_platform_kit/common/strategy_plugins.py index 699dc80..76741f8 100644 --- a/src/quant_platform_kit/common/strategy_plugins.py +++ b/src/quant_platform_kit/common/strategy_plugins.py @@ -933,6 +933,8 @@ def should_alert_strategy_plugin_signal(signal: StrategyPluginSignal) -> bool: action = _normalize_strategy_plugin_field(getattr(signal, "suggested_action", None)) if _is_strategy_position_control_notice(signal, action=action): return False + if _is_strategy_manual_review_notification_delegated(signal): + return False return ( bool(getattr(signal, "would_trade_if_enabled", False)) or route not in STRATEGY_PLUGIN_NON_ALERT_ROUTES @@ -962,6 +964,21 @@ def _is_strategy_position_control_notice(signal: StrategyPluginSignal, *, action return evidence_status == "automation_approved" +def _is_strategy_manual_review_notification_delegated(signal: StrategyPluginSignal) -> bool: + """Return true when a strategy artifact delegates human-review alerts to a notification target.""" + + target_type = _normalize_strategy_plugin_field(getattr(signal, "target_type", None)) or "strategy" + if target_type != "strategy": + return False + controls = getattr(signal, "execution_controls", {}) or {} + if not isinstance(controls, Mapping): + return False + if not _as_bool(controls.get("manual_review_notification_delegated"), default=False): + return False + notification_target = _optional_string(controls.get("manual_review_notification_target")) + return notification_target is not None + + def build_strategy_plugin_alert_guidance( signal: StrategyPluginSignal, *, @@ -1124,7 +1141,16 @@ def build_strategy_plugin_alert_messages( strategy = str(strategy_label or getattr(signal, "strategy", None) or "").strip() notification_target = str(getattr(signal, "notification_target", None) or "").strip() target_label = strategy or notification_target or "unknown" - target_name = "Notification target" if target_type == "notification_target" else "Strategy" + target_name_key = ( + "strategy_plugin_alert_target_name_notification_target" + if target_type == "notification_target" + else "strategy_plugin_alert_target_name_strategy" + ) + target_name = _translate( + translator, + target_name_key, + fallback="Notification target" if target_type == "notification_target" else "Strategy", + ) guidance = build_strategy_plugin_alert_guidance(signal, translator=translator) scope_note = build_strategy_plugin_alert_scope_note(signal, translator=translator) ai_audit_note = build_strategy_plugin_ai_audit_note(signal, translator=translator) diff --git a/tests/test_strategy_plugins.py b/tests/test_strategy_plugins.py index 2bcf3ef..a8a114b 100644 --- a/tests/test_strategy_plugins.py +++ b/tests/test_strategy_plugins.py @@ -728,6 +728,78 @@ def test_strategy_plugin_manual_review_strategy_signal_still_alerts_plugin_bot(s self.assertEqual(len(alerts), 1) self.assertIn("Manual review only", alerts[0].body) + def test_delegated_manual_review_strategy_signal_stays_with_notification_target(self): + signal = validate_strategy_plugin_signal_payload( + { + **_signal_payload(plugin=PLUGIN_MARKET_REGIME_CONTROL), + "canonical_route": "opportunity_watch", + "suggested_action": "notify_manual_review", + "would_trade_if_enabled": False, + "execution_controls": { + **_signal_payload()["execution_controls"], + "strategy_runtime_metadata_allowed": True, + "position_control_allowed": True, + "consumption_evidence_status": "automation_approved", + "manual_review_notification_delegated": True, + "manual_review_notification_target": GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, + "manual_review_notification_delegate": ( + f"notification_target:{GENERAL_MARKET_REGIME_NOTIFICATION_TARGET}" + ), + }, + } + ) + + self.assertFalse(should_alert_strategy_plugin_signal(signal)) + self.assertEqual(build_strategy_plugin_alert_messages([signal]), ()) + + def test_notification_target_alert_uses_localized_target_name(self): + signal = validate_strategy_plugin_signal_payload( + { + **_signal_payload(plugin=PLUGIN_MARKET_REGIME_CONTROL), + "target_type": "notification_target", + "strategy": "", + "notification_target": GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, + "canonical_route": "watch", + "suggested_action": "notify_manual_review", + "would_trade_if_enabled": False, + "execution_controls": { + **_signal_payload()["execution_controls"], + "strategy_runtime_metadata_allowed": False, + "position_control_allowed": False, + "consumption_evidence_status": "notification_only", + "capital_impact": "notification_only", + }, + } + ) + translations = { + "strategy_plugin_alert_subject": "告警:{plugin}:{route}", + "strategy_plugin_alert_title": "插件告警", + "strategy_plugin_alert_target": "{target_name}={target}", + "strategy_plugin_alert_target_name_notification_target": "通知目标", + "strategy_plugin_alert_plugin": "插件={plugin}", + "strategy_plugin_alert_status": "状态={route}", + "strategy_plugin_alert_action": "建议={action}", + "strategy_plugin_alert_mode": "模式={mode}", + "strategy_plugin_alert_as_of": "时间={as_of}", + "strategy_plugin_alert_scope_note": "范围={scope_note}", + "strategy_plugin_alert_scope": "只通知人工复核", + "strategy_plugin_name_market_regime_control": "市场状态控制通知", + "strategy_plugin_mode_shadow": "影子观察", + "strategy_plugin_route_watch": "观察", + "strategy_plugin_action_notify_manual_review": "通知人工复核", + } + + alerts = build_strategy_plugin_alert_messages( + [signal], + translator=lambda key, **kwargs: translations.get(key, key).format(**kwargs) + if kwargs + else translations.get(key, key), + ) + + self.assertEqual(len(alerts), 1) + self.assertIn("通知目标=market_regime_notification", alerts[0].body) + self.assertNotIn("Notification target", alerts[0].body) + def test_strategy_plugin_true_crisis_builds_generic_alert_message(self): signal = validate_strategy_plugin_signal_payload( {