33from dataclasses import dataclass , field
44from importlib import import_module
55from types import ModuleType
6- from typing import Iterable
6+ from typing import Iterable , Mapping
77
88US_EQUITY_DOMAIN = "us_equity"
99CRYPTO_DOMAIN = "crypto"
@@ -23,6 +23,257 @@ class StrategyDefinition:
2323 components : tuple [StrategyComponentDefinition , ...] = field (default_factory = tuple )
2424
2525
26+ @dataclass (frozen = True )
27+ class StrategyMetadata :
28+ canonical_profile : str
29+ display_name : str
30+ description : str
31+ aliases : tuple [str , ...] = ()
32+ cadence : str | None = None
33+ asset_scope : str | None = None
34+ benchmark : str | None = None
35+ role : str | None = None
36+ status : str | None = None
37+
38+
39+ @dataclass (frozen = True )
40+ class StrategyCatalog :
41+ definitions : Mapping [str , StrategyDefinition ]
42+ metadata : Mapping [str , StrategyMetadata ] = field (default_factory = dict )
43+ compatible_platforms : Mapping [str , frozenset [str ]] = field (default_factory = dict )
44+ profile_aliases : Mapping [str , str ] = field (default_factory = dict )
45+
46+
47+ @dataclass (frozen = True )
48+ class PlatformStrategyPolicy :
49+ platform_id : str
50+ supported_domains : frozenset [str ]
51+ enabled_profiles : frozenset [str ]
52+ default_profile : str
53+ rollback_profile : str
54+ require_explicit_profile : bool = False
55+
56+
57+ def normalize_profile_name (profile : str | None ) -> str :
58+ return str (profile or "" ).strip ().lower ()
59+
60+
61+ def build_profile_aliases (metadata_map : Mapping [str , StrategyMetadata ]) -> dict [str , str ]:
62+ aliases : dict [str , str ] = {}
63+ for canonical_profile , metadata in metadata_map .items ():
64+ canonical = normalize_profile_name (canonical_profile )
65+ if not canonical :
66+ continue
67+ for alias in metadata .aliases :
68+ normalized_alias = normalize_profile_name (alias )
69+ if not normalized_alias :
70+ continue
71+ existing = aliases .get (normalized_alias )
72+ if existing is not None and existing != canonical :
73+ raise ValueError (
74+ f"Duplicate strategy alias { alias !r} ; already assigned to { existing !r} "
75+ )
76+ if normalized_alias in metadata_map and normalized_alias != canonical :
77+ raise ValueError (
78+ f"Strategy alias { alias !r} collides with canonical profile { normalized_alias !r} "
79+ )
80+ aliases [normalized_alias ] = canonical
81+ return aliases
82+
83+
84+ def build_strategy_catalog (
85+ * ,
86+ strategy_definitions : Mapping [str , StrategyDefinition ],
87+ metadata : Mapping [str , StrategyMetadata ] | None = None ,
88+ compatible_platforms : Mapping [str , frozenset [str ]] | None = None ,
89+ profile_aliases : Mapping [str , str ] | None = None ,
90+ ) -> StrategyCatalog :
91+ definitions = {normalize_profile_name (profile ): definition for profile , definition in strategy_definitions .items ()}
92+ metadata_map = {
93+ normalize_profile_name (profile ): value for profile , value in (metadata or {}).items ()
94+ }
95+ compatibility_map = {
96+ normalize_profile_name (profile ): frozenset (platforms )
97+ for profile , platforms in (compatible_platforms or {}).items ()
98+ }
99+ missing_metadata = sorted (set (metadata_map ) - set (definitions ))
100+ if missing_metadata :
101+ raise ValueError (f"Metadata provided for unknown profiles: { ', ' .join (missing_metadata )} " )
102+ missing_compatibility = sorted (set (compatibility_map ) - set (definitions ))
103+ if missing_compatibility :
104+ raise ValueError (
105+ f"Compatibility provided for unknown profiles: { ', ' .join (missing_compatibility )} "
106+ )
107+ aliases = {
108+ normalize_profile_name (alias ): normalize_profile_name (canonical )
109+ for alias , canonical in (
110+ profile_aliases .items () if profile_aliases is not None else build_profile_aliases (metadata_map ).items ()
111+ )
112+ }
113+ return StrategyCatalog (
114+ definitions = definitions ,
115+ metadata = metadata_map ,
116+ compatible_platforms = compatibility_map ,
117+ profile_aliases = aliases ,
118+ )
119+
120+
121+ def _unsupported_profile_error (* , profile : str | None , supported : Iterable [str ], aliases : Iterable [str ]) -> ValueError :
122+ supported_text = ", " .join (sorted (supported )) or "<none>"
123+ alias_text = ", " .join (sorted (aliases )) or "<none>"
124+ return ValueError (
125+ f"Unknown strategy profile={ profile !r} ; supported canonical values: { supported_text } ; aliases: { alias_text } "
126+ )
127+
128+
129+ def resolve_catalog_profile (profile : str | None , * , strategy_catalog : StrategyCatalog ) -> str :
130+ normalized = normalize_profile_name (profile )
131+ if not normalized :
132+ return normalized
133+ return str (strategy_catalog .profile_aliases .get (normalized , normalized ))
134+
135+
136+ def get_catalog_strategy_definition (
137+ strategy_catalog : StrategyCatalog ,
138+ profile : str ,
139+ ) -> StrategyDefinition :
140+ canonical = resolve_catalog_profile (profile , strategy_catalog = strategy_catalog )
141+ definition = strategy_catalog .definitions .get (canonical )
142+ if definition is None :
143+ raise _unsupported_profile_error (
144+ profile = profile ,
145+ supported = strategy_catalog .definitions ,
146+ aliases = strategy_catalog .profile_aliases ,
147+ )
148+ return definition
149+
150+
151+ def get_catalog_strategy_metadata (
152+ strategy_catalog : StrategyCatalog ,
153+ profile : str ,
154+ ) -> StrategyMetadata :
155+ canonical = resolve_catalog_profile (profile , strategy_catalog = strategy_catalog )
156+ metadata = strategy_catalog .metadata .get (canonical )
157+ if metadata is None :
158+ raise _unsupported_profile_error (
159+ profile = profile ,
160+ supported = strategy_catalog .metadata ,
161+ aliases = strategy_catalog .profile_aliases ,
162+ )
163+ return metadata
164+
165+
166+ def get_catalog_compatible_platforms (
167+ strategy_catalog : StrategyCatalog ,
168+ profile : str ,
169+ ) -> frozenset [str ]:
170+ canonical = resolve_catalog_profile (profile , strategy_catalog = strategy_catalog )
171+ platforms = strategy_catalog .compatible_platforms .get (canonical )
172+ if platforms is not None :
173+ return frozenset (platforms )
174+ definition = strategy_catalog .definitions .get (canonical )
175+ if definition is None :
176+ raise _unsupported_profile_error (
177+ profile = profile ,
178+ supported = strategy_catalog .definitions ,
179+ aliases = strategy_catalog .profile_aliases ,
180+ )
181+ return definition .supported_platforms
182+
183+
184+ def build_strategy_index_rows (strategy_catalog : StrategyCatalog ) -> list [dict [str , object ]]:
185+ rows : list [dict [str , object ]] = []
186+ for canonical_profile in sorted (strategy_catalog .definitions ):
187+ definition = strategy_catalog .definitions [canonical_profile ]
188+ metadata = strategy_catalog .metadata .get (canonical_profile )
189+ rows .append (
190+ {
191+ "canonical_profile" : canonical_profile ,
192+ "display_name" : metadata .display_name if metadata else canonical_profile ,
193+ "aliases" : metadata .aliases if metadata else (),
194+ "description" : metadata .description if metadata else "" ,
195+ "cadence" : metadata .cadence if metadata else None ,
196+ "asset_scope" : metadata .asset_scope if metadata else None ,
197+ "benchmark" : metadata .benchmark if metadata else None ,
198+ "role" : metadata .role if metadata else None ,
199+ "status" : metadata .status if metadata else None ,
200+ "component_names" : tuple (component .name for component in definition .components ),
201+ "compatible_platforms" : get_catalog_compatible_platforms (
202+ strategy_catalog ,
203+ canonical_profile ,
204+ ),
205+ }
206+ )
207+ return rows
208+
209+
210+ def get_enabled_profiles_for_platform (
211+ platform_id : str ,
212+ * ,
213+ policy : PlatformStrategyPolicy ,
214+ ) -> frozenset [str ]:
215+ if platform_id != policy .platform_id :
216+ return frozenset ()
217+ return policy .enabled_profiles
218+
219+
220+ def build_platform_profile_matrix (
221+ strategy_catalog : StrategyCatalog ,
222+ * ,
223+ policy : PlatformStrategyPolicy ,
224+ ) -> list [dict [str , object ]]:
225+ rows : list [dict [str , object ]] = []
226+ for profile in sorted (policy .enabled_profiles ):
227+ definition = get_catalog_strategy_definition (strategy_catalog , profile )
228+ metadata = strategy_catalog .metadata .get (definition .profile )
229+ rows .append (
230+ {
231+ "platform" : policy .platform_id ,
232+ "canonical_profile" : definition .profile ,
233+ "display_name" : metadata .display_name if metadata else definition .profile ,
234+ "aliases" : metadata .aliases if metadata else (),
235+ "enabled" : True ,
236+ "is_default" : definition .profile == policy .default_profile ,
237+ "is_rollback" : definition .profile == policy .rollback_profile ,
238+ "domain" : definition .domain ,
239+ }
240+ )
241+ return rows
242+
243+
244+ def resolve_platform_strategy_definition (
245+ raw_value : str | None ,
246+ * ,
247+ platform_id : str ,
248+ strategy_catalog : StrategyCatalog ,
249+ policy : PlatformStrategyPolicy ,
250+ ) -> StrategyDefinition :
251+ if platform_id != policy .platform_id :
252+ raise ValueError (f"Unsupported platform_id={ platform_id !r} " )
253+
254+ normalized = normalize_profile_name (raw_value )
255+ if policy .require_explicit_profile and not normalized :
256+ raise EnvironmentError ("STRATEGY_PROFILE is required" )
257+
258+ candidate = normalized or normalize_profile_name (policy .default_profile )
259+ if not candidate :
260+ raise EnvironmentError ("STRATEGY_PROFILE is required" )
261+
262+ canonical = resolve_catalog_profile (candidate , strategy_catalog = strategy_catalog )
263+ supported = ", " .join (sorted (policy .enabled_profiles )) or "<none>"
264+ if canonical not in policy .enabled_profiles :
265+ raise ValueError (
266+ f"Unsupported STRATEGY_PROFILE={ raw_value !r} ; supported values: { supported } "
267+ )
268+
269+ definition = get_catalog_strategy_definition (strategy_catalog , canonical )
270+ if definition .domain not in policy .supported_domains :
271+ raise ValueError (
272+ f"Unsupported strategy domain { definition .domain !r} for platform { platform_id !r} "
273+ )
274+ return definition
275+
276+
26277def get_strategy_component_map (
27278 definition : StrategyDefinition ,
28279) -> dict [str , StrategyComponentDefinition ]:
0 commit comments