diff --git a/notifications/telegram.py b/notifications/telegram.py index a406f7e..707f4e4 100644 --- a/notifications/telegram.py +++ b/notifications/telegram.py @@ -140,6 +140,8 @@ "strategy_name_mega_cap_leader_rotation_top50_balanced": "Mega Cap Top50 平衡龙头轮动", "strategy_name_nasdaq_sp500_smart_dca": "纳斯达克 / 标普智能定投", "strategy_name_hk_listed_global_etf_rotation": "港股上市全球 ETF 轮动", + "strategy_name_hk_high_dividend_low_vol_trend": "港股高股息低波趋势", + "strategy_name_hk_low_vol_dividend_quality": "港股低波股息质量", "strategy_name_tqqq_growth_income": "TQQQ 增长收益", "strategy_name_soxl_soxx_trend_income": "SOXL/SOXX 半导体趋势收益", }, @@ -279,6 +281,8 @@ "strategy_name_mega_cap_leader_rotation_top50_balanced": "Mega Cap Leader Rotation Top50 Balanced", "strategy_name_nasdaq_sp500_smart_dca": "Nasdaq/S&P 500 Smart DCA", "strategy_name_hk_listed_global_etf_rotation": "HK-listed Global ETF Rotation", + "strategy_name_hk_high_dividend_low_vol_trend": "HK High Dividend Low-Volatility Trend", + "strategy_name_hk_low_vol_dividend_quality": "HK Low-Volatility Dividend Quality", "strategy_name_tqqq_growth_income": "TQQQ Growth Income", "strategy_name_soxl_soxx_trend_income": "SOXL/SOXX Semiconductor Trend Income", }, diff --git a/requirements.txt b/requirements.txt index 3009974..4793e51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ flask gunicorn -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@v0.7.35 -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@v0.7.49 -hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@71141ce9e8a343cec8e2140994071eea66422bc6 +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@v0.7.36 +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@v0.7.50 +hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@hk-low-vol-dividend-quality-20260603 pandas numpy requests diff --git a/scripts/print_strategy_switch_env_plan.py b/scripts/print_strategy_switch_env_plan.py index 1289e94..f0178d2 100644 --- a/scripts/print_strategy_switch_env_plan.py +++ b/scripts/print_strategy_switch_env_plan.py @@ -81,6 +81,12 @@ def _should_add_local_src(candidate: Path) -> bool: ] +def _feature_snapshot_filenames(profile: str, snapshot_contract_version: str | None) -> tuple[str, str]: + suffix = "factor_snapshot" if ".factor_snapshot." in str(snapshot_contract_version or "") else "feature_snapshot" + snapshot_filename = f"{profile}_{suffix}_latest.csv" + return snapshot_filename, f"{snapshot_filename}.manifest.json" + + def build_switch_plan( profile: str, *, @@ -223,10 +229,12 @@ def build_switch_plan( hints: dict[str, str] = {} if requires_feature_snapshot: - hints["feature_snapshot_filename"] = f"{definition.profile}_feature_snapshot_latest.csv" - hints["feature_snapshot_manifest_filename"] = ( - f"{definition.profile}_feature_snapshot_latest.csv.manifest.json" + snapshot_filename, manifest_filename = _feature_snapshot_filenames( + definition.profile, + runtime_requirements.get("snapshot_contract_version"), ) + hints["feature_snapshot_filename"] = snapshot_filename + hints["feature_snapshot_manifest_filename"] = manifest_filename if artifact_paths.bundled_config_path is not None: hints["bundled_strategy_config_path"] = str(artifact_paths.bundled_config_path) diff --git a/tests/test_notifications.py b/tests/test_notifications.py index b3f9992..bdbe712 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -86,6 +86,10 @@ def test_strategy_display_name_translates_new_live_profiles(): assert en_name("nasdaq_sp500_smart_dca") == "Nasdaq/S&P 500 Smart DCA" assert zh_name("hk_listed_global_etf_rotation") == "港股上市全球 ETF 轮动" assert en_name("hk_listed_global_etf_rotation") == "HK-listed Global ETF Rotation" + assert zh_name("hk_high_dividend_low_vol_trend") == "港股高股息低波趋势" + assert en_name("hk_high_dividend_low_vol_trend") == "HK High Dividend Low-Volatility Trend" + assert zh_name("hk_low_vol_dividend_quality") == "港股低波股息质量" + assert en_name("hk_low_vol_dividend_quality") == "HK Low-Volatility Dividend Quality" def test_supported_strategy_profiles_have_translated_names(): diff --git a/tests/test_runtime_config_support.py b/tests/test_runtime_config_support.py index 8bbdafe..6062afc 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -63,7 +63,9 @@ "soxl_soxx_trend_income", "tech_communication_pullback_enhancement", "tqqq_growth_income", + "hk_high_dividend_low_vol_trend", "hk_listed_global_etf_rotation", + "hk_low_vol_dividend_quality", } ) HK_DISABLED_PROFILES = frozenset( @@ -1058,6 +1060,42 @@ def test_print_strategy_switch_env_plan_for_mega_cap_top50_balanced_profile(): assert plan["hints"]["feature_snapshot_filename"] == "mega_cap_leader_rotation_top50_balanced_feature_snapshot_latest.csv" +def test_print_strategy_switch_env_plan_for_hk_low_vol_dividend_quality_profile(): + result = subprocess.run( + [ + sys.executable, + str(SWITCH_PLAN_SCRIPT_PATH), + "--profile", + "hk_low_vol_dividend_quality", + "--dry-run-only", + "--deployment-selector", + "hk-verify", + "--account-scope", + "HK", + "--service-name", + "interactive-brokers-hk-verify-service", + "--json", + ], + check=True, + capture_output=True, + text=True, + ) + + plan = json.loads(result.stdout) + assert plan["canonical_profile"] == "hk_low_vol_dividend_quality" + assert plan["enabled"] is True + assert plan["profile_group"] == "snapshot_backed" + assert plan["input_mode"] == "feature_snapshot" + assert plan["snapshot_contract_version"] == "hk_low_vol_dividend_quality.factor_snapshot.v1" + assert plan["set_env"]["IBKR_DRY_RUN_ONLY"] == "true" + assert plan["set_env"]["IBKR_FEATURE_SNAPSHOT_PATH"] == "" + assert plan["set_env"]["IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH"] == "" + assert plan["hints"]["feature_snapshot_filename"] == "hk_low_vol_dividend_quality_factor_snapshot_latest.csv" + assert plan["hints"]["feature_snapshot_manifest_filename"] == ( + "hk_low_vol_dividend_quality_factor_snapshot_latest.csv.manifest.json" + ) + + @pytest.mark.parametrize("profile", sorted(HK_DISABLED_PROFILES)) def test_print_strategy_switch_env_plan_rejects_hk_disabled_profiles(profile): result = subprocess.run(