Skip to content

Commit c87f16d

Browse files
authored
Merge pull request #3 from QuantStrategyLab/codex/qpk-shared-strategy-contract
[codex] refactor shared strategy catalog contract
2 parents f2045c5 + ba6de76 commit c87f16d

3 files changed

Lines changed: 384 additions & 1 deletion

File tree

src/quant_platform_kit/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,53 @@
1414
)
1515
from .common.strategies import (
1616
CRYPTO_DOMAIN,
17+
PlatformStrategyPolicy,
18+
StrategyCatalog,
1719
US_EQUITY_DOMAIN,
1820
StrategyDefinition,
21+
StrategyMetadata,
22+
build_platform_profile_matrix,
23+
build_profile_aliases,
24+
build_strategy_catalog,
25+
build_strategy_index_rows,
26+
get_catalog_compatible_platforms,
27+
get_catalog_strategy_definition,
28+
get_catalog_strategy_metadata,
1929
get_supported_profiles_for_platform,
30+
get_enabled_profiles_for_platform,
31+
normalize_profile_name,
32+
resolve_catalog_profile,
33+
resolve_platform_strategy_definition,
2034
resolve_strategy_definition,
2135
)
2236

2337
__all__ = [
2438
"__version__",
2539
"CRYPTO_DOMAIN",
2640
"ExecutionReport",
41+
"PlatformStrategyPolicy",
2742
"OrderIntent",
2843
"PortfolioSnapshot",
2944
"Position",
3045
"PricePoint",
3146
"PriceSeries",
3247
"QuoteSnapshot",
48+
"StrategyCatalog",
3349
"StrategyDefinition",
3450
"StrategyDecision",
51+
"StrategyMetadata",
3552
"US_EQUITY_DOMAIN",
53+
"build_platform_profile_matrix",
54+
"build_profile_aliases",
55+
"build_strategy_catalog",
56+
"build_strategy_index_rows",
57+
"get_catalog_compatible_platforms",
58+
"get_catalog_strategy_definition",
59+
"get_catalog_strategy_metadata",
60+
"get_enabled_profiles_for_platform",
3661
"get_supported_profiles_for_platform",
62+
"normalize_profile_name",
63+
"resolve_catalog_profile",
64+
"resolve_platform_strategy_definition",
3765
"resolve_strategy_definition",
3866
]

src/quant_platform_kit/common/strategies.py

Lines changed: 252 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from dataclasses import dataclass, field
44
from importlib import import_module
55
from types import ModuleType
6-
from typing import Iterable
6+
from typing import Iterable, Mapping
77

88
US_EQUITY_DOMAIN = "us_equity"
99
CRYPTO_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+
26277
def get_strategy_component_map(
27278
definition: StrategyDefinition,
28279
) -> dict[str, StrategyComponentDefinition]:

0 commit comments

Comments
 (0)