Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ This repository is the strategy layer: it owns pure signal, allocation, and targ

### Strategy index

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

These strategies are consumed by platform repositories through `QuantPlatformKit` strategy contracts and component loaders.
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.

### global_etf_rotation

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

### 策略索引

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

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

### global_etf_rotation

Expand Down
28 changes: 26 additions & 2 deletions src/us_equity_strategies/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
from .catalog import STRATEGY_DEFINITIONS, get_strategy_definition, get_strategy_definitions

__all__ = [
"STRATEGY_DEFINITIONS",
"get_compatible_platforms",
"get_profile_aliases",
"get_strategy_index_rows",
"get_strategy_definition",
"get_strategy_definitions",
"get_strategy_metadata",
"get_strategy_metadata_map",
"get_strategy_platform_compatibility_map",
"resolve_canonical_profile",
]


def __getattr__(name: str):
if name in {
"STRATEGY_DEFINITIONS",
"get_profile_aliases",
"get_compatible_platforms",
"get_strategy_index_rows",
"get_strategy_definition",
"get_strategy_definitions",
"get_strategy_metadata",
"get_strategy_metadata_map",
"get_strategy_platform_compatibility_map",
"resolve_canonical_profile",
}:
from . import catalog as _catalog

return getattr(_catalog, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
250 changes: 200 additions & 50 deletions src/us_equity_strategies/catalog.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from dataclasses import dataclass

from quant_platform_kit.common.strategies import (
StrategyComponentDefinition,
StrategyDefinition,
Expand All @@ -12,74 +14,222 @@
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE = "russell_1000_multi_factor_defensive"
CASH_BUFFER_BRANCH_DEFAULT_PROFILE = "cash_buffer_branch_default"

STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
GLOBAL_ETF_ROTATION_PROFILE: StrategyDefinition(
profile=GLOBAL_ETF_ROTATION_PROFILE,

STRATEGY_PLATFORM_COMPATIBILITY: dict[str, frozenset[str]] = {
GLOBAL_ETF_ROTATION_PROFILE: frozenset({"ibkr"}),
HYBRID_GROWTH_INCOME_PROFILE: frozenset({"schwab"}),
SEMICONDUCTOR_ROTATION_INCOME_PROFILE: frozenset({"longbridge"}),
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: frozenset({"ibkr"}),
CASH_BUFFER_BRANCH_DEFAULT_PROFILE: frozenset({"ibkr"}),
}


@dataclass(frozen=True)
class StrategyMetadata:
canonical_profile: str
display_name: str
description: str
aliases: tuple[str, ...] = ()
cadence: str | None = None
asset_scope: str | None = None
benchmark: str | None = None
role: str | None = None
status: str | None = None


# `supported_platforms` 仍保留为兼容镜像,避免一次性改动所有平台 runtime。
# 平台真正的启用状态由各自 runtime 仓库维护;UES 这里只表达策略层兼容性。
def _build_strategy_definition(
profile: str,
*,
component_name: str,
module_path: str,
) -> StrategyDefinition:
return StrategyDefinition(
profile=profile,
domain=US_EQUITY_DOMAIN,
supported_platforms=frozenset({"ibkr"}),
supported_platforms=STRATEGY_PLATFORM_COMPATIBILITY[profile],
components=(
StrategyComponentDefinition(
name="signal_logic",
module_path="us_equity_strategies.strategies.global_etf_rotation",
name=component_name,
module_path=module_path,
),
),
)


STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
GLOBAL_ETF_ROTATION_PROFILE: _build_strategy_definition(
GLOBAL_ETF_ROTATION_PROFILE,
component_name="signal_logic",
module_path="us_equity_strategies.strategies.global_etf_rotation",
),
HYBRID_GROWTH_INCOME_PROFILE: StrategyDefinition(
profile=HYBRID_GROWTH_INCOME_PROFILE,
domain=US_EQUITY_DOMAIN,
supported_platforms=frozenset({"schwab"}),
components=(
StrategyComponentDefinition(
name="allocation",
module_path="us_equity_strategies.strategies.hybrid_growth_income",
),
),
HYBRID_GROWTH_INCOME_PROFILE: _build_strategy_definition(
HYBRID_GROWTH_INCOME_PROFILE,
component_name="allocation",
module_path="us_equity_strategies.strategies.hybrid_growth_income",
),
SEMICONDUCTOR_ROTATION_INCOME_PROFILE: StrategyDefinition(
profile=SEMICONDUCTOR_ROTATION_INCOME_PROFILE,
domain=US_EQUITY_DOMAIN,
supported_platforms=frozenset({"longbridge"}),
components=(
StrategyComponentDefinition(
name="allocation",
module_path="us_equity_strategies.strategies.semiconductor_rotation_income",
),
),
SEMICONDUCTOR_ROTATION_INCOME_PROFILE: _build_strategy_definition(
SEMICONDUCTOR_ROTATION_INCOME_PROFILE,
component_name="allocation",
module_path="us_equity_strategies.strategies.semiconductor_rotation_income",
),
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: StrategyDefinition(
profile=RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE,
domain=US_EQUITY_DOMAIN,
supported_platforms=frozenset({"ibkr"}),
components=(
StrategyComponentDefinition(
name="signal_logic",
module_path="us_equity_strategies.strategies.russell_1000_multi_factor_defensive",
),
),
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: _build_strategy_definition(
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE,
component_name="signal_logic",
module_path="us_equity_strategies.strategies.russell_1000_multi_factor_defensive",
),
CASH_BUFFER_BRANCH_DEFAULT_PROFILE: StrategyDefinition(
profile=CASH_BUFFER_BRANCH_DEFAULT_PROFILE,
domain=US_EQUITY_DOMAIN,
supported_platforms=frozenset({"ibkr"}),
components=(
StrategyComponentDefinition(
name="signal_logic",
module_path="us_equity_strategies.strategies.cash_buffer_branch_default",
),
),
CASH_BUFFER_BRANCH_DEFAULT_PROFILE: _build_strategy_definition(
CASH_BUFFER_BRANCH_DEFAULT_PROFILE,
component_name="signal_logic",
module_path="us_equity_strategies.strategies.cash_buffer_branch_default",
),
}


STRATEGY_METADATA: dict[str, StrategyMetadata] = {
GLOBAL_ETF_ROTATION_PROFILE: StrategyMetadata(
canonical_profile=GLOBAL_ETF_ROTATION_PROFILE,
display_name="Global ETF Rotation Defense",
description="Quarterly top-2 global ETF rotation with daily canary defense and BIL safe haven.",
aliases=("global_macro_etf_rotation",),
cadence="quarterly + daily canary",
asset_scope="global_etf_rotation",
benchmark="VOO",
role="defensive_rotation",
status="runtime_enabled",
),
HYBRID_GROWTH_INCOME_PROFILE: StrategyMetadata(
canonical_profile=HYBRID_GROWTH_INCOME_PROFILE,
display_name="QQQ/TQQQ Growth Income",
description="QQQ-led TQQQ attack sleeve with SPYI / QQQI income and BOXX defense.",
aliases=("qqq_tqqq_growth_income",),
cadence="daily",
asset_scope="us_equity_etf_plus_income",
benchmark="QQQ",
role="offensive_income",
status="runtime_enabled",
),
SEMICONDUCTOR_ROTATION_INCOME_PROFILE: StrategyMetadata(
canonical_profile=SEMICONDUCTOR_ROTATION_INCOME_PROFILE,
display_name="Semiconductor Trend Income",
description="SOXL / SOXX semiconductor trend switch with BOXX parking and additive income sleeve.",
aliases=("semiconductor_trend_income",),
cadence="daily",
asset_scope="semiconductor_etf_plus_income",
benchmark="SOXX",
role="sector_offensive_income",
status="runtime_enabled",
),
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: StrategyMetadata(
canonical_profile=RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE,
display_name="Russell 1000 Multi-Factor Defensive",
description="Monthly price-only Russell 1000 stock selection with SPY+breadth defense and BOXX parking.",
aliases=("r1000_multifactor_defensive",),
cadence="monthly",
asset_scope="us_large_cap_stocks",
benchmark="SPY",
role="defensive_stock_baseline",
status="runtime_enabled",
),
CASH_BUFFER_BRANCH_DEFAULT_PROFILE: StrategyMetadata(
canonical_profile=CASH_BUFFER_BRANCH_DEFAULT_PROFILE,
display_name="Tech Pullback Cash Buffer",
description="Tech-heavy monthly stock selection with controlled pullback entry and explicit BOXX cash buffer.",
aliases=("tech_pullback_cash_buffer",),
cadence="monthly",
asset_scope="us_tech_communication_stocks",
benchmark="QQQ",
role="parallel_cash_buffer_branch",
status="paper_dry_run",
),
}

PROFILE_ALIASES: dict[str, str] = {
alias: metadata.canonical_profile
for metadata in STRATEGY_METADATA.values()
for alias in metadata.aliases
}


def normalize_profile_name(profile: str | None) -> str:
return str(profile or "").strip().lower()


def resolve_canonical_profile(profile: str | None) -> str:
normalized = normalize_profile_name(profile)
return PROFILE_ALIASES.get(normalized, normalized)


def get_strategy_definitions() -> dict[str, StrategyDefinition]:
return dict(STRATEGY_DEFINITIONS)


def get_strategy_platform_compatibility_map() -> dict[str, frozenset[str]]:
return dict(STRATEGY_PLATFORM_COMPATIBILITY)


def get_compatible_platforms(profile: str) -> frozenset[str]:
canonical = resolve_canonical_profile(profile)
if canonical not in STRATEGY_PLATFORM_COMPATIBILITY:
supported = ", ".join(sorted(STRATEGY_PLATFORM_COMPATIBILITY)) or "<none>"
aliases = ", ".join(sorted(PROFILE_ALIASES)) or "<none>"
raise ValueError(
f"Unknown us_equity strategy profile={profile!r}; supported canonical values: {supported}; aliases: {aliases}"
)
return STRATEGY_PLATFORM_COMPATIBILITY[canonical]


def get_strategy_definition(profile: str) -> StrategyDefinition:
normalized = str(profile or "").strip().lower()
if normalized not in STRATEGY_DEFINITIONS:
canonical = resolve_canonical_profile(profile)
if canonical not in STRATEGY_DEFINITIONS:
supported = ", ".join(sorted(STRATEGY_DEFINITIONS)) or "<none>"
aliases = ", ".join(sorted(PROFILE_ALIASES)) or "<none>"
raise ValueError(
f"Unknown us_equity strategy profile={profile!r}; supported canonical values: {supported}; aliases: {aliases}"
)
return STRATEGY_DEFINITIONS[canonical]



def get_strategy_index_rows() -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
for canonical_profile in sorted(STRATEGY_METADATA):
metadata = STRATEGY_METADATA[canonical_profile]
definition = STRATEGY_DEFINITIONS[canonical_profile]
rows.append(
{
"canonical_profile": metadata.canonical_profile,
"display_name": metadata.display_name,
"aliases": metadata.aliases,
"description": metadata.description,
"cadence": metadata.cadence,
"asset_scope": metadata.asset_scope,
"benchmark": metadata.benchmark,
"role": metadata.role,
"status": metadata.status,
"component_names": tuple(component.name for component in definition.components),
"compatible_platforms": STRATEGY_PLATFORM_COMPATIBILITY[canonical_profile],
}
)
return rows



def get_strategy_metadata_map() -> dict[str, StrategyMetadata]:
return dict(STRATEGY_METADATA)


def get_strategy_metadata(profile: str) -> StrategyMetadata:
canonical = resolve_canonical_profile(profile)
if canonical not in STRATEGY_METADATA:
supported = ", ".join(sorted(STRATEGY_METADATA)) or "<none>"
aliases = ", ".join(sorted(PROFILE_ALIASES)) or "<none>"
raise ValueError(
f"Unknown us_equity strategy profile={profile!r}; supported values: {supported}"
f"Unknown us_equity strategy profile={profile!r}; supported canonical values: {supported}; aliases: {aliases}"
)
return STRATEGY_DEFINITIONS[normalized]
return STRATEGY_METADATA[canonical]


def get_profile_aliases() -> dict[str, str]:
return dict(PROFILE_ALIASES)
Loading