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
20 changes: 19 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@ jobs:
- name: Checkout
uses: actions/checkout@v5

- name: Resolve QuantPlatformKit ref
id: quant-platform-kit-ref
run: |
set -euo pipefail
ref="main"
if [ -n "${GITHUB_HEAD_REF:-}" ] && git ls-remote --exit-code --heads https://github.com/QuantStrategyLab/QuantPlatformKit.git "${GITHUB_HEAD_REF}" >/dev/null 2>&1; then
ref="${GITHUB_HEAD_REF}"
fi
echo "ref=${ref}" >> "$GITHUB_OUTPUT"

- name: Checkout QuantPlatformKit
uses: actions/checkout@v5
with:
repository: QuantStrategyLab/QuantPlatformKit
ref: ${{ steps.quant-platform-kit-ref.outputs.ref }}
path: external/QuantPlatformKit

- name: Setup Python
uses: actions/setup-python@v6
with:
Expand All @@ -21,7 +38,8 @@ jobs:
run: |
set -euo pipefail
python -m pip install --upgrade pip
python -m pip install -e . numpy pandas ruff
python -m pip install -e . numpy pandas pandas_market_calendars pytz ruff
python -m pip install --no-deps -e external/QuantPlatformKit

- name: Run Ruff
run: |
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description = "Shared US equity strategy catalog and implementations"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@6e8cc058b821aea8a54015d4b39e02fbdd3dc198",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@5174d9e40f79fffae47450a42e26434145d28b31",
]

[tool.setuptools]
Expand Down
7 changes: 7 additions & 0 deletions src/us_equity_strategies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
"get_compatible_platforms",
"get_profile_aliases",
"get_strategy_catalog",
"get_strategy_entrypoint",
"get_strategy_index_rows",
"get_strategy_definition",
"get_strategy_definitions",
"get_strategy_metadata",
"get_strategy_metadata_map",
"get_platform_runtime_adapter",
"get_strategy_platform_compatibility_map",
"resolve_canonical_profile",
]
Expand All @@ -21,6 +23,7 @@ def __getattr__(name: str):
"get_profile_aliases",
"get_compatible_platforms",
"get_strategy_catalog",
"get_strategy_entrypoint",
"get_strategy_index_rows",
"get_strategy_definition",
"get_strategy_definitions",
Expand All @@ -32,4 +35,8 @@ def __getattr__(name: str):
from . import catalog as _catalog

return getattr(_catalog, name)
if name == "get_platform_runtime_adapter":
from .runtime_adapters import get_platform_runtime_adapter as _get_platform_runtime_adapter

return _get_platform_runtime_adapter
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
116 changes: 116 additions & 0 deletions src/us_equity_strategies/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
StrategyCatalog,
StrategyComponentDefinition,
StrategyDefinition,
StrategyEntrypointDefinition,
StrategyMetadata,
US_EQUITY_DOMAIN,
build_strategy_catalog,
build_strategy_index_rows,
get_catalog_compatible_platforms,
get_catalog_strategy_definition,
get_catalog_strategy_metadata,
load_strategy_entrypoint,
normalize_profile_name as qpk_normalize_profile_name,
resolve_catalog_profile,
)
Expand All @@ -30,6 +32,108 @@
TECH_PULLBACK_CASH_BUFFER_PROFILE: frozenset({"ibkr"}),
}

STRATEGY_REQUIRED_INPUTS: dict[str, frozenset[str]] = {
GLOBAL_ETF_ROTATION_PROFILE: frozenset({"historical_close_loader"}),
HYBRID_GROWTH_INCOME_PROFILE: frozenset({"qqq_history"}),
SEMICONDUCTOR_ROTATION_INCOME_PROFILE: frozenset({"indicators", "account_state"}),
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: frozenset({"feature_snapshot"}),
TECH_PULLBACK_CASH_BUFFER_PROFILE: frozenset({"feature_snapshot"}),
}

STRATEGY_DEFAULT_CONFIG: dict[str, dict[str, object]] = {
GLOBAL_ETF_ROTATION_PROFILE: {
"ranking_pool": (
"EWY", "EWT", "INDA", "FXI", "EWJ", "VGK", "VOO", "XLK", "SMH", "GLD",
"SLV", "USO", "DBA", "XLE", "XLF", "ITA", "XLP", "XLU", "XLV", "IHI", "VNQ", "KRE",
),
"canary_assets": ("SPY", "EFA", "EEM", "AGG"),
"safe_haven": "BIL",
"top_n": 2,
"hold_bonus": 0.02,
"canary_bad_threshold": 4,
"rebalance_months": (3, 6, 9, 12),
"sma_period": 200,
},
HYBRID_GROWTH_INCOME_PROFILE: {
"benchmark_symbol": "QQQ",
"managed_symbols": ("TQQQ", "BOXX", "SPYI", "QQQI"),
"income_threshold_usd": 100000.0,
"qqqi_income_ratio": 0.5,
"cash_reserve_ratio": 0.05,
"rebalance_threshold_ratio": 0.01,
"alloc_tier1_breakpoints": (0, 15000, 30000, 70000),
"alloc_tier1_values": (1.0, 0.95, 0.85, 0.70),
"alloc_tier2_breakpoints": (70000, 140000),
"alloc_tier2_values": (0.70, 0.50),
"risk_leverage_factor": 3.0,
"risk_agg_cap": 0.50,
"risk_numerator": 0.30,
"atr_exit_scale": 2.0,
"atr_entry_scale": 2.5,
"exit_line_floor": 0.92,
"exit_line_cap": 0.98,
"entry_line_floor": 1.02,
"entry_line_cap": 1.08,
},
SEMICONDUCTOR_ROTATION_INCOME_PROFILE: {
"managed_symbols": ("SOXL", "SOXX", "BOXX", "QQQI", "SPYI"),
"trend_ma_window": 150,
"cash_reserve_ratio": 0.03,
"min_trade_ratio": 0.01,
"min_trade_floor": 100.0,
"rebalance_threshold_ratio": 0.01,
"small_account_deploy_ratio": 0.60,
"mid_account_deploy_ratio": 0.57,
"large_account_deploy_ratio": 0.50,
"trade_layer_decay_coeff": 0.04,
"income_layer_start_usd": 150000.0,
"income_layer_max_ratio": 0.15,
"income_layer_qqqi_weight": 0.70,
"income_layer_spyi_weight": 0.30,
},
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: {
"benchmark_symbol": "SPY",
"safe_haven": "BOXX",
"holdings_count": 24,
"single_name_cap": 0.06,
"sector_cap": 0.20,
"hold_bonus": 0.15,
"soft_defense_exposure": 0.50,
"hard_defense_exposure": 0.10,
"soft_breadth_threshold": 0.55,
"hard_breadth_threshold": 0.35,
},
TECH_PULLBACK_CASH_BUFFER_PROFILE: {
"benchmark_symbol": "QQQ",
"safe_haven": "BOXX",
"holdings_count": 8,
"single_name_cap": 0.10,
"sector_cap": 0.40,
"hold_bonus": 0.10,
"risk_on_exposure": 0.80,
"soft_defense_exposure": 0.60,
"hard_defense_exposure": 0.00,
"soft_breadth_threshold": 0.55,
"hard_breadth_threshold": 0.35,
"min_adv20_usd": 50000000.0,
"sector_whitelist": ("Information Technology", "Communication"),
"normalization": "universe_cross_sectional",
"score_template": "balanced_pullback",
"runtime_execution_window_trading_days": 3,
"execution_cash_reserve_ratio": 0.0,
"residual_proxy": "simple_excess_return_vs_QQQ",
},
}

STRATEGY_ENTRYPOINT_ATTRIBUTES: dict[str, str] = {
GLOBAL_ETF_ROTATION_PROFILE: "global_etf_rotation_entrypoint",
HYBRID_GROWTH_INCOME_PROFILE: "hybrid_growth_income_entrypoint",
SEMICONDUCTOR_ROTATION_INCOME_PROFILE: "semiconductor_rotation_income_entrypoint",
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE: "russell_1000_multi_factor_defensive_entrypoint",
TECH_PULLBACK_CASH_BUFFER_PROFILE: "tech_pullback_cash_buffer_entrypoint",
}


# `supported_platforms` 仍保留为兼容镜像,避免一次性改动所有平台 runtime。
# 平台真正的启用状态由各自 runtime 仓库维护;UES 这里只表达策略层兼容性。
def _build_strategy_definition(
Expand All @@ -48,6 +152,12 @@ def _build_strategy_definition(
module_path=module_path,
),
),
entrypoint=StrategyEntrypointDefinition(
module_path="us_equity_strategies.entrypoints",
attribute_name=STRATEGY_ENTRYPOINT_ATTRIBUTES[profile],
),
required_inputs=STRATEGY_REQUIRED_INPUTS[profile],
default_config=STRATEGY_DEFAULT_CONFIG[profile],
)


Expand Down Expand Up @@ -180,6 +290,12 @@ def get_strategy_definition(profile: str) -> StrategyDefinition:
return get_catalog_strategy_definition(STRATEGY_CATALOG, profile)


def get_strategy_entrypoint(profile: str):
definition = get_strategy_definition(profile)
metadata = get_strategy_metadata(profile)
return load_strategy_entrypoint(definition, metadata=metadata)


def get_strategy_index_rows() -> list[dict[str, object]]:
return build_strategy_index_rows(STRATEGY_CATALOG)

Expand Down
Loading