Skip to content

Commit bea1278

Browse files
committed
Expose strategy capability metadata
1 parent 4780df6 commit bea1278

4 files changed

Lines changed: 364 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "quant-platform-kit"
7-
version = "0.7.7"
7+
version = "0.7.8"
88
description = "Shared broker adapters, domain models, execution ports, and notification utilities for QuantStrategyLab strategies."
99
readme = "README.md"
1010
requires-python = ">=3.9"

src/quant_platform_kit/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""QuantPlatformKit public package surface."""
22

3-
__version__ = "0.7.7"
3+
__version__ = "0.7.8"
44

55
from .common.models import (
66
ExecutionReport,

src/quant_platform_kit/common/strategies.py

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
from dataclasses import dataclass, field
44
from importlib import import_module
5+
from pathlib import Path
56
from types import ModuleType
6-
from typing import Any, Iterable, Mapping
7+
from typing import Any, Callable, Iterable, Mapping
78

89
from .strategy_contracts import (
910
CallableStrategyEntrypoint,
1011
StrategyContractValidationError,
1112
StrategyEntrypoint,
1213
StrategyManifest,
14+
StrategyRuntimeAdapter,
1315
validate_strategy_manifest,
1416
)
1517

@@ -39,6 +41,18 @@ class StrategyDefinition:
3941
required_inputs: frozenset[str] = frozenset()
4042
compatible_capabilities: frozenset[str] = frozenset()
4143
default_config: Mapping[str, Any] = field(default_factory=dict)
44+
target_mode: str | None = None
45+
bundled_config_relpath: str | None = None
46+
47+
48+
@dataclass(frozen=True)
49+
class StrategyArtifactPaths:
50+
artifact_root: Path | None = None
51+
artifact_dir: Path | None = None
52+
bundled_config_path: Path | None = None
53+
feature_snapshot_path: Path | None = None
54+
feature_snapshot_manifest_path: Path | None = None
55+
reconciliation_output_dir: Path | None = None
4256

4357

4458
@dataclass(frozen=True)
@@ -72,6 +86,18 @@ class PlatformStrategyPolicy:
7286
require_explicit_profile: bool = False
7387

7488

89+
@dataclass(frozen=True)
90+
class PlatformCapabilityMatrix:
91+
platform_id: str
92+
supported_domains: frozenset[str]
93+
supported_target_modes: frozenset[str]
94+
supported_inputs: frozenset[str] = frozenset()
95+
supported_capabilities: frozenset[str] = frozenset()
96+
97+
98+
_SUPPORTED_TARGET_MODES = frozenset({"weight", "value"})
99+
100+
75101
def normalize_profile_name(profile: str | None) -> str:
76102
return str(profile or "").strip().lower()
77103

@@ -141,6 +167,19 @@ def build_strategy_catalog(
141167
)
142168

143169

170+
def _resolve_target_mode(definition: StrategyDefinition) -> str | None:
171+
target_mode = str(definition.target_mode or "").strip().lower()
172+
if not target_mode:
173+
return None
174+
if target_mode not in _SUPPORTED_TARGET_MODES:
175+
supported_text = ", ".join(sorted(_SUPPORTED_TARGET_MODES))
176+
raise ValueError(
177+
f"Unsupported target_mode={definition.target_mode!r} for strategy profile "
178+
f"{definition.profile!r}; supported values: {supported_text}"
179+
)
180+
return target_mode
181+
182+
144183
def _unsupported_profile_error(
145184
*,
146185
profile: str | None,
@@ -234,11 +273,59 @@ def build_strategy_index_rows(strategy_catalog: StrategyCatalog) -> list[dict[st
234273
or "entrypoint" in {component.name for component in definition.components},
235274
"required_inputs": definition.required_inputs,
236275
"compatible_capabilities": definition.compatible_capabilities,
276+
"target_mode": _resolve_target_mode(definition),
277+
"bundled_config_relpath": definition.bundled_config_relpath,
237278
}
238279
)
239280
return rows
240281

241282

283+
def get_catalog_target_mode(
284+
strategy_catalog: StrategyCatalog,
285+
profile: str,
286+
) -> str | None:
287+
definition = get_catalog_strategy_definition(strategy_catalog, profile)
288+
return _resolve_target_mode(definition)
289+
290+
291+
def derive_strategy_artifact_paths(
292+
strategy_catalog: StrategyCatalog,
293+
profile: str,
294+
*,
295+
artifact_root: str | Path | None = None,
296+
repo_root: str | Path | None = None,
297+
) -> StrategyArtifactPaths:
298+
definition = get_catalog_strategy_definition(strategy_catalog, profile)
299+
artifact_root_path = Path(artifact_root).expanduser() if artifact_root else None
300+
artifact_dir = artifact_root_path / definition.profile if artifact_root_path else None
301+
repo_root_path = Path(repo_root).expanduser() if repo_root else None
302+
303+
bundled_config_path = None
304+
bundled_config_relpath = str(definition.bundled_config_relpath or "").strip()
305+
if bundled_config_relpath and repo_root_path is not None:
306+
bundled_config_path = repo_root_path / bundled_config_relpath
307+
308+
feature_snapshot_path = None
309+
feature_snapshot_manifest_path = None
310+
if artifact_dir is not None and "feature_snapshot" in frozenset(definition.required_inputs):
311+
feature_snapshot_filename = f"{definition.profile}_feature_snapshot_latest.csv"
312+
feature_snapshot_path = artifact_dir / feature_snapshot_filename
313+
feature_snapshot_manifest_path = artifact_dir / f"{feature_snapshot_filename}.manifest.json"
314+
315+
reconciliation_output_dir = None
316+
if artifact_dir is not None:
317+
reconciliation_output_dir = artifact_dir / "reconciliation"
318+
319+
return StrategyArtifactPaths(
320+
artifact_root=artifact_root_path,
321+
artifact_dir=artifact_dir,
322+
bundled_config_path=bundled_config_path,
323+
feature_snapshot_path=feature_snapshot_path,
324+
feature_snapshot_manifest_path=feature_snapshot_manifest_path,
325+
reconciliation_output_dir=reconciliation_output_dir,
326+
)
327+
328+
242329
def get_enabled_profiles_for_platform(
243330
platform_id: str,
244331
*,
@@ -249,6 +336,78 @@ def get_enabled_profiles_for_platform(
249336
return policy.enabled_profiles
250337

251338

339+
def _matches_platform_capability_matrix(
340+
definition: StrategyDefinition,
341+
*,
342+
runtime_adapter: StrategyRuntimeAdapter,
343+
capability_matrix: PlatformCapabilityMatrix,
344+
) -> bool:
345+
if definition.domain not in capability_matrix.supported_domains:
346+
return False
347+
348+
target_mode = _resolve_target_mode(definition)
349+
if target_mode is not None and target_mode not in capability_matrix.supported_target_modes:
350+
return False
351+
352+
adapter_inputs = frozenset(runtime_adapter.available_inputs)
353+
if definition.required_inputs - adapter_inputs:
354+
return False
355+
if adapter_inputs - capability_matrix.supported_inputs:
356+
return False
357+
358+
adapter_capabilities = frozenset(runtime_adapter.available_capabilities)
359+
if definition.compatible_capabilities - adapter_capabilities:
360+
return False
361+
if adapter_capabilities - capability_matrix.supported_capabilities:
362+
return False
363+
364+
return True
365+
366+
367+
def derive_eligible_profiles_for_platform(
368+
strategy_catalog: StrategyCatalog,
369+
*,
370+
capability_matrix: PlatformCapabilityMatrix,
371+
runtime_adapter_loader: Callable[[str], StrategyRuntimeAdapter],
372+
) -> frozenset[str]:
373+
eligible_profiles: list[str] = []
374+
for profile in sorted(strategy_catalog.definitions):
375+
definition = strategy_catalog.definitions[profile]
376+
try:
377+
runtime_adapter = runtime_adapter_loader(profile)
378+
except ValueError:
379+
continue
380+
if _matches_platform_capability_matrix(
381+
definition,
382+
runtime_adapter=runtime_adapter,
383+
capability_matrix=capability_matrix,
384+
):
385+
eligible_profiles.append(definition.profile)
386+
return frozenset(eligible_profiles)
387+
388+
389+
def derive_enabled_profiles_for_platform(
390+
strategy_catalog: StrategyCatalog,
391+
*,
392+
capability_matrix: PlatformCapabilityMatrix,
393+
runtime_adapter_loader: Callable[[str], StrategyRuntimeAdapter],
394+
rollout_allowlist: Iterable[str] | None = None,
395+
) -> frozenset[str]:
396+
eligible_profiles = derive_eligible_profiles_for_platform(
397+
strategy_catalog,
398+
capability_matrix=capability_matrix,
399+
runtime_adapter_loader=runtime_adapter_loader,
400+
)
401+
if rollout_allowlist is None:
402+
return eligible_profiles
403+
404+
normalized_allowlist = {
405+
resolve_catalog_profile(profile, strategy_catalog=strategy_catalog)
406+
for profile in rollout_allowlist
407+
}
408+
return frozenset(sorted(eligible_profiles & normalized_allowlist))
409+
410+
252411
def build_platform_profile_matrix(
253412
strategy_catalog: StrategyCatalog,
254413
*,
@@ -273,6 +432,36 @@ def build_platform_profile_matrix(
273432
return rows
274433

275434

435+
def build_platform_profile_status_matrix(
436+
strategy_catalog: StrategyCatalog,
437+
*,
438+
policy: PlatformStrategyPolicy,
439+
eligible_profiles: Iterable[str],
440+
) -> list[dict[str, object]]:
441+
eligible = {
442+
resolve_catalog_profile(profile, strategy_catalog=strategy_catalog)
443+
for profile in eligible_profiles
444+
}
445+
visible_profiles = sorted(eligible | set(policy.enabled_profiles))
446+
rows: list[dict[str, object]] = []
447+
for profile in visible_profiles:
448+
definition = get_catalog_strategy_definition(strategy_catalog, profile)
449+
metadata = strategy_catalog.metadata.get(definition.profile)
450+
rows.append(
451+
{
452+
"platform": policy.platform_id,
453+
"canonical_profile": definition.profile,
454+
"display_name": metadata.display_name if metadata else definition.profile,
455+
"eligible": definition.profile in eligible,
456+
"enabled": definition.profile in policy.enabled_profiles,
457+
"is_default": definition.profile == policy.default_profile,
458+
"is_rollback": definition.profile == policy.rollback_profile,
459+
"domain": definition.domain,
460+
}
461+
)
462+
return rows
463+
464+
276465
def resolve_platform_strategy_definition(
277466
raw_value: str | None,
278467
*,

0 commit comments

Comments
 (0)