1010from pathlib import Path
1111from typing import Any , Callable
1212
13+ PLUGIN_CRISIS_RESPONSE_SHADOW = "crisis_response_shadow"
1314PLUGIN_MODE_SHADOW = "shadow"
15+ STRATEGY_PLUGIN_ALERT_CHANNEL_GOOGLE_VOICE = "google_voice"
1416SUPPORTED_STRATEGY_PLUGIN_MODES = frozenset ({PLUGIN_MODE_SHADOW })
1517DEFAULT_PLUGIN_ARTIFACT_CACHE_DIR = Path (tempfile .gettempdir ()) / "quant_strategy_plugin_artifacts"
1618STRATEGY_PLUGIN_NON_ALERT_ROUTES = frozenset ({"no_action" })
1719STRATEGY_PLUGIN_ALERT_ACTIONS = frozenset ({"defend" , "blocked" })
20+ CRISIS_RESPONSE_SHADOW_SUPPORTED_STRATEGIES = frozenset (
21+ {
22+ "tqqq_growth_income" ,
23+ "soxl_soxx_trend_income" ,
24+ }
25+ )
26+
27+
28+ @dataclass (frozen = True )
29+ class StrategyPluginDefinition :
30+ plugin : str
31+ supported_strategies : frozenset [str ] | None = None
32+ supported_modes : frozenset [str ] = field (default_factory = lambda : SUPPORTED_STRATEGY_PLUGIN_MODES )
33+ alert_channels : tuple [str , ...] = ()
34+
35+ def normalized (self ) -> "StrategyPluginDefinition" :
36+ plugin = _required_string (self .plugin , field_name = "plugin" )
37+ supported_strategies = (
38+ frozenset (_required_string (strategy , field_name = "supported_strategy" ) for strategy in self .supported_strategies )
39+ if self .supported_strategies is not None
40+ else None
41+ )
42+ supported_modes = frozenset (
43+ normalize_strategy_plugin_mode (mode , field_name = "supported_mode" )
44+ for mode in self .supported_modes
45+ )
46+ if not supported_modes :
47+ raise ValueError (f"strategy plugin definition for { plugin } must include at least one supported mode" )
48+ alert_channels = tuple (
49+ _required_string (channel , field_name = "alert_channel" )
50+ for channel in self .alert_channels
51+ )
52+ return StrategyPluginDefinition (
53+ plugin = plugin ,
54+ supported_strategies = supported_strategies ,
55+ supported_modes = supported_modes ,
56+ alert_channels = alert_channels ,
57+ )
58+
59+ def supports_strategy (self , strategy : str ) -> bool :
60+ if self .supported_strategies is None :
61+ return True
62+ return strategy in self .supported_strategies
63+
64+
65+ DEFAULT_STRATEGY_PLUGIN_DEFINITIONS : Mapping [str , StrategyPluginDefinition ] = {
66+ PLUGIN_CRISIS_RESPONSE_SHADOW : StrategyPluginDefinition (
67+ plugin = PLUGIN_CRISIS_RESPONSE_SHADOW ,
68+ supported_strategies = CRISIS_RESPONSE_SHADOW_SUPPORTED_STRATEGIES ,
69+ supported_modes = SUPPORTED_STRATEGY_PLUGIN_MODES ,
70+ alert_channels = (STRATEGY_PLUGIN_ALERT_CHANNEL_GOOGLE_VOICE ,),
71+ )
72+ }
1873
1974
2075@dataclass (frozen = True )
@@ -77,11 +132,66 @@ def normalize_strategy_plugin_mode(value: Any, *, field_name: str = "mode") -> s
77132 return mode
78133
79134
135+ def normalize_strategy_plugin_definitions (
136+ plugin_definitions : Mapping [str , StrategyPluginDefinition ] | Sequence [StrategyPluginDefinition ] | None = None ,
137+ ) -> Mapping [str , StrategyPluginDefinition ]:
138+ raw_definitions = (
139+ DEFAULT_STRATEGY_PLUGIN_DEFINITIONS .values ()
140+ if plugin_definitions is None
141+ else plugin_definitions .values ()
142+ if isinstance (plugin_definitions , Mapping )
143+ else plugin_definitions
144+ )
145+ definitions : dict [str , StrategyPluginDefinition ] = {}
146+ for definition in raw_definitions :
147+ if not isinstance (definition , StrategyPluginDefinition ):
148+ raise TypeError ("strategy plugin definitions must be StrategyPluginDefinition objects" )
149+ normalized = definition .normalized ()
150+ if normalized .plugin in definitions :
151+ raise ValueError (f"duplicate strategy plugin definition: plugin={ normalized .plugin } " )
152+ definitions [normalized .plugin ] = normalized
153+ return definitions
154+
155+
156+ def validate_strategy_plugin_compatibility (
157+ * ,
158+ strategy : str ,
159+ plugin : str ,
160+ mode : str | None = None ,
161+ plugin_definitions : Mapping [str , StrategyPluginDefinition ] | Sequence [StrategyPluginDefinition ] | None = None ,
162+ source : str = "plugin" ,
163+ ) -> None :
164+ strategy_name = _required_string (strategy , field_name = "strategy" )
165+ plugin_name = _required_string (plugin , field_name = "plugin" )
166+ definitions = normalize_strategy_plugin_definitions (plugin_definitions )
167+ definition = definitions .get (plugin_name )
168+ if definition is None :
169+ return
170+ if not definition .supports_strategy (strategy_name ):
171+ allowed = ", " .join (sorted (definition .supported_strategies or ())) or "any"
172+ raise ValueError (
173+ f"strategy plugin { plugin_name } does not support strategy { strategy_name } "
174+ f"in { source } ; supported strategies: { allowed } "
175+ )
176+ if mode is None :
177+ return
178+ mode_name = normalize_strategy_plugin_mode (mode , field_name = "mode" )
179+ if mode_name not in definition .supported_modes :
180+ allowed_modes = ", " .join (sorted (definition .supported_modes ))
181+ raise ValueError (
182+ f"strategy plugin { plugin_name } does not support mode { mode_name } "
183+ f"in { source } ; supported modes: { allowed_modes } "
184+ )
185+
186+
80187def parse_strategy_plugin_mounts (
81188 raw_config : str | Sequence [Mapping [str , Any ]] | Mapping [str , Any ] | None ,
189+ * ,
190+ plugin_definitions : Mapping [str , StrategyPluginDefinition ] | Sequence [StrategyPluginDefinition ] | None = None ,
82191) -> tuple [StrategyPluginMountConfig , ...]:
83192 if raw_config is None or raw_config == "" :
84193 return ()
194+ definitions = normalize_strategy_plugin_definitions (plugin_definitions )
85195 payload : Any
86196 if isinstance (raw_config , str ):
87197 payload = json .loads (raw_config )
@@ -111,17 +221,25 @@ def parse_strategy_plugin_mounts(
111221 raise ValueError (f"duplicate strategy plugin mount: strategy={ strategy } plugin={ plugin } " )
112222 seen .add (key )
113223 expected_mode = item .get ("expected_mode" )
224+ normalized_expected_mode = (
225+ normalize_strategy_plugin_mode (expected_mode , field_name = "expected_mode" )
226+ if expected_mode is not None
227+ else None
228+ )
229+ validate_strategy_plugin_compatibility (
230+ strategy = strategy ,
231+ plugin = plugin ,
232+ mode = normalized_expected_mode ,
233+ plugin_definitions = definitions ,
234+ source = "mount" ,
235+ )
114236 mounts .append (
115237 StrategyPluginMountConfig (
116238 strategy = strategy ,
117239 plugin = plugin ,
118240 signal_path = signal_path ,
119241 enabled = _as_bool (item .get ("enabled" ), default = True ),
120- expected_mode = (
121- normalize_strategy_plugin_mode (expected_mode , field_name = "expected_mode" )
122- if expected_mode is not None
123- else None
124- ),
242+ expected_mode = normalized_expected_mode ,
125243 )
126244 )
127245 return tuple (mounts )
@@ -132,21 +250,31 @@ def load_configured_strategy_plugin_signals(
132250 * ,
133251 strategy_profile : str | None = None ,
134252 client_factory : Any = None ,
253+ plugin_definitions : Mapping [str , StrategyPluginDefinition ] | Sequence [StrategyPluginDefinition ] | None = None ,
135254) -> tuple [StrategyPluginSignal , ...]:
136255 selected_strategy = _optional_string (strategy_profile )
256+ definitions = normalize_strategy_plugin_definitions (plugin_definitions )
137257 signals : list [StrategyPluginSignal ] = []
138258 for mount in mounts :
139259 if not mount .enabled :
140260 continue
141261 if selected_strategy is not None and mount .strategy != selected_strategy :
142262 continue
263+ validate_strategy_plugin_compatibility (
264+ strategy = mount .strategy ,
265+ plugin = mount .plugin ,
266+ mode = mount .expected_mode ,
267+ plugin_definitions = definitions ,
268+ source = "mount" ,
269+ )
143270 signals .append (
144271 load_strategy_plugin_signal (
145272 mount .signal_path ,
146273 expected_strategy = mount .strategy ,
147274 expected_plugin = mount .plugin ,
148275 expected_mode = mount .expected_mode ,
149276 client_factory = client_factory ,
277+ plugin_definitions = definitions ,
150278 )
151279 )
152280 return tuple (signals )
@@ -159,6 +287,7 @@ def load_strategy_plugin_signal(
159287 expected_plugin : str | None = None ,
160288 expected_mode : str | None = None ,
161289 client_factory : Any = None ,
290+ plugin_definitions : Mapping [str , StrategyPluginDefinition ] | Sequence [StrategyPluginDefinition ] | None = None ,
162291) -> StrategyPluginSignal :
163292 local_path , metadata = _materialize_artifact_path (reference , client_factory = client_factory )
164293 if not local_path .exists ():
@@ -173,6 +302,7 @@ def load_strategy_plugin_signal(
173302 expected_mode = expected_mode ,
174303 source_uri = metadata .get ("source_uri" ),
175304 local_path = str (local_path ),
305+ plugin_definitions = plugin_definitions ,
176306 )
177307
178308
@@ -184,6 +314,7 @@ def validate_strategy_plugin_signal_payload(
184314 expected_mode : str | None = None ,
185315 source_uri : str | None = None ,
186316 local_path : str | None = None ,
317+ plugin_definitions : Mapping [str , StrategyPluginDefinition ] | Sequence [StrategyPluginDefinition ] | None = None ,
187318) -> StrategyPluginSignal :
188319 strategy = _required_string (payload .get ("strategy" ), field_name = "strategy" )
189320 plugin = _required_string (payload .get ("plugin" ), field_name = "plugin" )
@@ -210,6 +341,13 @@ def validate_strategy_plugin_signal_payload(
210341 "strategy plugin artifact mode mismatch: "
211342 f"expected { normalized_expected_mode } , got { effective_mode } "
212343 )
344+ validate_strategy_plugin_compatibility (
345+ strategy = strategy ,
346+ plugin = plugin ,
347+ mode = effective_mode ,
348+ plugin_definitions = plugin_definitions ,
349+ source = "artifact" ,
350+ )
213351
214352 execution_controls = payload .get ("execution_controls" ) or {}
215353 if not isinstance (execution_controls , Mapping ):
0 commit comments