Skip to content

Commit a4ce2da

Browse files
authored
refactor strategy metadata and platform registry support (#4)
1 parent 0a6c48c commit a4ce2da

6 files changed

Lines changed: 440 additions & 70 deletions

File tree

README.md

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ This repository is the strategy layer: it owns pure signal, allocation, and targ
1313

1414
### Strategy index
1515

16-
| Profile | Downstream runtime today | Core idea |
17-
| --- | --- | --- |
18-
| `global_etf_rotation` | `InteractiveBrokersPlatform` | Quarterly top-2 global ETF rotation with a daily canary defense |
19-
| `russell_1000_multi_factor_defensive` | `InteractiveBrokersPlatform` | Russell 1000 price-only monthly stock selection with SPY + breadth defense and BOXX parking |
20-
| `hybrid_growth_income` | `CharlesSchwabPlatform` | QQQ-driven TQQQ attack layer plus SPYI / QQQI income layer and BOXX defense |
21-
| `semiconductor_rotation_income` | `LongBridgePlatform` | SOXL / SOXX trend switch with BOXX parking and an additive income sleeve |
16+
| Canonical profile | Display name | Alias | Compatible platforms | Cadence | Benchmark | Role | Status |
17+
| --- | --- | --- | --- | --- | --- | --- | --- |
18+
| `global_etf_rotation` | Global ETF Rotation Defense | `global_macro_etf_rotation` | `InteractiveBrokersPlatform` | `quarterly + daily canary` | `VOO` | `defensive_rotation` | `runtime_enabled` |
19+
| `russell_1000_multi_factor_defensive` | Russell 1000 Multi-Factor Defensive | `r1000_multifactor_defensive` | `InteractiveBrokersPlatform` | `monthly` | `SPY` | `defensive_stock_baseline` | `runtime_enabled` |
20+
| `cash_buffer_branch_default` | Tech Pullback Cash Buffer | `tech_pullback_cash_buffer` | `InteractiveBrokersPlatform` | `monthly` | `QQQ` | `parallel_cash_buffer_branch` | `paper_dry_run` |
21+
| `hybrid_growth_income` | QQQ/TQQQ Growth Income | `qqq_tqqq_growth_income` | `CharlesSchwabPlatform` | `daily` | `QQQ` | `offensive_income` | `runtime_enabled` |
22+
| `semiconductor_rotation_income` | Semiconductor Trend Income | `semiconductor_trend_income` | `LongBridgePlatform` | `daily` | `SOXX` | `sector_offensive_income` | `runtime_enabled` |
2223

23-
These strategies are consumed by platform repositories through `QuantPlatformKit` strategy contracts and component loaders.
24+
These strategies are consumed by platform repositories through `QuantPlatformKit` strategy contracts and component loaders. Canonical profile keys stay stable for runtime compatibility; display names and aliases are the human-facing layer. Compatibility here means the strategy is structurally usable on that broker stack. Whether a profile is actually enabled, default, or rollback is now owned by each platform repository.
2425

2526
### global_etf_rotation
2627

@@ -274,14 +275,15 @@ PYTHONPATH=src:. python3 scripts/backtest_russell_1000_multi_factor_defensive.py
274275

275276
### 策略索引
276277

277-
| 策略档位 | 当前下游运行仓库 | 核心思路 |
278-
| --- | --- | --- |
279-
| `global_etf_rotation` | `InteractiveBrokersPlatform` | 22 只全球 ETF 的季度 Top 2 轮动,带每日 canary 防守 |
280-
| `russell_1000_multi_factor_defensive` | `InteractiveBrokersPlatform` | Russell 1000 个股月频 price-only 选股,带 SPY + breadth 防守和 BOXX 停泊 |
281-
| `hybrid_growth_income` | `CharlesSchwabPlatform` | 由 QQQ 驱动的 TQQQ 攻击层,加上 SPYI / QQQI 收入层和 BOXX 防守层 |
282-
| `semiconductor_rotation_income` | `LongBridgePlatform` | SOXL / SOXX 趋势切换,剩余资金停在 BOXX,并叠加收入层 |
278+
| Canonical profile | 显示名 | Alias | 当前下游运行仓库 | 核心思路 |
279+
| --- | --- | --- | --- | --- |
280+
| `global_etf_rotation` | 全球 ETF 轮动防守 | `global_macro_etf_rotation` | `InteractiveBrokersPlatform` | 22 只全球 ETF 的季度 Top 2 轮动,带每日 canary 防守 |
281+
| `russell_1000_multi_factor_defensive` | Russell 1000 多因子防守 | `r1000_multifactor_defensive` | `InteractiveBrokersPlatform` | Russell 1000 个股月频 price-only 选股,带 SPY + breadth 防守和 BOXX 停泊 |
282+
| `cash_buffer_branch_default` | 科技回调现金缓冲分支 | `tech_pullback_cash_buffer` | `InteractiveBrokersPlatform` | tech-heavy 月频个股选择,做受控回调,并显式保留 BOXX 缓冲 |
283+
| `hybrid_growth_income` | QQQ/TQQQ 增长收入混合 | `qqq_tqqq_growth_income` | `CharlesSchwabPlatform` | 由 QQQ 驱动的 TQQQ 攻击层,加上 SPYI / QQQI 收入层和 BOXX 防守层 |
284+
| `semiconductor_rotation_income` | 半导体趋势收入增强 | `semiconductor_trend_income` | `LongBridgePlatform` | SOXL / SOXX 趋势切换,剩余资金停在 BOXX,并叠加收入层 |
283285

284-
这些策略通过 `QuantPlatformKit` 提供的策略契约和组件加载接口,被各个平台仓库引用。
286+
这些策略通过 `QuantPlatformKit` 提供的策略契约和组件加载接口,被各个平台仓库引用。运行时继续使用稳定的 canonical profile key;显示名和 alias 只负责让人更容易看懂。
285287

286288
### global_etf_rotation
287289

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
1-
from .catalog import STRATEGY_DEFINITIONS, get_strategy_definition, get_strategy_definitions
2-
31
__all__ = [
42
"STRATEGY_DEFINITIONS",
3+
"get_compatible_platforms",
4+
"get_profile_aliases",
5+
"get_strategy_index_rows",
56
"get_strategy_definition",
67
"get_strategy_definitions",
8+
"get_strategy_metadata",
9+
"get_strategy_metadata_map",
10+
"get_strategy_platform_compatibility_map",
11+
"resolve_canonical_profile",
712
]
13+
14+
15+
def __getattr__(name: str):
16+
if name in {
17+
"STRATEGY_DEFINITIONS",
18+
"get_profile_aliases",
19+
"get_compatible_platforms",
20+
"get_strategy_index_rows",
21+
"get_strategy_definition",
22+
"get_strategy_definitions",
23+
"get_strategy_metadata",
24+
"get_strategy_metadata_map",
25+
"get_strategy_platform_compatibility_map",
26+
"resolve_canonical_profile",
27+
}:
28+
from . import catalog as _catalog
29+
30+
return getattr(_catalog, name)
31+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Lines changed: 200 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from dataclasses import dataclass
4+
35
from quant_platform_kit.common.strategies import (
46
StrategyComponentDefinition,
57
StrategyDefinition,
@@ -12,74 +14,222 @@
1214
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE = "russell_1000_multi_factor_defensive"
1315
CASH_BUFFER_BRANCH_DEFAULT_PROFILE = "cash_buffer_branch_default"
1416

15-
STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
16-
GLOBAL_ETF_ROTATION_PROFILE: StrategyDefinition(
17-
profile=GLOBAL_ETF_ROTATION_PROFILE,
17+
18+
STRATEGY_PLATFORM_COMPATIBILITY: dict[str, frozenset[str]] = {
19+
GLOBAL_ETF_ROTATION_PROFILE: frozenset({"ibkr"}),
20+
HYBRID_GROWTH_INCOME_PROFILE: frozenset({"schwab"}),
21+
SEMICONDUCTOR_ROTATION_INCOME_PROFILE: frozenset({"longbridge"}),
22+
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: frozenset({"ibkr"}),
23+
CASH_BUFFER_BRANCH_DEFAULT_PROFILE: frozenset({"ibkr"}),
24+
}
25+
26+
27+
@dataclass(frozen=True)
28+
class StrategyMetadata:
29+
canonical_profile: str
30+
display_name: str
31+
description: str
32+
aliases: tuple[str, ...] = ()
33+
cadence: str | None = None
34+
asset_scope: str | None = None
35+
benchmark: str | None = None
36+
role: str | None = None
37+
status: str | None = None
38+
39+
40+
# `supported_platforms` 仍保留为兼容镜像,避免一次性改动所有平台 runtime。
41+
# 平台真正的启用状态由各自 runtime 仓库维护;UES 这里只表达策略层兼容性。
42+
def _build_strategy_definition(
43+
profile: str,
44+
*,
45+
component_name: str,
46+
module_path: str,
47+
) -> StrategyDefinition:
48+
return StrategyDefinition(
49+
profile=profile,
1850
domain=US_EQUITY_DOMAIN,
19-
supported_platforms=frozenset({"ibkr"}),
51+
supported_platforms=STRATEGY_PLATFORM_COMPATIBILITY[profile],
2052
components=(
2153
StrategyComponentDefinition(
22-
name="signal_logic",
23-
module_path="us_equity_strategies.strategies.global_etf_rotation",
54+
name=component_name,
55+
module_path=module_path,
2456
),
2557
),
58+
)
59+
60+
61+
STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
62+
GLOBAL_ETF_ROTATION_PROFILE: _build_strategy_definition(
63+
GLOBAL_ETF_ROTATION_PROFILE,
64+
component_name="signal_logic",
65+
module_path="us_equity_strategies.strategies.global_etf_rotation",
2666
),
27-
HYBRID_GROWTH_INCOME_PROFILE: StrategyDefinition(
28-
profile=HYBRID_GROWTH_INCOME_PROFILE,
29-
domain=US_EQUITY_DOMAIN,
30-
supported_platforms=frozenset({"schwab"}),
31-
components=(
32-
StrategyComponentDefinition(
33-
name="allocation",
34-
module_path="us_equity_strategies.strategies.hybrid_growth_income",
35-
),
36-
),
67+
HYBRID_GROWTH_INCOME_PROFILE: _build_strategy_definition(
68+
HYBRID_GROWTH_INCOME_PROFILE,
69+
component_name="allocation",
70+
module_path="us_equity_strategies.strategies.hybrid_growth_income",
3771
),
38-
SEMICONDUCTOR_ROTATION_INCOME_PROFILE: StrategyDefinition(
39-
profile=SEMICONDUCTOR_ROTATION_INCOME_PROFILE,
40-
domain=US_EQUITY_DOMAIN,
41-
supported_platforms=frozenset({"longbridge"}),
42-
components=(
43-
StrategyComponentDefinition(
44-
name="allocation",
45-
module_path="us_equity_strategies.strategies.semiconductor_rotation_income",
46-
),
47-
),
72+
SEMICONDUCTOR_ROTATION_INCOME_PROFILE: _build_strategy_definition(
73+
SEMICONDUCTOR_ROTATION_INCOME_PROFILE,
74+
component_name="allocation",
75+
module_path="us_equity_strategies.strategies.semiconductor_rotation_income",
4876
),
49-
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: StrategyDefinition(
50-
profile=RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE,
51-
domain=US_EQUITY_DOMAIN,
52-
supported_platforms=frozenset({"ibkr"}),
53-
components=(
54-
StrategyComponentDefinition(
55-
name="signal_logic",
56-
module_path="us_equity_strategies.strategies.russell_1000_multi_factor_defensive",
57-
),
58-
),
77+
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: _build_strategy_definition(
78+
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE,
79+
component_name="signal_logic",
80+
module_path="us_equity_strategies.strategies.russell_1000_multi_factor_defensive",
5981
),
60-
CASH_BUFFER_BRANCH_DEFAULT_PROFILE: StrategyDefinition(
61-
profile=CASH_BUFFER_BRANCH_DEFAULT_PROFILE,
62-
domain=US_EQUITY_DOMAIN,
63-
supported_platforms=frozenset({"ibkr"}),
64-
components=(
65-
StrategyComponentDefinition(
66-
name="signal_logic",
67-
module_path="us_equity_strategies.strategies.cash_buffer_branch_default",
68-
),
69-
),
82+
CASH_BUFFER_BRANCH_DEFAULT_PROFILE: _build_strategy_definition(
83+
CASH_BUFFER_BRANCH_DEFAULT_PROFILE,
84+
component_name="signal_logic",
85+
module_path="us_equity_strategies.strategies.cash_buffer_branch_default",
86+
),
87+
}
88+
89+
90+
STRATEGY_METADATA: dict[str, StrategyMetadata] = {
91+
GLOBAL_ETF_ROTATION_PROFILE: StrategyMetadata(
92+
canonical_profile=GLOBAL_ETF_ROTATION_PROFILE,
93+
display_name="Global ETF Rotation Defense",
94+
description="Quarterly top-2 global ETF rotation with daily canary defense and BIL safe haven.",
95+
aliases=("global_macro_etf_rotation",),
96+
cadence="quarterly + daily canary",
97+
asset_scope="global_etf_rotation",
98+
benchmark="VOO",
99+
role="defensive_rotation",
100+
status="runtime_enabled",
101+
),
102+
HYBRID_GROWTH_INCOME_PROFILE: StrategyMetadata(
103+
canonical_profile=HYBRID_GROWTH_INCOME_PROFILE,
104+
display_name="QQQ/TQQQ Growth Income",
105+
description="QQQ-led TQQQ attack sleeve with SPYI / QQQI income and BOXX defense.",
106+
aliases=("qqq_tqqq_growth_income",),
107+
cadence="daily",
108+
asset_scope="us_equity_etf_plus_income",
109+
benchmark="QQQ",
110+
role="offensive_income",
111+
status="runtime_enabled",
70112
),
113+
SEMICONDUCTOR_ROTATION_INCOME_PROFILE: StrategyMetadata(
114+
canonical_profile=SEMICONDUCTOR_ROTATION_INCOME_PROFILE,
115+
display_name="Semiconductor Trend Income",
116+
description="SOXL / SOXX semiconductor trend switch with BOXX parking and additive income sleeve.",
117+
aliases=("semiconductor_trend_income",),
118+
cadence="daily",
119+
asset_scope="semiconductor_etf_plus_income",
120+
benchmark="SOXX",
121+
role="sector_offensive_income",
122+
status="runtime_enabled",
123+
),
124+
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: StrategyMetadata(
125+
canonical_profile=RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE,
126+
display_name="Russell 1000 Multi-Factor Defensive",
127+
description="Monthly price-only Russell 1000 stock selection with SPY+breadth defense and BOXX parking.",
128+
aliases=("r1000_multifactor_defensive",),
129+
cadence="monthly",
130+
asset_scope="us_large_cap_stocks",
131+
benchmark="SPY",
132+
role="defensive_stock_baseline",
133+
status="runtime_enabled",
134+
),
135+
CASH_BUFFER_BRANCH_DEFAULT_PROFILE: StrategyMetadata(
136+
canonical_profile=CASH_BUFFER_BRANCH_DEFAULT_PROFILE,
137+
display_name="Tech Pullback Cash Buffer",
138+
description="Tech-heavy monthly stock selection with controlled pullback entry and explicit BOXX cash buffer.",
139+
aliases=("tech_pullback_cash_buffer",),
140+
cadence="monthly",
141+
asset_scope="us_tech_communication_stocks",
142+
benchmark="QQQ",
143+
role="parallel_cash_buffer_branch",
144+
status="paper_dry_run",
145+
),
146+
}
147+
148+
PROFILE_ALIASES: dict[str, str] = {
149+
alias: metadata.canonical_profile
150+
for metadata in STRATEGY_METADATA.values()
151+
for alias in metadata.aliases
71152
}
72153

73154

155+
def normalize_profile_name(profile: str | None) -> str:
156+
return str(profile or "").strip().lower()
157+
158+
159+
def resolve_canonical_profile(profile: str | None) -> str:
160+
normalized = normalize_profile_name(profile)
161+
return PROFILE_ALIASES.get(normalized, normalized)
162+
163+
74164
def get_strategy_definitions() -> dict[str, StrategyDefinition]:
75165
return dict(STRATEGY_DEFINITIONS)
76166

77167

168+
def get_strategy_platform_compatibility_map() -> dict[str, frozenset[str]]:
169+
return dict(STRATEGY_PLATFORM_COMPATIBILITY)
170+
171+
172+
def get_compatible_platforms(profile: str) -> frozenset[str]:
173+
canonical = resolve_canonical_profile(profile)
174+
if canonical not in STRATEGY_PLATFORM_COMPATIBILITY:
175+
supported = ", ".join(sorted(STRATEGY_PLATFORM_COMPATIBILITY)) or "<none>"
176+
aliases = ", ".join(sorted(PROFILE_ALIASES)) or "<none>"
177+
raise ValueError(
178+
f"Unknown us_equity strategy profile={profile!r}; supported canonical values: {supported}; aliases: {aliases}"
179+
)
180+
return STRATEGY_PLATFORM_COMPATIBILITY[canonical]
181+
182+
78183
def get_strategy_definition(profile: str) -> StrategyDefinition:
79-
normalized = str(profile or "").strip().lower()
80-
if normalized not in STRATEGY_DEFINITIONS:
184+
canonical = resolve_canonical_profile(profile)
185+
if canonical not in STRATEGY_DEFINITIONS:
81186
supported = ", ".join(sorted(STRATEGY_DEFINITIONS)) or "<none>"
187+
aliases = ", ".join(sorted(PROFILE_ALIASES)) or "<none>"
188+
raise ValueError(
189+
f"Unknown us_equity strategy profile={profile!r}; supported canonical values: {supported}; aliases: {aliases}"
190+
)
191+
return STRATEGY_DEFINITIONS[canonical]
192+
193+
194+
195+
def get_strategy_index_rows() -> list[dict[str, object]]:
196+
rows: list[dict[str, object]] = []
197+
for canonical_profile in sorted(STRATEGY_METADATA):
198+
metadata = STRATEGY_METADATA[canonical_profile]
199+
definition = STRATEGY_DEFINITIONS[canonical_profile]
200+
rows.append(
201+
{
202+
"canonical_profile": metadata.canonical_profile,
203+
"display_name": metadata.display_name,
204+
"aliases": metadata.aliases,
205+
"description": metadata.description,
206+
"cadence": metadata.cadence,
207+
"asset_scope": metadata.asset_scope,
208+
"benchmark": metadata.benchmark,
209+
"role": metadata.role,
210+
"status": metadata.status,
211+
"component_names": tuple(component.name for component in definition.components),
212+
"compatible_platforms": STRATEGY_PLATFORM_COMPATIBILITY[canonical_profile],
213+
}
214+
)
215+
return rows
216+
217+
218+
219+
def get_strategy_metadata_map() -> dict[str, StrategyMetadata]:
220+
return dict(STRATEGY_METADATA)
221+
222+
223+
def get_strategy_metadata(profile: str) -> StrategyMetadata:
224+
canonical = resolve_canonical_profile(profile)
225+
if canonical not in STRATEGY_METADATA:
226+
supported = ", ".join(sorted(STRATEGY_METADATA)) or "<none>"
227+
aliases = ", ".join(sorted(PROFILE_ALIASES)) or "<none>"
82228
raise ValueError(
83-
f"Unknown us_equity strategy profile={profile!r}; supported values: {supported}"
229+
f"Unknown us_equity strategy profile={profile!r}; supported canonical values: {supported}; aliases: {aliases}"
84230
)
85-
return STRATEGY_DEFINITIONS[normalized]
231+
return STRATEGY_METADATA[canonical]
232+
233+
234+
def get_profile_aliases() -> dict[str, str]:
235+
return dict(PROFILE_ALIASES)

0 commit comments

Comments
 (0)