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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ This repository is the strategy layer: it owns pure signal, allocation, and targ
| --- | --- | --- | --- | --- | --- | --- | --- |
| `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` |
| `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. 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.
These strategies are consumed by platform repositories through `QuantPlatformKit` strategy contracts and component loaders. Canonical profile keys are the runtime-facing layer; display names 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 @@ -279,7 +279,7 @@ PYTHONPATH=src:. python3 scripts/backtest_russell_1000_multi_factor_defensive.py
| --- | --- | --- | --- | --- |
| `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 缓冲 |
| `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,并叠加收入层 |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
from datetime import datetime, timezone
from pathlib import Path

from us_equity_strategies.snapshots.cash_buffer_branch_default import (
from us_equity_strategies.snapshots.tech_pullback_cash_buffer import (
build_feature_snapshot,
read_table,
write_table,
)
from us_equity_strategies.strategies.cash_buffer_branch_default import (
from us_equity_strategies.strategies.tech_pullback_cash_buffer import (
PROFILE_NAME,
SNAPSHOT_CONTRACT_VERSION,
)
Expand All @@ -34,7 +34,7 @@ def _default_config_path() -> Path | None:
/ "InteractiveBrokersPlatform"
/ "research"
/ "configs"
/ "growth_pullback_cash_buffer_branch_default.json"
/ "growth_pullback_tech_pullback_cash_buffer.json"
)
return sibling if sibling.exists() else None

Expand All @@ -49,7 +49,7 @@ def write_snapshot_manifest(
resolved_manifest = manifest_path or Path(f"{snapshot_path}.manifest.json")
if config_path is None or not config_path.exists():
raise FileNotFoundError(
f"cash_buffer_branch_default snapshot manifest requires a valid config_path, got: {config_path}"
f"tech_pullback_cash_buffer snapshot manifest requires a valid config_path, got: {config_path}"
)
config_payload = json.loads(config_path.read_text(encoding="utf-8"))
config_sha256 = _sha256_file(config_path)
Expand All @@ -72,7 +72,7 @@ def write_snapshot_manifest(

def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Generate a cash_buffer_branch_default feature snapshot.",
description="Generate a tech_pullback_cash_buffer feature snapshot.",
)
parser.add_argument("--prices", required=True, help="Input price history file (.csv/.json/.jsonl/.parquet)")
parser.add_argument("--universe", required=True, help="Input universe file (.csv/.json/.jsonl/.parquet)")
Expand Down
18 changes: 8 additions & 10 deletions src/us_equity_strategies/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
HYBRID_GROWTH_INCOME_PROFILE = "hybrid_growth_income"
SEMICONDUCTOR_ROTATION_INCOME_PROFILE = "semiconductor_rotation_income"
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE = "russell_1000_multi_factor_defensive"
CASH_BUFFER_BRANCH_DEFAULT_PROFILE = "cash_buffer_branch_default"
TECH_PULLBACK_CASH_BUFFER_PROFILE = "tech_pullback_cash_buffer"


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"}),
TECH_PULLBACK_CASH_BUFFER_PROFILE: frozenset({"ibkr"}),
}

# `supported_platforms` 仍保留为兼容镜像,避免一次性改动所有平台 runtime。
Expand Down Expand Up @@ -72,10 +72,10 @@ def _build_strategy_definition(
component_name="signal_logic",
module_path="us_equity_strategies.strategies.russell_1000_multi_factor_defensive",
),
CASH_BUFFER_BRANCH_DEFAULT_PROFILE: _build_strategy_definition(
CASH_BUFFER_BRANCH_DEFAULT_PROFILE,
TECH_PULLBACK_CASH_BUFFER_PROFILE: _build_strategy_definition(
TECH_PULLBACK_CASH_BUFFER_PROFILE,
component_name="signal_logic",
module_path="us_equity_strategies.strategies.cash_buffer_branch_default",
module_path="us_equity_strategies.strategies.tech_pullback_cash_buffer",
),
}

Expand Down Expand Up @@ -125,11 +125,11 @@ def _build_strategy_definition(
role="defensive_stock_baseline",
status="runtime_enabled",
),
CASH_BUFFER_BRANCH_DEFAULT_PROFILE: StrategyMetadata(
canonical_profile=CASH_BUFFER_BRANCH_DEFAULT_PROFILE,
TECH_PULLBACK_CASH_BUFFER_PROFILE: StrategyMetadata(
canonical_profile=TECH_PULLBACK_CASH_BUFFER_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",),
aliases=(),
cadence="monthly",
asset_scope="us_tech_communication_stocks",
benchmark="QQQ",
Expand Down Expand Up @@ -180,12 +180,10 @@ def get_strategy_definition(profile: str) -> StrategyDefinition:
return get_catalog_strategy_definition(STRATEGY_CATALOG, profile)



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



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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import numpy as np
import pandas as pd

from us_equity_strategies.strategies.cash_buffer_branch_default import (
from us_equity_strategies.strategies.tech_pullback_cash_buffer import (
BENCHMARK_SYMBOL,
DEFAULT_MIN_ADV20_USD,
DEFAULT_SECTOR_WHITELIST,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

SIGNAL_SOURCE = "feature_snapshot"
STATUS_ICON = "🧲"
PROFILE_NAME = "cash_buffer_branch_default"
PROFILE_NAME = "tech_pullback_cash_buffer"
BRANCH_ROLE = "cash-buffered parallel branch"
BENCHMARK_SYMBOL = "QQQ"
SAFE_HAVEN = "BOXX"
Expand All @@ -33,7 +33,7 @@
SNAPSHOT_DATE_COLUMNS = ("as_of", "snapshot_date")
MAX_SNAPSHOT_MONTH_LAG = 1
REQUIRE_SNAPSHOT_MANIFEST = True
SNAPSHOT_CONTRACT_VERSION = "cash_buffer_branch_default.feature_snapshot.v1"
SNAPSHOT_CONTRACT_VERSION = "tech_pullback_cash_buffer.feature_snapshot.v1"

REQUIRED_FEATURE_COLUMNS = frozenset(
{
Expand Down
37 changes: 21 additions & 16 deletions tests/test_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from quant_platform_kit.common.strategies import get_strategy_component_map
from us_equity_strategies import get_strategy_definitions
from us_equity_strategies.catalog import (
CASH_BUFFER_BRANCH_DEFAULT_PROFILE,
TECH_PULLBACK_CASH_BUFFER_PROFILE,
GLOBAL_ETF_ROTATION_PROFILE,
HYBRID_GROWTH_INCOME_PROFILE,
RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE,
Expand Down Expand Up @@ -38,9 +38,9 @@ def test_catalog_contains_supported_profiles(self):
self.assertEqual(catalog[RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE].domain, "us_equity")
self.assertEqual(get_compatible_platforms(RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE), frozenset({"ibkr"}))

self.assertIn(CASH_BUFFER_BRANCH_DEFAULT_PROFILE, catalog)
self.assertEqual(catalog[CASH_BUFFER_BRANCH_DEFAULT_PROFILE].domain, "us_equity")
self.assertEqual(get_compatible_platforms(CASH_BUFFER_BRANCH_DEFAULT_PROFILE), frozenset({"ibkr"}))
self.assertIn(TECH_PULLBACK_CASH_BUFFER_PROFILE, catalog)
self.assertEqual(catalog[TECH_PULLBACK_CASH_BUFFER_PROFILE].domain, "us_equity")
self.assertEqual(get_compatible_platforms(TECH_PULLBACK_CASH_BUFFER_PROFILE), frozenset({"ibkr"}))

def test_supported_platforms_remains_only_a_compatibility_mirror(self):
catalog = get_strategy_definitions()
Expand Down Expand Up @@ -81,41 +81,46 @@ def test_known_profile_resolves(self):
"us_equity_strategies.strategies.russell_1000_multi_factor_defensive",
)

cash_buffer_definition = get_strategy_definition("cash_buffer_branch_default")
self.assertEqual(cash_buffer_definition.profile, CASH_BUFFER_BRANCH_DEFAULT_PROFILE)
cash_buffer_definition = get_strategy_definition("tech_pullback_cash_buffer")
self.assertEqual(cash_buffer_definition.profile, TECH_PULLBACK_CASH_BUFFER_PROFILE)
cash_buffer_module = get_strategy_component_map(cash_buffer_definition)["signal_logic"]
self.assertEqual(
cash_buffer_module.module_path,
"us_equity_strategies.strategies.cash_buffer_branch_default",
"us_equity_strategies.strategies.tech_pullback_cash_buffer",
)

def test_aliases_resolve_to_canonical_profiles(self):
self.assertEqual(resolve_canonical_profile("tech_pullback_cash_buffer"), CASH_BUFFER_BRANCH_DEFAULT_PROFILE)
self.assertEqual(resolve_canonical_profile("global_macro_etf_rotation"), GLOBAL_ETF_ROTATION_PROFILE)
self.assertEqual(resolve_canonical_profile("r1000_multifactor_defensive"), RUSSELL_1000_MULTI_FACTOR_DEFENSIVE_PROFILE)
self.assertEqual(resolve_canonical_profile("qqq_tqqq_growth_income"), HYBRID_GROWTH_INCOME_PROFILE)
self.assertEqual(resolve_canonical_profile("semiconductor_trend_income"), SEMICONDUCTOR_ROTATION_INCOME_PROFILE)
self.assertEqual(get_strategy_definition("tech_pullback_cash_buffer").profile, CASH_BUFFER_BRANCH_DEFAULT_PROFILE)
self.assertEqual(get_strategy_definition("tech_pullback_cash_buffer").profile, TECH_PULLBACK_CASH_BUFFER_PROFILE)

def test_metadata_map_exposes_display_names_and_roles(self):
metadata_map = get_strategy_metadata_map()
self.assertEqual(metadata_map[CASH_BUFFER_BRANCH_DEFAULT_PROFILE].display_name, "Tech Pullback Cash Buffer")
self.assertEqual(metadata_map[CASH_BUFFER_BRANCH_DEFAULT_PROFILE].role, "parallel_cash_buffer_branch")
self.assertEqual(metadata_map[TECH_PULLBACK_CASH_BUFFER_PROFILE].display_name, "Tech Pullback Cash Buffer")
self.assertEqual(metadata_map[TECH_PULLBACK_CASH_BUFFER_PROFILE].role, "parallel_cash_buffer_branch")
self.assertEqual(metadata_map[GLOBAL_ETF_ROTATION_PROFILE].benchmark, "VOO")
self.assertEqual(get_strategy_metadata("tech_pullback_cash_buffer").canonical_profile, CASH_BUFFER_BRANCH_DEFAULT_PROFILE)
self.assertEqual(get_strategy_metadata("tech_pullback_cash_buffer").canonical_profile, TECH_PULLBACK_CASH_BUFFER_PROFILE)
aliases = get_profile_aliases()
self.assertEqual(aliases["tech_pullback_cash_buffer"], CASH_BUFFER_BRANCH_DEFAULT_PROFILE)
self.assertNotIn("tech_pullback_cash_buffer", aliases)
compatibility = get_strategy_platform_compatibility_map()
self.assertEqual(compatibility[CASH_BUFFER_BRANCH_DEFAULT_PROFILE], frozenset({"ibkr"}))
self.assertEqual(compatibility[TECH_PULLBACK_CASH_BUFFER_PROFILE], frozenset({"ibkr"}))

def test_strategy_index_rows_are_human_readable(self):
rows = get_strategy_index_rows()
by_profile = {row["canonical_profile"]: row for row in rows}
self.assertEqual(by_profile[CASH_BUFFER_BRANCH_DEFAULT_PROFILE]["display_name"], "Tech Pullback Cash Buffer")
self.assertEqual(by_profile[TECH_PULLBACK_CASH_BUFFER_PROFILE]["display_name"], "Tech Pullback Cash Buffer")
self.assertEqual(by_profile[HYBRID_GROWTH_INCOME_PROFILE]["aliases"], ("qqq_tqqq_growth_income",))
self.assertIn("signal_logic", by_profile[GLOBAL_ETF_ROTATION_PROFILE]["component_names"])
self.assertEqual(by_profile[CASH_BUFFER_BRANCH_DEFAULT_PROFILE]["compatible_platforms"], frozenset({"ibkr"}))
self.assertEqual(by_profile[TECH_PULLBACK_CASH_BUFFER_PROFILE]["compatible_platforms"], frozenset({"ibkr"}))


class LegacyProfileCompatibilityTest(unittest.TestCase):
def test_legacy_cash_buffer_profile_is_not_supported_anymore(self):
with self.assertRaises(ValueError):
get_strategy_definition("cash_buffer_branch_default")


if __name__ == "__main__":
unittest.main()
14 changes: 7 additions & 7 deletions tests/test_platform_registry_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class PlatformRegistrySupportTest(unittest.TestCase):
def test_get_enabled_profiles_for_platform_filters_by_platform(self):
enabled = frozenset({"cash_buffer_branch_default"})
enabled = frozenset({"tech_pullback_cash_buffer"})
self.assertEqual(
get_enabled_profiles_for_platform(
"ibkr",
Expand All @@ -30,27 +30,27 @@ def test_get_enabled_profiles_for_platform_filters_by_platform(self):
def test_build_platform_profile_matrix_uses_metadata(self):
rows = build_platform_profile_matrix(
platform_id="ibkr",
enabled_profiles=frozenset({"cash_buffer_branch_default"}),
enabled_profiles=frozenset({"tech_pullback_cash_buffer"}),
default_profile="global_etf_rotation",
rollback_profile="global_etf_rotation",
)
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["canonical_profile"], "cash_buffer_branch_default")
self.assertEqual(rows[0]["canonical_profile"], "tech_pullback_cash_buffer")
self.assertEqual(rows[0]["display_name"], "Tech Pullback Cash Buffer")
self.assertEqual(rows[0]["aliases"], ("tech_pullback_cash_buffer",))
self.assertEqual(rows[0]["aliases"], ())
self.assertFalse(rows[0]["is_default"])
self.assertFalse(rows[0]["is_rollback"])

def test_resolve_platform_strategy_definition_supports_alias(self):
def test_resolve_platform_strategy_definition_supports_canonical_profile(self):
definition = resolve_platform_strategy_definition(
"tech_pullback_cash_buffer",
platform_id="ibkr",
expected_platform_id="ibkr",
enabled_profiles=frozenset({"cash_buffer_branch_default"}),
enabled_profiles=frozenset({"tech_pullback_cash_buffer"}),
platform_supported_domains={"ibkr": frozenset({"us_equity"})},
require_explicit=True,
)
self.assertEqual(definition.profile, "cash_buffer_branch_default")
self.assertEqual(definition.profile, "tech_pullback_cash_buffer")


if __name__ == "__main__":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def _feature_snapshot() -> pd.DataFrame:

class CashBufferBranchDefaultStrategyTest(unittest.TestCase):
def test_build_target_weights_is_geometry_honest(self):
from us_equity_strategies.strategies.cash_buffer_branch_default import build_target_weights
from us_equity_strategies.strategies.tech_pullback_cash_buffer import build_target_weights

weights, signal, metadata = build_target_weights(
_feature_snapshot(),
Expand All @@ -113,7 +113,7 @@ def test_build_target_weights_is_geometry_honest(self):
self.assertAlmostEqual(weights["BOXX"], 0.2, places=8)

def test_compute_signals_noops_outside_execution_window(self):
from us_equity_strategies.strategies.cash_buffer_branch_default import compute_signals
from us_equity_strategies.strategies.tech_pullback_cash_buffer import compute_signals

weights, _signal, _emergency, status_desc, metadata = compute_signals(
_feature_snapshot(),
Expand All @@ -126,14 +126,14 @@ def test_compute_signals_noops_outside_execution_window(self):
self.assertIn("no-op", status_desc)

def test_load_runtime_parameters_reads_canonical_config(self):
from us_equity_strategies.strategies.cash_buffer_branch_default import load_runtime_parameters
from us_equity_strategies.strategies.tech_pullback_cash_buffer import load_runtime_parameters

with TemporaryDirectory() as tmp_dir:
config_path = Path(tmp_dir) / "cash_buffer_branch_default.json"
config_path = Path(tmp_dir) / "tech_pullback_cash_buffer.json"
config_path.write_text(
json.dumps(
{
"name": "cash_buffer_branch_default",
"name": "tech_pullback_cash_buffer",
"family": "tech_heavy_pullback",
"branch_role": "cash-buffered parallel branch",
"benchmark_symbol": "QQQ",
Expand All @@ -157,7 +157,7 @@ def test_load_runtime_parameters_reads_canonical_config(self):
params = load_runtime_parameters(config_path=config_path)

self.assertEqual(params["runtime_config_source"], "external_config")
self.assertEqual(params["runtime_config_name"], "cash_buffer_branch_default")
self.assertEqual(params["runtime_config_name"], "tech_pullback_cash_buffer")
self.assertEqual(params["sector_whitelist"], ("Information Technology", "Communication"))
self.assertEqual(params["execution_cash_reserve_ratio"], 0.0)

Expand Down
Loading