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
4 changes: 4 additions & 0 deletions notifications/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ def _break_telegram_market_symbol_auto_links(value) -> str:
"strategy_name_russell_top50_leader_rotation": "罗素 Top50 领涨轮动",
"strategy_name_ibit_smart_dca": "IBIT 比特币 ETF 智能定投",
"strategy_name_nasdaq_sp500_smart_dca": "纳指100 / 标普500 智能定投",
"strategy_name_us_equity_combo": "美股核心组合",
"strategy_name_us_equity_combo_leveraged": "美股Alpha组合",
"strategy_name_hk_global_etf_tactical_rotation": "港股全球 ETF 战术轮动",
"strategy_name_hk_low_vol_dividend_quality_snapshot": "港股低波股息质量快照",
"strategy_name_cn_industry_etf_rotation": "A股行业ETF轮动",
Expand Down Expand Up @@ -346,6 +348,8 @@ def _break_telegram_market_symbol_auto_links(value) -> str:
"strategy_name_russell_top50_leader_rotation": "Russell Top50 Leader Rotation",
"strategy_name_ibit_smart_dca": "IBIT Smart DCA",
"strategy_name_nasdaq_sp500_smart_dca": "Nasdaq 100 / S&P 500 Smart DCA",
"strategy_name_us_equity_combo": "US Equity Combo",
"strategy_name_us_equity_combo_leveraged": "US Equity Combo Leveraged",
"strategy_name_hk_global_etf_tactical_rotation": "HK Global ETF Tactical Rotation",
"strategy_name_hk_low_vol_dividend_quality_snapshot": "HK Low-Vol Dividend Quality Snapshot",
"strategy_name_cn_industry_etf_rotation": "CN Industry ETF Rotation",
Expand Down
16 changes: 11 additions & 5 deletions runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,19 +334,19 @@ def load_platform_runtime_settings(
debug_position_snapshot=resolve_bool_value(os.getenv("LONGBRIDGE_DEBUG_POSITION_SNAPSHOT")),
income_threshold_usd=resolve_optional_float_env(os.environ, "INCOME_THRESHOLD_USD"),
qqqi_income_ratio=_qqqi_income_ratio_env(),
income_layer_enabled=resolve_optional_bool_env("INCOME_LAYER_ENABLED"),
income_layer_enabled=_optional_bool_env("INCOME_LAYER_ENABLED"),
income_layer_start_usd=_optional_non_negative_float_env("INCOME_LAYER_START_USD"),
income_layer_max_ratio=resolve_optional_ratio_env("INCOME_LAYER_MAX_RATIO"),
dca_mode=resolve_optional_dca_mode_env("DCA_MODE"),
dca_base_investment_usd=resolve_optional_positive_float_env("DCA_BASE_INVESTMENT_USD"),
ibit_zscore_exit_enabled=resolve_optional_bool_env("IBIT_ZSCORE_EXIT_ENABLED"),
ibit_zscore_exit_enabled=_optional_bool_env("IBIT_ZSCORE_EXIT_ENABLED"),
ibit_zscore_exit_mode=resolve_optional_ibit_zscore_exit_mode_env("IBIT_ZSCORE_EXIT_MODE"),
ibit_zscore_exit_parking_symbol=resolve_optional_symbol_env("IBIT_ZSCORE_EXIT_PARKING_SYMBOL"),
ibit_zscore_exit_risk_reduced_exposure=resolve_optional_ratio_env(
"IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE"
),
ibit_zscore_exit_risk_off_exposure=resolve_optional_ratio_env("IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE"),
ibit_zscore_exit_allow_outside_execution_window=resolve_optional_bool_env(
ibit_zscore_exit_allow_outside_execution_window=_optional_bool_env(
"IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW"
),
runtime_execution_window_trading_days=_runtime_execution_window_trading_days_env(
Expand Down Expand Up @@ -487,8 +487,14 @@ def _qqqi_income_ratio_env() -> float | None:


def _runtime_target_enabled_env() -> bool:
value = resolve_optional_bool_env("RUNTIME_TARGET_ENABLED")
return True if value is None else value
return resolve_optional_bool_env("RUNTIME_TARGET_ENABLED", default=True)


def _optional_bool_env(name: str) -> bool | None:
raw_value = os.getenv(f"QSL_{name}") or os.getenv(name)
if raw_value is None or str(raw_value).strip() == "":
return None
return resolve_optional_bool_env(name)


def _optional_non_negative_float_env(name: str) -> float | None:
Expand Down
28 changes: 20 additions & 8 deletions tests/test_runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
QPK_SRC = ROOT.parent / "QuantPlatformKit" / "src"
if (QPK_SRC / "quant_platform_kit" / "common" / "runtime_config.py").exists() and str(QPK_SRC) not in sys.path:
sys.path.insert(0, str(QPK_SRC))
UES_SRC = ROOT.parent / "UsEquityStrategies" / "src"
HES_SRC = ROOT.parent / "HkEquityStrategies" / "src"
for candidate in (QPK_SRC, UES_SRC, HES_SRC):
if candidate.exists() and str(candidate) not in sys.path:
sys.path.insert(0, str(candidate))
SCRIPT_PATH = ROOT / "scripts" / "print_strategy_profile_status.py"
SWITCH_PLAN_SCRIPT_PATH = ROOT / "scripts" / "print_strategy_switch_env_plan.py"

Expand Down Expand Up @@ -54,9 +57,13 @@
BASE_LONGBRIDGE_PROFILES = frozenset(
{
"global_etf_rotation",
"ibit_smart_dca",
"nasdaq_sp500_smart_dca",
"russell_top50_leader_rotation",
"tqqq_growth_income",
"soxl_soxx_trend_income",
"us_equity_combo",
"us_equity_combo_leveraged",
Comment on lines +65 to +66

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate combo profiles until their inputs are wired

Adding these combo profiles to the supported LongBridge set makes them appear in the status/sync flows, but the runtime adapter path still cannot supply their required inputs: us_equity_combo requires russell_snapshot and current_holdings, and us_equity_combo_leveraged requires market_data, while application/runtime_strategy_adapters.py only materializes market/benchmark/QQQ history, derived indicators, portfolio snapshots, and account state before calling build_strategy_evaluation_inputs. In any Cloud Run target selecting either combo profile, evaluation will be invoked without the required inputs and fail before producing a rebalance plan, so these should stay gated or the missing input plumbing should be added first.

Useful? React with 👍 / 👎.

}
)
OPTIONAL_LONGBRIDGE_PROFILES = frozenset({"global_etf_confidence_vol_gate"})
Expand Down Expand Up @@ -213,7 +220,7 @@ def test_load_platform_runtime_settings_prefers_runtime_target_json(self):

def test_load_platform_runtime_settings_requires_strategy_profile(self):
with patch.dict(os.environ, {}, clear=True):
with self.assertRaisesRegex(EnvironmentError, "RUNTIME_TARGET_JSON is required"):
with self.assertRaisesRegex(EnvironmentError, "RUNTIME_TARGET_JSON"):
load_platform_runtime_settings(project_id_resolver=lambda: "project-1")

def test_platform_supported_profiles_are_filtered_by_registry(self):
Expand Down Expand Up @@ -496,7 +503,7 @@ def test_income_layer_overrides_are_loaded_from_env(self):
self.assertEqual(settings.income_layer_start_usd, 250000.0)
self.assertEqual(settings.income_layer_max_ratio, 0.25)

def test_ibit_smart_dca_profile_is_rejected_on_longbridge(self):
def test_ibit_smart_dca_profile_is_loaded_on_longbridge(self):
with patch.dict(
os.environ,
{
Expand All @@ -510,8 +517,11 @@ def test_ibit_smart_dca_profile_is_rejected_on_longbridge(self):
},
clear=True,
):
with self.assertRaises(ValueError):
load_platform_runtime_settings(project_id_resolver=lambda: "project-1")
settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1")

self.assertEqual(settings.strategy_profile, "ibit_smart_dca")
self.assertTrue(settings.ibit_zscore_exit_enabled)
self.assertEqual(settings.ibit_zscore_exit_mode, "live")

def test_tech_runtime_execution_window_override_rejects_research_only_profile(self):
with patch.dict(
Expand Down Expand Up @@ -661,6 +671,7 @@ def test_platform_profile_status_matrix_matches_current_longbridge_rollout(self)
{
"canonical_profile": "soxl_soxx_trend_income",
"display_name": "SOXL/SOXX Semiconductor Trend Income",
"display_name_zh": "半导体趋势收益",
"domain": "us_equity",
"eligible": True,
"enabled": True,
Expand Down Expand Up @@ -690,6 +701,7 @@ def test_platform_profile_status_matrix_matches_current_longbridge_rollout(self)
{
"canonical_profile": "hk_global_etf_tactical_rotation",
"display_name": "HK Global ETF Tactical Rotation",
"display_name_zh": "港股ETF战术轮动",
"domain": "hk_equity",
"eligible": True,
"enabled": True,
Expand Down Expand Up @@ -819,7 +831,7 @@ def test_print_strategy_profile_status_json_matches_registry(self):
self.assertEqual(by_profile["russell_top50_leader_rotation"]["profile_group"], "snapshot_backed")
self.assertEqual(
by_profile["russell_top50_leader_rotation"]["display_name_zh"],
"罗素 Top50 领涨轮动",
"罗素Top50领涨",
)
self.assertEqual(by_profile["russell_top50_leader_rotation"]["input_mode"], "feature_snapshot")
self.assertTrue(by_profile["russell_top50_leader_rotation"]["requires_snapshot_artifacts"])
Expand Down Expand Up @@ -865,7 +877,7 @@ def test_print_strategy_profile_status_table_contains_expected_headers(self):
self.assertIn("HK Global ETF Tactical Rotation", result.stdout)
self.assertNotIn("HK Dividend-Gold Defensive Rotation", result.stdout)
self.assertIn("Russell Top50 Leader Rotation", result.stdout)
self.assertIn("罗素 Top50 领涨轮动", result.stdout)
self.assertIn("罗素Top50领涨", result.stdout)
self.assertNotIn("Tech/Communication Pullback Enhancement", result.stdout)
self.assertNotIn("hk_blue_chip_leader_rotation", result.stdout)
self.assertNotIn("hk_index_mean_reversion", result.stdout)
Expand Down
Loading