Skip to content

Commit ab63117

Browse files
committed
Version strategy plugin contracts
1 parent e4aa520 commit ab63117

4 files changed

Lines changed: 193 additions & 25 deletions

File tree

docs/strategy_plugin_runtime_contract.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,21 @@ The shared kit owns plugin compatibility through a registry-style
6464
a plugin supports; they should call the shared parser/loader and let it reject
6565
unsupported mounts or artifacts.
6666

67-
The default registry currently defines:
68-
69-
| Plugin | Supported strategies | Supported mode | Escalated alert channel |
70-
| --- | --- | --- | --- |
71-
| `crisis_response_shadow` | `tqqq_growth_income`, `soxl_soxx_trend_income` | `shadow` | `email`, `sms`, `push`, `telegram` |
72-
| `taco_rebound_shadow` | `tqqq_growth_income` | `shadow` | `email`, `sms`, `push`, `telegram` |
73-
74-
`taco_rebound_shadow` is notification-only. Its artifact may escalate a
75-
manual-review alert when a TACO-style rebound context is active, but it must not
76-
recommend position size, mutate live allocation, or imply broker order
77-
permission.
67+
The default registry currently defines versioned plugin contracts:
68+
69+
| Plugin | Schema versions | Supported strategies | Status | Supported mode | Escalated alert channel |
70+
| --- | --- | --- | --- | --- | --- |
71+
| `market_regime_control` | `market_regime_control.v1` | `tqqq_growth_income`, `soxl_soxx_trend_income` | default | `shadow` | `email`, `sms`, `push`, `telegram` |
72+
| `crisis_response_shadow` | `crisis_response_shadow.v1` | `tqqq_growth_income`, `soxl_soxx_trend_income` | deprecated; successor `market_regime_control` | `shadow` | `email`, `sms`, `push`, `telegram` |
73+
| `macro_risk_governor` | `macro_risk_governor.v1` | `tqqq_growth_income` | deprecated; successor `market_regime_control` | `shadow` | `email`, `sms`, `push`, `telegram` |
74+
| `taco_rebound_shadow` | `taco_rebound_shadow.v2` | `tqqq_growth_income` | deprecated; successor `market_regime_control` | `shadow` | `email`, `sms`, `push`, `telegram` |
75+
76+
Deprecated plugins remain loadable for historical backtests and staged rollout.
77+
New strategy integrations should mount `market_regime_control` and read the
78+
artifact's `notification` and `position_control` sections. The old TACO artifact
79+
is notification-only. It may escalate a manual-review alert when a TACO-style
80+
rebound context is active, but it must not recommend position size, mutate live
81+
allocation, or imply broker order permission.
7882

7983
To expand a plugin later, update the shared definition or pass an explicit
8084
definition registry into the parser/loader. This keeps future plugin eligibility

src/quant_platform_kit/common/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
STRATEGY_PLUGIN_ALERT_CHANNEL_TELEGRAM,
5353
STRATEGY_PLUGIN_ALERT_ACTIONS,
5454
STRATEGY_PLUGIN_NON_ALERT_ROUTES,
55+
STRATEGY_PLUGIN_SCHEMA_VERSIONS,
5556
SUPPORTED_STRATEGY_PLUGIN_MODES,
5657
MACRO_RISK_GOVERNOR_SUPPORTED_STRATEGIES,
5758
MARKET_REGIME_CONTROL_SUPPORTED_STRATEGIES,
@@ -74,6 +75,7 @@
7475
should_alert_strategy_plugin_signal,
7576
translate_strategy_plugin_value,
7677
validate_strategy_plugin_compatibility,
78+
validate_strategy_plugin_schema_version,
7779
validate_strategy_plugin_signal_payload,
7880
)
7981

@@ -104,6 +106,7 @@
104106
"STRATEGY_PLUGIN_ALERT_CHANNEL_TELEGRAM",
105107
"STRATEGY_PLUGIN_ALERT_ACTIONS",
106108
"STRATEGY_PLUGIN_NON_ALERT_ROUTES",
109+
"STRATEGY_PLUGIN_SCHEMA_VERSIONS",
107110
"SUPPORTED_STRATEGY_PLUGIN_MODES",
108111
"MACRO_RISK_GOVERNOR_SUPPORTED_STRATEGIES",
109112
"MARKET_REGIME_CONTROL_SUPPORTED_STRATEGIES",
@@ -144,5 +147,6 @@
144147
"translate_strategy_plugin_value",
145148
"translator_uses_zh",
146149
"validate_strategy_plugin_compatibility",
150+
"validate_strategy_plugin_schema_version",
147151
"validate_strategy_plugin_signal_payload",
148152
]

src/quant_platform_kit/common/strategy_plugins.py

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@
3232
)
3333
TACO_REBOUND_SHADOW_SUPPORTED_STRATEGIES = frozenset({"tqqq_growth_income"})
3434
MACRO_RISK_GOVERNOR_SUPPORTED_STRATEGIES = frozenset({"tqqq_growth_income"})
35-
MARKET_REGIME_CONTROL_SUPPORTED_STRATEGIES = frozenset({"tqqq_growth_income"})
35+
MARKET_REGIME_CONTROL_SUPPORTED_STRATEGIES = frozenset({"tqqq_growth_income", "soxl_soxx_trend_income"})
36+
STRATEGY_PLUGIN_SCHEMA_VERSIONS: Mapping[str, frozenset[str]] = {
37+
PLUGIN_CRISIS_RESPONSE_SHADOW: frozenset({"crisis_response_shadow.v1"}),
38+
PLUGIN_MARKET_REGIME_CONTROL: frozenset({"market_regime_control.v1"}),
39+
PLUGIN_MACRO_RISK_GOVERNOR: frozenset({"macro_risk_governor.v1"}),
40+
PLUGIN_TACO_REBOUND_SHADOW: frozenset({"taco_rebound_shadow.v2"}),
41+
}
3642
_DEFAULT_STRATEGY_PLUGIN_ALERT_GUIDANCE: Mapping[tuple[str, str, str], str] = {
3743
(
3844
PLUGIN_CRISIS_RESPONSE_SHADOW,
@@ -117,6 +123,10 @@ class StrategyPluginDefinition:
117123
plugin: str
118124
supported_strategies: frozenset[str] | None = None
119125
supported_modes: frozenset[str] = field(default_factory=lambda: SUPPORTED_STRATEGY_PLUGIN_MODES)
126+
supported_schema_versions: frozenset[str] = field(default_factory=frozenset)
127+
default_schema_version: str | None = None
128+
deprecated: bool = False
129+
successor_plugin: str | None = None
120130
alert_channels: tuple[str, ...] = ()
121131

122132
def normalized(self) -> "StrategyPluginDefinition":
@@ -132,6 +142,16 @@ def normalized(self) -> "StrategyPluginDefinition":
132142
)
133143
if not supported_modes:
134144
raise ValueError(f"strategy plugin definition for {plugin} must include at least one supported mode")
145+
supported_schema_versions = frozenset(
146+
_required_string(version, field_name="supported_schema_version")
147+
for version in self.supported_schema_versions
148+
)
149+
default_schema_version = _optional_string(self.default_schema_version)
150+
if default_schema_version is not None and supported_schema_versions and default_schema_version not in supported_schema_versions:
151+
raise ValueError(
152+
f"strategy plugin definition for {plugin} default_schema_version must be listed in supported_schema_versions"
153+
)
154+
successor_plugin = _optional_string(self.successor_plugin)
135155
alert_channels = tuple(
136156
_required_string(channel, field_name="alert_channel")
137157
for channel in self.alert_channels
@@ -140,6 +160,10 @@ def normalized(self) -> "StrategyPluginDefinition":
140160
plugin=plugin,
141161
supported_strategies=supported_strategies,
142162
supported_modes=supported_modes,
163+
supported_schema_versions=supported_schema_versions,
164+
default_schema_version=default_schema_version,
165+
deprecated=bool(self.deprecated),
166+
successor_plugin=successor_plugin,
143167
alert_channels=alert_channels,
144168
)
145169

@@ -154,6 +178,10 @@ def supports_strategy(self, strategy: str) -> bool:
154178
plugin=PLUGIN_CRISIS_RESPONSE_SHADOW,
155179
supported_strategies=CRISIS_RESPONSE_SHADOW_SUPPORTED_STRATEGIES,
156180
supported_modes=SUPPORTED_STRATEGY_PLUGIN_MODES,
181+
supported_schema_versions=STRATEGY_PLUGIN_SCHEMA_VERSIONS[PLUGIN_CRISIS_RESPONSE_SHADOW],
182+
default_schema_version="crisis_response_shadow.v1",
183+
deprecated=True,
184+
successor_plugin=PLUGIN_MARKET_REGIME_CONTROL,
157185
alert_channels=(
158186
STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL,
159187
STRATEGY_PLUGIN_ALERT_CHANNEL_SMS,
@@ -165,6 +193,10 @@ def supports_strategy(self, strategy: str) -> bool:
165193
plugin=PLUGIN_TACO_REBOUND_SHADOW,
166194
supported_strategies=TACO_REBOUND_SHADOW_SUPPORTED_STRATEGIES,
167195
supported_modes=SUPPORTED_STRATEGY_PLUGIN_MODES,
196+
supported_schema_versions=STRATEGY_PLUGIN_SCHEMA_VERSIONS[PLUGIN_TACO_REBOUND_SHADOW],
197+
default_schema_version="taco_rebound_shadow.v2",
198+
deprecated=True,
199+
successor_plugin=PLUGIN_MARKET_REGIME_CONTROL,
168200
alert_channels=(
169201
STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL,
170202
STRATEGY_PLUGIN_ALERT_CHANNEL_SMS,
@@ -176,6 +208,8 @@ def supports_strategy(self, strategy: str) -> bool:
176208
plugin=PLUGIN_MARKET_REGIME_CONTROL,
177209
supported_strategies=MARKET_REGIME_CONTROL_SUPPORTED_STRATEGIES,
178210
supported_modes=SUPPORTED_STRATEGY_PLUGIN_MODES,
211+
supported_schema_versions=STRATEGY_PLUGIN_SCHEMA_VERSIONS[PLUGIN_MARKET_REGIME_CONTROL],
212+
default_schema_version="market_regime_control.v1",
179213
alert_channels=(
180214
STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL,
181215
STRATEGY_PLUGIN_ALERT_CHANNEL_SMS,
@@ -187,6 +221,10 @@ def supports_strategy(self, strategy: str) -> bool:
187221
plugin=PLUGIN_MACRO_RISK_GOVERNOR,
188222
supported_strategies=MACRO_RISK_GOVERNOR_SUPPORTED_STRATEGIES,
189223
supported_modes=SUPPORTED_STRATEGY_PLUGIN_MODES,
224+
supported_schema_versions=STRATEGY_PLUGIN_SCHEMA_VERSIONS[PLUGIN_MACRO_RISK_GOVERNOR],
225+
default_schema_version="macro_risk_governor.v1",
226+
deprecated=True,
227+
successor_plugin=PLUGIN_MARKET_REGIME_CONTROL,
190228
alert_channels=(
191229
STRATEGY_PLUGIN_ALERT_CHANNEL_EMAIL,
192230
STRATEGY_PLUGIN_ALERT_CHANNEL_SMS,
@@ -204,6 +242,7 @@ class StrategyPluginMountConfig:
204242
signal_path: str
205243
enabled: bool = True
206244
expected_mode: str | None = None
245+
expected_schema_version: str | None = None
207246

208247

209248
@dataclass(frozen=True)
@@ -222,6 +261,9 @@ class StrategyPluginSignal:
222261
payload: Mapping[str, Any]
223262
source_uri: str | None = None
224263
local_path: str | None = None
264+
deprecated_plugin: bool = False
265+
successor_plugin: str | None = None
266+
supported_schema_versions: tuple[str, ...] = ()
225267

226268
def report_summary(self) -> dict[str, Any]:
227269
return {
@@ -231,6 +273,9 @@ def report_summary(self) -> dict[str, Any]:
231273
"configured_mode": self.configured_mode,
232274
"effective_mode": self.effective_mode,
233275
"schema_version": self.schema_version,
276+
"deprecated_plugin": self.deprecated_plugin,
277+
"successor_plugin": self.successor_plugin,
278+
"supported_schema_versions": self.supported_schema_versions,
234279
"as_of": self.as_of,
235280
"canonical_route": self.canonical_route,
236281
"suggested_action": self.suggested_action,
@@ -309,6 +354,34 @@ def validate_strategy_plugin_compatibility(
309354
)
310355

311356

357+
def validate_strategy_plugin_schema_version(
358+
*,
359+
plugin: str,
360+
schema_version: str | None,
361+
expected_schema_version: str | None = None,
362+
plugin_definitions: Mapping[str, StrategyPluginDefinition] | Sequence[StrategyPluginDefinition] | None = None,
363+
source: str = "artifact",
364+
) -> None:
365+
plugin_name = _required_string(plugin, field_name="plugin")
366+
normalized_schema_version = _optional_string(schema_version)
367+
normalized_expected_schema_version = _optional_string(expected_schema_version)
368+
if normalized_expected_schema_version is not None and normalized_schema_version != normalized_expected_schema_version:
369+
raise ValueError(
370+
"strategy plugin artifact schema_version mismatch: "
371+
f"expected {normalized_expected_schema_version}, got {normalized_schema_version or '<missing>'}"
372+
)
373+
definitions = normalize_strategy_plugin_definitions(plugin_definitions)
374+
definition = definitions.get(plugin_name)
375+
if definition is None or not definition.supported_schema_versions or normalized_schema_version is None:
376+
return
377+
if normalized_schema_version not in definition.supported_schema_versions:
378+
allowed = ", ".join(sorted(definition.supported_schema_versions))
379+
raise ValueError(
380+
f"strategy plugin {plugin_name} does not support schema_version {normalized_schema_version} "
381+
f"in {source}; supported schema versions: {allowed}"
382+
)
383+
384+
312385
def parse_strategy_plugin_mounts(
313386
raw_config: str | Sequence[Mapping[str, Any]] | Mapping[str, Any] | None,
314387
*,
@@ -351,6 +424,14 @@ def parse_strategy_plugin_mounts(
351424
if expected_mode is not None
352425
else None
353426
)
427+
expected_schema_version = _optional_string(item.get("expected_schema_version"))
428+
validate_strategy_plugin_schema_version(
429+
plugin=plugin,
430+
schema_version=expected_schema_version,
431+
expected_schema_version=expected_schema_version,
432+
plugin_definitions=definitions,
433+
source="mount",
434+
)
354435
validate_strategy_plugin_compatibility(
355436
strategy=strategy,
356437
plugin=plugin,
@@ -365,6 +446,7 @@ def parse_strategy_plugin_mounts(
365446
signal_path=signal_path,
366447
enabled=_as_bool(item.get("enabled"), default=True),
367448
expected_mode=normalized_expected_mode,
449+
expected_schema_version=expected_schema_version,
368450
)
369451
)
370452
return tuple(mounts)
@@ -398,6 +480,7 @@ def load_configured_strategy_plugin_signals(
398480
expected_strategy=mount.strategy,
399481
expected_plugin=mount.plugin,
400482
expected_mode=mount.expected_mode,
483+
expected_schema_version=mount.expected_schema_version,
401484
client_factory=client_factory,
402485
plugin_definitions=definitions,
403486
)
@@ -411,6 +494,7 @@ def load_strategy_plugin_signal(
411494
expected_strategy: str | None = None,
412495
expected_plugin: str | None = None,
413496
expected_mode: str | None = None,
497+
expected_schema_version: str | None = None,
414498
client_factory: Any = None,
415499
plugin_definitions: Mapping[str, StrategyPluginDefinition] | Sequence[StrategyPluginDefinition] | None = None,
416500
) -> StrategyPluginSignal:
@@ -425,6 +509,7 @@ def load_strategy_plugin_signal(
425509
expected_strategy=expected_strategy,
426510
expected_plugin=expected_plugin,
427511
expected_mode=expected_mode,
512+
expected_schema_version=expected_schema_version,
428513
source_uri=metadata.get("source_uri"),
429514
local_path=str(local_path),
430515
plugin_definitions=plugin_definitions,
@@ -437,6 +522,7 @@ def validate_strategy_plugin_signal_payload(
437522
expected_strategy: str | None = None,
438523
expected_plugin: str | None = None,
439524
expected_mode: str | None = None,
525+
expected_schema_version: str | None = None,
440526
source_uri: str | None = None,
441527
local_path: str | None = None,
442528
plugin_definitions: Mapping[str, StrategyPluginDefinition] | Sequence[StrategyPluginDefinition] | None = None,
@@ -473,6 +559,16 @@ def validate_strategy_plugin_signal_payload(
473559
plugin_definitions=plugin_definitions,
474560
source="artifact",
475561
)
562+
schema_version = _optional_string(payload.get("schema_version")) or ""
563+
definitions = normalize_strategy_plugin_definitions(plugin_definitions)
564+
definition = definitions.get(plugin)
565+
validate_strategy_plugin_schema_version(
566+
plugin=plugin,
567+
schema_version=schema_version,
568+
expected_schema_version=expected_schema_version,
569+
plugin_definitions=definitions,
570+
source="artifact",
571+
)
476572

477573
execution_controls = payload.get("execution_controls") or {}
478574
if not isinstance(execution_controls, Mapping):
@@ -484,7 +580,7 @@ def validate_strategy_plugin_signal_payload(
484580
mode=mode,
485581
configured_mode=configured_mode,
486582
effective_mode=effective_mode,
487-
schema_version=_optional_string(payload.get("schema_version")) or "",
583+
schema_version=schema_version,
488584
as_of=_optional_string(payload.get("as_of")) or "",
489585
canonical_route=_optional_string(payload.get("canonical_route")),
490586
suggested_action=_optional_string(payload.get("suggested_action")),
@@ -493,6 +589,9 @@ def validate_strategy_plugin_signal_payload(
493589
payload=dict(payload),
494590
source_uri=_optional_string(source_uri),
495591
local_path=_optional_string(local_path),
592+
deprecated_plugin=bool(definition.deprecated) if definition is not None else False,
593+
successor_plugin=definition.successor_plugin if definition is not None else None,
594+
supported_schema_versions=tuple(sorted(definition.supported_schema_versions)) if definition is not None else (),
496595
)
497596

498597

0 commit comments

Comments
 (0)