diff --git a/docs/strategy_plugin_runtime_contract.md b/docs/strategy_plugin_runtime_contract.md index f8a85c3..9b04e51 100644 --- a/docs/strategy_plugin_runtime_contract.md +++ b/docs/strategy_plugin_runtime_contract.md @@ -167,6 +167,11 @@ Artifacts may include display-only i18n fields: Platform renderers may use these fields for notification and log text. Trading logic must continue to read machine fields such as `schema_version`, `canonical_route`, `suggested_action`, `reason_codes`, and `position_control`. +Shared `strategy_plugin_*` notification labels live in +`quant_platform_kit.common.notification_localization.STRATEGY_PLUGIN_I18N`; +broker platforms can merge them with local text through +`merge_strategy_plugin_i18n()` so the plugin alert wording stays consistent +across runtimes. `paper`, `advisory`, and `live` plugin modes are not supported by the shared contract. Platforms should not maintain plugin ledgers or execute plugin-driven diff --git a/docs/strategy_plugin_runtime_contract.zh-CN.md b/docs/strategy_plugin_runtime_contract.zh-CN.md index a7c2b85..93071a6 100644 --- a/docs/strategy_plugin_runtime_contract.zh-CN.md +++ b/docs/strategy_plugin_runtime_contract.zh-CN.md @@ -152,6 +152,10 @@ artifact 可以包含展示层 i18n 字段: 平台 renderer 可以使用这些字段渲染通知和日志文案。交易逻辑必须继续读取 `schema_version`、`canonical_route`、`suggested_action`、`reason_codes` 和 `position_control` 等机器字段。 +共享的 `strategy_plugin_*` 通知标签由 +`quant_platform_kit.common.notification_localization.STRATEGY_PLUGIN_I18N` +维护;broker 平台可以通过 `merge_strategy_plugin_i18n()` 与本地文案合并, +从而保持各运行时的插件告警文案一致。 共享契约不支持 `paper`、`advisory` 和 `live` 插件模式。平台不应从这条 sidecar 路径维护插件账本或执行插件驱动的 allocation 变更。 diff --git a/src/quant_platform_kit/common/__init__.py b/src/quant_platform_kit/common/__init__.py index f17bad6..50352e2 100644 --- a/src/quant_platform_kit/common/__init__.py +++ b/src/quant_platform_kit/common/__init__.py @@ -2,7 +2,9 @@ from .notification_localization import ( COMMON_ZH_NOTIFICATION_REPLACEMENTS, + STRATEGY_PLUGIN_I18N, localize_notification_text, + merge_strategy_plugin_i18n, translator_uses_zh, ) from .execution_outcomes import ( @@ -113,6 +115,7 @@ "STRATEGY_PLUGIN_ALERT_CHANNEL_SMS", "STRATEGY_PLUGIN_ALERT_CHANNEL_TELEGRAM", "STRATEGY_PLUGIN_ALERT_ACTIONS", + "STRATEGY_PLUGIN_I18N", "STRATEGY_PLUGIN_NON_ALERT_ROUTES", "STRATEGY_PLUGIN_NOTIFICATION_TARGETS", "STRATEGY_PLUGIN_SCHEMA_VERSIONS", @@ -124,6 +127,7 @@ "is_terminal_funding_block", "is_terminal_strategy_run_stage", "localize_notification_text", + "merge_strategy_plugin_i18n", "normalize_skip_reason", "normalize_stage", "resolve_strategy_run_stage", diff --git a/src/quant_platform_kit/common/notification_localization.py b/src/quant_platform_kit/common/notification_localization.py index 3f34194..0c5a243 100644 --- a/src/quant_platform_kit/common/notification_localization.py +++ b/src/quant_platform_kit/common/notification_localization.py @@ -24,6 +24,109 @@ "market_data": ("市场数据", "market data"), } +STRATEGY_PLUGIN_I18N: dict[str, dict[str, str]] = { + "zh": { + "strategy_plugin_line": "🧩 插件:{plugin} | 状态:{route} | 提醒:{action}", + "strategy_plugin_alert_subject": "🚨 策略插件告警:{plugin} | {route}", + "strategy_plugin_alert_title": "🚨 【策略插件告警】", + "strategy_plugin_alert_context": "运行环境:{context}", + "strategy_plugin_alert_strategy": "策略:{strategy}", + "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_guidance": "处置建议:{guidance}", + "strategy_plugin_alert_scope_note": "执行范围:{scope_note}", + "strategy_plugin_alert_scope": "仅作人工复核提醒;插件不会自动下单或改仓位", + "strategy_plugin_name_crisis_response_shadow": "危机观察通知", + "strategy_plugin_name_macro_risk_governor": "宏观风险控制通知", + "strategy_plugin_name_market_regime_control": "市场状态控制通知", + "strategy_plugin_name_panic_reversal_shadow": "恐慌反转观察通知", + "strategy_plugin_name_taco_rebound_shadow": "TACO 反弹观察通知", + "strategy_plugin_mode_shadow": "影子观察", + "strategy_plugin_route_blocked": "已阻断", + "strategy_plugin_route_crisis": "危机", + "strategy_plugin_route_delever": "降杠杆", + "strategy_plugin_route_no_action": "未触发", + "strategy_plugin_route_opportunity_watch": "机会观察", + "strategy_plugin_route_panic_reversal": "恐慌反转", + "strategy_plugin_route_risk_off": "风险关闭", + "strategy_plugin_route_risk_reduced": "风险降低", + "strategy_plugin_route_true_crisis": "真危机", + "strategy_plugin_route_taco_rebound": "TACO 反弹确认", + "strategy_plugin_route_unknown_route": "未知状态", + "strategy_plugin_route_watch": "观察", + "strategy_plugin_action_no_action": "不操作", + "strategy_plugin_action_watch_only": "仅通知", + "strategy_plugin_action_notify_manual_review": "通知人工复核", + "strategy_plugin_action_defend": "防守", + "strategy_plugin_action_delever": "降杠杆", + "strategy_plugin_action_blocked": "已阻断", + "strategy_plugin_action_monitor": "持续观察", + "strategy_plugin_action_unknown_action": "未知提醒", + "strategy_plugin_guidance_crisis_response_shadow_true_crisis_defend": "优先考虑降低杠杆或清理杠杆仓位,暂停加仓;如需保留风险敞口,先降到可承受的小仓位。", + "strategy_plugin_guidance_crisis_response_shadow_no_action_blocked": "危机路线被风控阻断;先核对数据新鲜度和外部情境,不建议仅凭此条加仓。", + "strategy_plugin_guidance_macro_risk_governor_delever_delever": "宏观风险控制建议降低杠杆敞口;是否执行由策略侧可回测规则和仓位适配器决定。", + "strategy_plugin_guidance_macro_risk_governor_crisis_defend": "宏观危机信号建议风险仓位转向防守或现金类资产,直到压力缓和。", + "strategy_plugin_guidance_market_regime_control_risk_off_defend": "市场状态控制进入风险关闭;机会类信号先不执行,风险仓位应保持防守。", + "strategy_plugin_guidance_market_regime_control_risk_reduced_delever": "市场状态控制建议降杠杆;自动仓位调整只按策略侧已批准的可回测规则执行。", + "strategy_plugin_guidance_market_regime_control_opportunity_watch_notify_manual_review": "仅作人工复核:市场状态允许有限机会观察,但插件本身不会下单或直接改仓位。", + "strategy_plugin_guidance_market_regime_control_blocked_blocked": "市场状态控制被数据质量或新鲜度保护阻断;先核对数据源和产物,再决定是否人工处理。", + "strategy_plugin_guidance_taco_rebound_shadow_taco_rebound_notify_manual_review": "TACO 仅提示可能的反弹窗口;可考虑小仓位、分批、预设止损/失效条件的人工博弈,不建议一次性满仓。", + }, + "en": { + "strategy_plugin_line": "🧩 Plugin: {plugin} | status: {route} | notice: {action}", + "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_strategy": "Strategy: {strategy}", + "strategy_plugin_alert_plugin": "Plugin: {plugin}", + "strategy_plugin_alert_status": "Status: {route}", + "strategy_plugin_alert_action": "Notice: {action}", + "strategy_plugin_alert_mode": "Mode: {mode}", + "strategy_plugin_alert_as_of": "Signal as-of: {as_of}", + "strategy_plugin_alert_guidance": "Manual guidance: {guidance}", + "strategy_plugin_alert_scope_note": "Execution scope: {scope_note}", + "strategy_plugin_alert_scope": "Manual review notice only; the plugin does not place orders or change allocations", + "strategy_plugin_name_crisis_response_shadow": "Crisis Watch Notice", + "strategy_plugin_name_macro_risk_governor": "Macro Risk Governor Notice", + "strategy_plugin_name_market_regime_control": "Market Regime Control Notice", + "strategy_plugin_name_panic_reversal_shadow": "Panic Reversal Watch Notice", + "strategy_plugin_name_taco_rebound_shadow": "TACO Rebound Watch Notice", + "strategy_plugin_mode_shadow": "shadow", + "strategy_plugin_route_blocked": "blocked", + "strategy_plugin_route_crisis": "crisis", + "strategy_plugin_route_delever": "de-lever", + "strategy_plugin_route_no_action": "no alert", + "strategy_plugin_route_opportunity_watch": "opportunity watch", + "strategy_plugin_route_panic_reversal": "panic reversal", + "strategy_plugin_route_risk_off": "risk off", + "strategy_plugin_route_risk_reduced": "risk reduced", + "strategy_plugin_route_true_crisis": "true crisis", + "strategy_plugin_route_taco_rebound": "TACO rebound confirmed", + "strategy_plugin_route_unknown_route": "unknown status", + "strategy_plugin_route_watch": "watch", + "strategy_plugin_action_no_action": "no action", + "strategy_plugin_action_watch_only": "notify only", + "strategy_plugin_action_notify_manual_review": "notify manual review", + "strategy_plugin_action_defend": "defend", + "strategy_plugin_action_delever": "de-lever", + "strategy_plugin_action_blocked": "blocked", + "strategy_plugin_action_monitor": "watch", + "strategy_plugin_action_unknown_action": "unknown notice", + "strategy_plugin_guidance_crisis_response_shadow_true_crisis_defend": "Consider reducing or clearing leveraged exposure, then pause new risk additions; if keeping exposure, resize it to a small amount you can tolerate.", + "strategy_plugin_guidance_crisis_response_shadow_no_action_blocked": "A guard blocked the crisis route; verify data freshness and external context before acting on this alert.", + "strategy_plugin_guidance_macro_risk_governor_delever_delever": "The macro risk governor suggests reducing leveraged exposure; execution is controlled by strategy-side backtestable rules and position adapters.", + "strategy_plugin_guidance_macro_risk_governor_crisis_defend": "The macro crisis signal suggests moving the risk sleeve toward defensive or cash-like exposure until stress de-escalates.", + "strategy_plugin_guidance_market_regime_control_risk_off_defend": "Market regime control is risk-off; opportunity signals should stay blocked and risk exposure should remain defensive.", + "strategy_plugin_guidance_market_regime_control_risk_reduced_delever": "Market regime control suggests de-levering; automatic position changes only follow strategy-side approved, backtestable rules.", + "strategy_plugin_guidance_market_regime_control_opportunity_watch_notify_manual_review": "Manual review only: the market regime allows bounded opportunity watch, but the plugin does not place orders or directly change allocations.", + "strategy_plugin_guidance_market_regime_control_blocked_blocked": "Market regime control was blocked by data-quality or freshness guards; verify source data and artifacts before manual action.", + "strategy_plugin_guidance_taco_rebound_shadow_taco_rebound_notify_manual_review": "TACO only flags a possible rebound window; consider a small staged manual probe with a predefined invalidation level instead of full-size exposure.", + }, +} + COMMON_ZH_NOTIFICATION_REPLACEMENTS: tuple[NotificationReplacement, ...] = ( ("feature snapshot guard blocked execution", "特征快照校验阻止执行"), @@ -97,6 +200,21 @@ ) +def merge_strategy_plugin_i18n( + i18n: dict[str, dict[str, str]], + *, + shared_wins: bool = True, +) -> dict[str, dict[str, str]]: + merged = {str(locale): dict(values) for locale, values in i18n.items()} + for locale, shared_values in STRATEGY_PLUGIN_I18N.items(): + existing = merged.setdefault(locale, {}) + if shared_wins: + existing.update(shared_values) + else: + merged[locale] = {**shared_values, **existing} + return merged + + def translator_uses_zh(translator: NotificationTranslator) -> bool: sample = str(translator("no_trades")) return any("\u4e00" <= ch <= "\u9fff" for ch in sample) diff --git a/tests/test_notification_localization.py b/tests/test_notification_localization.py index 91e721d..328d2c4 100644 --- a/tests/test_notification_localization.py +++ b/tests/test_notification_localization.py @@ -4,8 +4,10 @@ from quant_platform_kit.common.notification_localization import ( COMMON_ZH_NOTIFICATION_REPLACEMENTS, + STRATEGY_PLUGIN_I18N, localize_price_source_label, localize_notification_text, + merge_strategy_plugin_i18n, translator_uses_zh, ) @@ -73,6 +75,26 @@ def test_localize_price_source_label_supports_broker_sources(self): "LongBridge daily candlesticks", ) + def test_strategy_plugin_i18n_has_matching_locale_keys(self): + self.assertEqual(set(STRATEGY_PLUGIN_I18N["zh"]), set(STRATEGY_PLUGIN_I18N["en"])) + self.assertIn("strategy_plugin_name_taco_rebound_shadow", STRATEGY_PLUGIN_I18N["zh"]) + self.assertEqual(STRATEGY_PLUGIN_I18N["zh"]["strategy_plugin_name_taco_rebound_shadow"], "TACO 反弹观察通知") + + def test_merge_strategy_plugin_i18n_fills_and_overrides_shared_keys(self): + merged = merge_strategy_plugin_i18n( + { + "zh": { + "no_trades": "无需调仓", + "strategy_plugin_name_taco_rebound_shadow": "TACO 旧观察通知", + }, + "en": {"no_trades": "No trades"}, + } + ) + + self.assertEqual(merged["zh"]["no_trades"], "无需调仓") + self.assertEqual(merged["zh"]["strategy_plugin_name_taco_rebound_shadow"], "TACO 反弹观察通知") + self.assertEqual(merged["en"]["strategy_plugin_route_watch"], "watch") + if __name__ == "__main__": unittest.main()