Skip to content

Commit 3dffc0f

Browse files
committed
Add shared strategy component loader
1 parent 37e7fec commit 3dffc0f

3 files changed

Lines changed: 81 additions & 2 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.5.0"
7+
version = "0.6.0"
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/common/strategies.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,62 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass
3+
from dataclasses import dataclass, field
4+
from importlib import import_module
5+
from types import ModuleType
6+
from typing import Iterable
47

58
US_EQUITY_DOMAIN = "us_equity"
69
CRYPTO_DOMAIN = "crypto"
710

811

12+
@dataclass(frozen=True)
13+
class StrategyComponentDefinition:
14+
name: str
15+
module_path: str
16+
17+
918
@dataclass(frozen=True)
1019
class StrategyDefinition:
1120
profile: str
1221
domain: str
1322
supported_platforms: frozenset[str]
23+
components: tuple[StrategyComponentDefinition, ...] = field(default_factory=tuple)
24+
25+
26+
def get_strategy_component_map(
27+
definition: StrategyDefinition,
28+
) -> dict[str, StrategyComponentDefinition]:
29+
return {component.name: component for component in definition.components}
30+
31+
32+
def load_strategy_component_module(
33+
definition: StrategyDefinition,
34+
*,
35+
component_name: str,
36+
) -> ModuleType:
37+
component_map = get_strategy_component_map(definition)
38+
component = component_map.get(component_name)
39+
if component is None:
40+
available = ", ".join(sorted(component_map)) or "<none>"
41+
raise ValueError(
42+
f"Strategy profile {definition.profile!r} does not expose component "
43+
f"{component_name!r}; available components: {available}"
44+
)
45+
return import_module(component.module_path)
46+
47+
48+
def load_strategy_component_modules(
49+
definition: StrategyDefinition,
50+
*,
51+
component_names: Iterable[str],
52+
) -> dict[str, ModuleType]:
53+
return {
54+
component_name: load_strategy_component_module(
55+
definition,
56+
component_name=component_name,
57+
)
58+
for component_name in component_names
59+
}
1460

1561

1662
def get_supported_profiles_for_platform(

tests/test_strategies.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
from quant_platform_kit.common.strategies import (
66
CRYPTO_DOMAIN,
77
US_EQUITY_DOMAIN,
8+
StrategyComponentDefinition,
89
StrategyDefinition,
910
get_supported_profiles_for_platform,
11+
load_strategy_component_module,
1012
resolve_strategy_definition,
1113
)
1214

@@ -18,11 +20,23 @@ def setUp(self) -> None:
1820
profile="global_etf_rotation",
1921
domain=US_EQUITY_DOMAIN,
2022
supported_platforms=frozenset({"ibkr", "schwab", "longbridge"}),
23+
components=(
24+
StrategyComponentDefinition(
25+
name="signal_logic",
26+
module_path="math",
27+
),
28+
),
2129
),
2230
"crypto_leader_rotation": StrategyDefinition(
2331
profile="crypto_leader_rotation",
2432
domain=CRYPTO_DOMAIN,
2533
supported_platforms=frozenset({"binance"}),
34+
components=(
35+
StrategyComponentDefinition(
36+
name="core",
37+
module_path="math",
38+
),
39+
),
2640
),
2741
}
2842
self.platform_supported_domains = {
@@ -69,3 +83,22 @@ def test_resolve_strategy_definition_rejects_profile_outside_platform_domain(sel
6983
strategy_definitions=self.strategy_definitions,
7084
platform_supported_domains=self.platform_supported_domains,
7185
)
86+
87+
def test_load_strategy_component_module_imports_named_component(self) -> None:
88+
definition = self.strategy_definitions["global_etf_rotation"]
89+
90+
module = load_strategy_component_module(
91+
definition,
92+
component_name="signal_logic",
93+
)
94+
95+
self.assertEqual(module.__name__, "math")
96+
97+
def test_load_strategy_component_module_rejects_unknown_component(self) -> None:
98+
definition = self.strategy_definitions["global_etf_rotation"]
99+
100+
with self.assertRaisesRegex(ValueError, "available components: signal_logic"):
101+
load_strategy_component_module(
102+
definition,
103+
component_name="allocation",
104+
)

0 commit comments

Comments
 (0)