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
17 changes: 14 additions & 3 deletions docs/strategy_plugin_runtime_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 10 additions & 2 deletions docs/strategy_plugin_runtime_contract.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 或本地化文案升级成自动仓位权限。

Expand Down Expand Up @@ -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 和
Expand Down
6 changes: 6 additions & 0 deletions src/quant_platform_kit/common/notification_localization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down Expand Up @@ -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}",
Expand Down
28 changes: 27 additions & 1 deletion src/quant_platform_kit/common/strategy_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +936 to +937

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict delegated suppression to manual-review alerts

When a strategy artifact has manual_review_notification_delegated=true, this early return runs before the normal escalation predicates, so a later blocked/blocked signal or other non-manual-review alert with the delegation flag still set produces no plugin-bot message. The contract keeps blocked cases in the plugin-alert stream, so this should only suppress the specific delegated manual-review route/action rather than every alert from that artifact.

Useful? React with 👍 / 👎.

return (
bool(getattr(signal, "would_trade_if_enabled", False))
or route not in STRATEGY_PLUGIN_NON_ALERT_ROUTES
Expand Down Expand Up @@ -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,
*,
Expand Down Expand Up @@ -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)
Expand Down
72 changes: 72 additions & 0 deletions tests/test_strategy_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down