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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ 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 artifacts may carry display-only `strategy_plugin_messages.v1` and
`strategy_plugin_log.v1` localized notification/log text. Platform renderers can
use those strings, while strategy and platform logic should continue to depend
on machine fields such as `canonical_route`, `suggested_action`,
`reason_codes`, and `position_control`.

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
3 changes: 3 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ QuantPlatformKit

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

插件 artifact 可以携带展示层 `strategy_plugin_messages.v1` 和
`strategy_plugin_log.v1` 中英文通知 / 日志文案。平台 renderer 可以使用这些文案,但策略和平台逻辑应继续依赖 `canonical_route`、`suggested_action`、`reason_codes`、`position_control` 等机器字段。

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

## 目录结构
Expand Down
22 changes: 19 additions & 3 deletions docs/strategy_plugin_runtime_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ The default registry currently defines versioned plugin contracts:

| Plugin | Schema versions | Supported strategies | Status | Supported mode | Escalated alert channel |
| --- | --- | --- | --- | --- | --- |
| `market_regime_control` | `market_regime_control.v1` | `tqqq_growth_income`, `soxl_soxx_trend_income` | default | `shadow` | `email`, `sms`, `push`, `telegram` |
| `crisis_response_shadow` | `crisis_response_shadow.v1` | `tqqq_growth_income`, `soxl_soxx_trend_income` | deprecated; successor `market_regime_control` | `shadow` | `email`, `sms`, `push`, `telegram` |
| `market_regime_control` | `market_regime_control.v1` | `tqqq_growth_income`, `global_etf_rotation`, `russell_1000_multi_factor_defensive`, `tech_communication_pullback_enhancement`, `mega_cap_leader_rotation_top50_balanced` | default | `shadow` | `email`, `sms`, `push`, `telegram` |
| `crisis_response_shadow` | `crisis_response_shadow.v1` | `tqqq_growth_income` | deprecated; successor `market_regime_control` | `shadow` | `email`, `sms`, `push`, `telegram` |
| `macro_risk_governor` | `macro_risk_governor.v1` | `tqqq_growth_income` | deprecated; successor `market_regime_control` | `shadow` | `email`, `sms`, `push`, `telegram` |
| `taco_rebound_shadow` | `taco_rebound_shadow.v2` | `tqqq_growth_income` | deprecated; successor `market_regime_control` | `shadow` | `email`, `sms`, `push`, `telegram` |

Expand All @@ -84,6 +84,11 @@ To expand a plugin later, update the shared definition or pass an explicit
definition registry into the parser/loader. This keeps future plugin eligibility
changes out of platform runtime code.

SOXL/SOXX is intentionally not listed as a `market_regime_control` runtime
mount. Broad macro and crisis signals for SOXL should be delivered through a
general notification artifact and reviewed manually unless a future backtest
promotes an explicit strategy-level opt-in.

## Runtime Loader

Use `quant_platform_kit.common.strategy_plugins`:
Expand All @@ -103,7 +108,7 @@ signals = load_configured_strategy_plugin_signals(
strategy_profile=current_strategy_profile,
)
report_section = build_strategy_plugin_report_payload(signals)
notification_lines = build_strategy_plugin_notification_lines(signals)
notification_lines = build_strategy_plugin_notification_lines(signals, locale="zh-CN")
alert_messages = build_strategy_plugin_alert_messages(signals)
```

Expand All @@ -121,6 +126,17 @@ The loader validates:
For `shadow`, platform runtimes should only add logs, runtime report fields, and
notification context.

Artifacts may include display-only i18n fields:

- `localized_messages.schema_version = strategy_plugin_messages.v1`
- `localized_messages.notification.en-US` / `localized_messages.notification.zh-CN`
- `localized_messages.log.en-US` / `localized_messages.log.zh-CN`
- `log_record.schema_version = strategy_plugin_log.v1`

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

`paper`, `advisory`, and `live` plugin modes are not supported by the shared
contract. Platforms should not maintain plugin ledgers or execute plugin-driven
allocation changes from this sidecar path.
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.34"
version = "0.7.35"
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: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

setup(
name="quant-platform-kit",
version="0.7.34",
version="0.7.35",
description="Shared broker adapters, domain models, execution ports, and notification utilities for QuantStrategyLab strategies.",
package_dir={"": "src"},
packages=find_packages(where="src"),
Expand Down
2 changes: 1 addition & 1 deletion src/quant_platform_kit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
used by older strategy repositories.
"""

__version__ = "0.7.34"
__version__ = "0.7.35"

from .common.models import (
ExecutionReport,
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 @@ -67,6 +67,7 @@
build_strategy_plugin_alert_scope_note,
build_strategy_plugin_notification_lines,
build_strategy_plugin_report_payload,
extract_strategy_plugin_localized_message,
load_configured_strategy_plugin_signals,
load_strategy_plugin_signal,
normalize_strategy_plugin_definitions,
Expand Down Expand Up @@ -136,6 +137,7 @@
"build_strategy_plugin_alert_scope_note",
"build_strategy_plugin_notification_lines",
"build_strategy_plugin_report_payload",
"extract_strategy_plugin_localized_message",
"build_runtime_target",
"load_configured_strategy_plugin_signals",
"load_strategy_plugin_signal",
Expand Down
63 changes: 61 additions & 2 deletions src/quant_platform_kit/common/strategy_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,19 @@
CRISIS_RESPONSE_SHADOW_SUPPORTED_STRATEGIES = frozenset(
{
"tqqq_growth_income",
"soxl_soxx_trend_income",
}
)
TACO_REBOUND_SHADOW_SUPPORTED_STRATEGIES = frozenset({"tqqq_growth_income"})
MACRO_RISK_GOVERNOR_SUPPORTED_STRATEGIES = frozenset({"tqqq_growth_income"})
MARKET_REGIME_CONTROL_SUPPORTED_STRATEGIES = frozenset({"tqqq_growth_income", "soxl_soxx_trend_income"})
MARKET_REGIME_CONTROL_SUPPORTED_STRATEGIES = frozenset(
{
"tqqq_growth_income",
"global_etf_rotation",
"russell_1000_multi_factor_defensive",
"tech_communication_pullback_enhancement",
"mega_cap_leader_rotation_top50_balanced",
}
)
STRATEGY_PLUGIN_SCHEMA_VERSIONS: Mapping[str, frozenset[str]] = {
PLUGIN_CRISIS_RESPONSE_SHADOW: frozenset({"crisis_response_shadow.v1"}),
PLUGIN_MARKET_REGIME_CONTROL: frozenset({"market_regime_control.v1"}),
Expand Down Expand Up @@ -661,13 +668,65 @@ def translate_strategy_plugin_value(
return translated if translated != key else value


def extract_strategy_plugin_localized_message(
signal: StrategyPluginSignal,
*,
section: str,
locale: str,
) -> str | None:
payload = getattr(signal, "payload", {}) or {}
if not isinstance(payload, Mapping):
return None
normalized_section = str(section or "").strip()
normalized_locale = str(locale or "").strip()
if not normalized_section or not normalized_locale:
return None

localized_messages = payload.get("localized_messages")
if isinstance(localized_messages, Mapping):
section_messages = localized_messages.get(normalized_section)
if isinstance(section_messages, Mapping):
localized = _optional_string(section_messages.get(normalized_locale))
if localized:
return localized
default_locale = _optional_string(localized_messages.get("default_locale"))
if default_locale:
localized = _optional_string(section_messages.get(default_locale))
if localized:
return localized

if normalized_section == "notification":
notification = payload.get("notification")
if isinstance(notification, Mapping):
notification_messages = notification.get("localized_messages")
if isinstance(notification_messages, Mapping):
localized = _optional_string(notification_messages.get(normalized_locale))
if localized:
return localized
default_locale = _optional_string(notification.get("default_locale"))
if default_locale:
localized = _optional_string(notification_messages.get(default_locale))
if localized:
return localized
return None


def build_strategy_plugin_notification_lines(
signals: Sequence[StrategyPluginSignal],
*,
translator: Callable[..., str] | None = None,
locale: str | None = None,
) -> tuple[str, ...]:
lines: list[str] = []
for signal in signals:
localized_line = (
extract_strategy_plugin_localized_message(signal, section="notification", locale=locale)
if locale is not None
else None
)
if localized_line:
lines.append(localized_line)
continue
route = getattr(signal, "canonical_route", None) or "unknown_route"
action = getattr(signal, "suggested_action", None) or "unknown_action"
lines.append(
Expand Down
87 changes: 74 additions & 13 deletions tests/test_strategy_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
build_strategy_plugin_alert_messages,
build_strategy_plugin_notification_lines,
build_strategy_plugin_report_payload,
extract_strategy_plugin_localized_message,
load_configured_strategy_plugin_signals,
load_strategy_plugin_signal,
parse_strategy_plugin_mounts,
Expand Down Expand Up @@ -105,7 +106,7 @@ def test_parse_strategy_plugin_mounts_rejects_platform_mode_selection(self):
with self.assertRaisesRegex(ValueError, "must not set mode"):
parse_strategy_plugin_mounts(raw)

def test_default_plugin_definition_limits_crisis_response_to_supported_strategies(self):
def test_default_plugin_definition_limits_crisis_response_to_tqqq(self):
definition = DEFAULT_STRATEGY_PLUGIN_DEFINITIONS[PLUGIN_CRISIS_RESPONSE_SHADOW]

self.assertEqual(definition.supported_strategies, CRISIS_RESPONSE_SHADOW_SUPPORTED_STRATEGIES)
Expand All @@ -123,11 +124,15 @@ def test_default_plugin_definition_limits_crisis_response_to_supported_strategie
plugin=PLUGIN_CRISIS_RESPONSE_SHADOW,
mode=PLUGIN_MODE_SHADOW,
)
validate_strategy_plugin_compatibility(
strategy="soxl_soxx_trend_income",
plugin=PLUGIN_CRISIS_RESPONSE_SHADOW,
mode=PLUGIN_MODE_SHADOW,
)
with self.assertRaisesRegex(
ValueError,
"crisis_response_shadow does not support strategy soxl_soxx_trend_income",
):
validate_strategy_plugin_compatibility(
strategy="soxl_soxx_trend_income",
plugin=PLUGIN_CRISIS_RESPONSE_SHADOW,
mode=PLUGIN_MODE_SHADOW,
)

def test_default_plugin_definition_limits_taco_rebound_to_tqqq_notifications(self):
definition = DEFAULT_STRATEGY_PLUGIN_DEFINITIONS[PLUGIN_TACO_REBOUND_SHADOW]
Expand Down Expand Up @@ -185,7 +190,7 @@ def test_default_plugin_definition_limits_macro_risk_governor_to_tqqq(self):
mode=PLUGIN_MODE_SHADOW,
)

def test_default_plugin_definition_supports_market_regime_control_for_tqqq_and_soxl(self):
def test_default_plugin_definition_supports_market_regime_control_for_approved_strategies(self):
definition = DEFAULT_STRATEGY_PLUGIN_DEFINITIONS[PLUGIN_MARKET_REGIME_CONTROL]

self.assertEqual(definition.supported_strategies, MARKET_REGIME_CONTROL_SUPPORTED_STRATEGIES)
Expand All @@ -201,12 +206,27 @@ def test_default_plugin_definition_supports_market_regime_control_for_tqqq_and_s
STRATEGY_PLUGIN_ALERT_CHANNEL_TELEGRAM,
),
)
for strategy in ("tqqq_growth_income", "soxl_soxx_trend_income"):
for strategy in (
"tqqq_growth_income",
"global_etf_rotation",
"russell_1000_multi_factor_defensive",
"tech_communication_pullback_enhancement",
"mega_cap_leader_rotation_top50_balanced",
):
validate_strategy_plugin_compatibility(
strategy=strategy,
plugin=PLUGIN_MARKET_REGIME_CONTROL,
mode=PLUGIN_MODE_SHADOW,
)
with self.assertRaisesRegex(
ValueError,
"market_regime_control does not support strategy soxl_soxx_trend_income",
):
validate_strategy_plugin_compatibility(
strategy="soxl_soxx_trend_income",
plugin=PLUGIN_MARKET_REGIME_CONTROL,
mode=PLUGIN_MODE_SHADOW,
)

def test_parse_strategy_plugin_mounts_rejects_unsupported_crisis_response_strategy(self):
raw = [
Expand Down Expand Up @@ -267,18 +287,33 @@ def test_parse_strategy_plugin_mounts_accepts_market_regime_control_tqqq(self):
self.assertEqual(mounts[0].plugin, PLUGIN_MARKET_REGIME_CONTROL)
self.assertEqual(mounts[0].expected_schema_version, "market_regime_control.v1")

def test_parse_strategy_plugin_mounts_accepts_market_regime_control_soxl(self):
def test_parse_strategy_plugin_mounts_rejects_market_regime_control_soxl(self):
with self.assertRaisesRegex(
ValueError,
"market_regime_control does not support strategy soxl_soxx_trend_income",
):
parse_strategy_plugin_mounts(
[
{
"strategy": "soxl_soxx_trend_income",
"plugin": PLUGIN_MARKET_REGIME_CONTROL,
"signal_path": "gs://bucket/market_regime/latest_signal.json",
}
]
)

def test_parse_strategy_plugin_mounts_accepts_market_regime_control_weight_profile(self):
mounts = parse_strategy_plugin_mounts(
[
{
"strategy": "soxl_soxx_trend_income",
"strategy": "global_etf_rotation",
"plugin": PLUGIN_MARKET_REGIME_CONTROL,
"signal_path": "gs://bucket/market_regime/latest_signal.json",
}
]
)

self.assertEqual(mounts[0].strategy, "soxl_soxx_trend_income")
self.assertEqual(mounts[0].strategy, "global_etf_rotation")
self.assertEqual(mounts[0].plugin, PLUGIN_MARKET_REGIME_CONTROL)

def test_plugin_definition_marks_legacy_plugins_deprecated(self):
Expand Down Expand Up @@ -375,8 +410,8 @@ def test_load_configured_strategy_plugin_signals_filters_strategy_and_disabled_m
"signal_path": str(signal_path),
},
{
"strategy": "soxl_soxx_trend_income",
"plugin": "crisis_response_shadow",
"strategy": "global_etf_rotation",
"plugin": PLUGIN_MARKET_REGIME_CONTROL,
"signal_path": str(root / "missing.json"),
},
{
Expand Down Expand Up @@ -508,6 +543,32 @@ def test_strategy_plugin_notification_lines_use_translator_when_available(self):

self.assertEqual(lines, ("plugin=Crisis|mode=shadow|route=no action|action=watch only",))

def test_strategy_plugin_notification_lines_can_use_artifact_localized_message(self):
signal = validate_strategy_plugin_signal_payload(
{
**_signal_payload(plugin=PLUGIN_MARKET_REGIME_CONTROL),
"canonical_route": "risk_reduced",
"suggested_action": "delever",
"localized_messages": {
"schema_version": "strategy_plugin_messages.v1",
"default_locale": "en-US",
"notification": {
"en-US": "Notification required: risk reduced.",
"zh-CN": "需要通知:市场状态风险降低。",
},
},
}
)

self.assertEqual(
extract_strategy_plugin_localized_message(signal, section="notification", locale="zh-CN"),
"需要通知:市场状态风险降低。",
)
self.assertEqual(
build_strategy_plugin_notification_lines([signal], locale="zh-CN"),
("需要通知:市场状态风险降低。",),
)

def test_strategy_plugin_no_action_signal_does_not_escalate_alert(self):
signal = validate_strategy_plugin_signal_payload(_signal_payload())

Expand Down