|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +from dataclasses import dataclass |
| 4 | + |
3 | 5 | from quant_platform_kit.common.strategies import ( |
4 | 6 | StrategyComponentDefinition, |
5 | 7 | StrategyDefinition, |
|
12 | 14 | RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE = "russell_1000_multi_factor_defensive" |
13 | 15 | CASH_BUFFER_BRANCH_DEFAULT_PROFILE = "cash_buffer_branch_default" |
14 | 16 |
|
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, |
18 | 50 | domain=US_EQUITY_DOMAIN, |
19 | | - supported_platforms=frozenset({"ibkr"}), |
| 51 | + supported_platforms=STRATEGY_PLATFORM_COMPATIBILITY[profile], |
20 | 52 | components=( |
21 | 53 | StrategyComponentDefinition( |
22 | | - name="signal_logic", |
23 | | - module_path="us_equity_strategies.strategies.global_etf_rotation", |
| 54 | + name=component_name, |
| 55 | + module_path=module_path, |
24 | 56 | ), |
25 | 57 | ), |
| 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", |
26 | 66 | ), |
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", |
37 | 71 | ), |
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", |
48 | 76 | ), |
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", |
59 | 81 | ), |
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", |
70 | 112 | ), |
| 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 |
71 | 152 | } |
72 | 153 |
|
73 | 154 |
|
| 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 | + |
74 | 164 | def get_strategy_definitions() -> dict[str, StrategyDefinition]: |
75 | 165 | return dict(STRATEGY_DEFINITIONS) |
76 | 166 |
|
77 | 167 |
|
| 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 | + |
78 | 183 | 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: |
81 | 186 | 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>" |
82 | 228 | 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}" |
84 | 230 | ) |
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