Skip to content

Commit 4900397

Browse files
Pigbibicodex
andauthored
Fix runtime settings defaults (#283)
Co-authored-by: Codex <noreply@openai.com>
1 parent 1c59341 commit 4900397

3 files changed

Lines changed: 63 additions & 19 deletions

File tree

notifications/telegram.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ def _break_telegram_market_symbol_auto_links(value) -> str:
233233
"strategy_name_russell_top50_leader_rotation": "罗素 Top50 领涨轮动",
234234
"strategy_name_nasdaq_sp500_smart_dca": "纳指100 / 标普500 智能定投",
235235
"strategy_name_ibit_smart_dca": "IBIT 比特币 ETF 智能定投",
236+
"strategy_name_us_equity_combo": "美股核心组合",
237+
"strategy_name_us_equity_combo_leveraged": "美股Alpha组合",
236238
"strategy_name_hk_global_etf_tactical_rotation": "港股全球 ETF 战术轮动",
237239
"strategy_name_hk_low_vol_dividend_quality_snapshot": "港股低波股息质量快照",
238240
"strategy_name_tqqq_growth_income": "TQQQ 增长收益",
@@ -447,6 +449,8 @@ def _break_telegram_market_symbol_auto_links(value) -> str:
447449
"strategy_name_russell_top50_leader_rotation": "Russell Top50 Leader Rotation",
448450
"strategy_name_nasdaq_sp500_smart_dca": "Nasdaq 100 / S&P 500 Smart DCA",
449451
"strategy_name_ibit_smart_dca": "IBIT Smart DCA",
452+
"strategy_name_us_equity_combo": "US Equity Combo",
453+
"strategy_name_us_equity_combo_leveraged": "US Equity Combo Leveraged",
450454
"strategy_name_hk_global_etf_tactical_rotation": "HK Global ETF Tactical Rotation",
451455
"strategy_name_hk_low_vol_dividend_quality_snapshot": "HK Low-Vol Dividend Quality Snapshot",
452456
"strategy_name_tqqq_growth_income": "TQQQ Growth Income",

runtime_config_support.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
resolve_optional_dca_mode_env,
1919
resolve_optional_ibit_zscore_exit_mode_env,
2020
resolve_optional_positive_float_env,
21-
resolve_optional_ratio_env,
2221
resolve_optional_symbol_env,
2322
resolve_split_env_list,
2423
resolve_strategy_runtime_path_settings,
@@ -430,12 +429,12 @@ def load_platform_runtime_settings(
430429
os.environ,
431430
platform_env_prefix="IBKR",
432431
),
433-
income_layer_enabled=resolve_optional_bool_env("INCOME_LAYER_ENABLED"),
432+
income_layer_enabled=resolve_optional_bool_env_value("INCOME_LAYER_ENABLED"),
434433
income_layer_start_usd=resolve_optional_non_negative_float_env("INCOME_LAYER_START_USD"),
435434
income_layer_max_ratio=resolve_optional_ratio_env("INCOME_LAYER_MAX_RATIO"),
436435
dca_mode=resolve_optional_dca_mode_env("DCA_MODE"),
437436
dca_base_investment_usd=resolve_optional_positive_float_env("DCA_BASE_INVESTMENT_USD"),
438-
ibit_zscore_exit_enabled=resolve_optional_bool_env("IBIT_ZSCORE_EXIT_ENABLED"),
437+
ibit_zscore_exit_enabled=resolve_optional_bool_env_value("IBIT_ZSCORE_EXIT_ENABLED"),
439438
ibit_zscore_exit_mode=resolve_optional_ibit_zscore_exit_mode_env("IBIT_ZSCORE_EXIT_MODE"),
440439
ibit_zscore_exit_parking_symbol=resolve_optional_symbol_env("IBIT_ZSCORE_EXIT_PARKING_SYMBOL"),
441440
ibit_zscore_exit_risk_reduced_exposure=resolve_optional_ratio_env(
@@ -444,7 +443,7 @@ def load_platform_runtime_settings(
444443
ibit_zscore_exit_risk_off_exposure=resolve_optional_ratio_env(
445444
"IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE"
446445
),
447-
ibit_zscore_exit_allow_outside_execution_window=resolve_optional_bool_env(
446+
ibit_zscore_exit_allow_outside_execution_window=resolve_optional_bool_env_value(
448447
"IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW"
449448
),
450449
market_signal_handoff_index_uri=first_non_empty(
@@ -621,9 +620,27 @@ def resolve_optional_non_negative_float_env(name: str) -> float | None:
621620
return resolve_non_negative_float_env(name, default=0.0)
622621

623622

623+
def resolve_optional_bool_env_value(name: str) -> bool | None:
624+
raw_value = os.getenv(f"QSL_{name}") or os.getenv(name)
625+
if raw_value is None or str(raw_value).strip() == "":
626+
return None
627+
return resolve_optional_bool_env(name)
628+
629+
630+
def resolve_optional_ratio_env(name: str, default: float | None = None) -> float | None:
631+
raw_value = os.getenv(f"QSL_{name}") or os.getenv(name)
632+
if raw_value is None or str(raw_value).strip() == "":
633+
return default
634+
value = float(raw_value)
635+
if not math.isfinite(value):
636+
raise ValueError(f"{name} must be finite, got {value}")
637+
if not 0.0 <= value <= 1.0:
638+
raise ValueError(f"{name} must be in [0,1], got {value}")
639+
return value
640+
641+
624642
def resolve_runtime_target_enabled_env() -> bool:
625-
value = resolve_optional_bool_env("RUNTIME_TARGET_ENABLED")
626-
return True if value is None else value
643+
return resolve_optional_bool_env("RUNTIME_TARGET_ENABLED", default=True)
627644

628645

629646
def resolve_account_group(raw_value: str | None) -> str:
@@ -709,6 +726,11 @@ def load_secret_payload(
709726
*,
710727
secret_client_factory: Callable[[], Any] | None = None,
711728
) -> str:
729+
if secret_client_factory is not None:
730+
client = secret_client_factory()
731+
name = f"projects/{project_id}/secrets/{secret_name}/versions/latest"
732+
response = client.access_secret_version(request={"name": name})
733+
return response.payload.data.decode("utf-8")
712734
return get_secret_store().get_secret(secret_name, project_id=project_id)
713735

714736

tests/test_runtime_config_support.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66

77
import pytest
88

9+
ROOT = Path(__file__).resolve().parents[1]
10+
QPK_SRC = ROOT.parent / "QuantPlatformKit" / "src"
11+
UES_SRC = ROOT.parent / "UsEquityStrategies" / "src"
12+
HES_SRC = ROOT.parent / "HkEquityStrategies" / "src"
13+
for candidate in (ROOT, QPK_SRC, UES_SRC, HES_SRC):
14+
if candidate.exists() and str(candidate) not in sys.path:
15+
sys.path.insert(0, str(candidate))
16+
917
from runtime_config_support import (
1018
DEFAULT_MARKET,
1119
DEFAULT_MARKET_CALENDAR,
@@ -59,9 +67,13 @@
5967
EXPECTED_IBKR_ENABLED_PROFILES = frozenset(
6068
{
6169
"global_etf_rotation",
70+
"ibit_smart_dca",
71+
"nasdaq_sp500_smart_dca",
6272
"russell_top50_leader_rotation",
6373
"soxl_soxx_trend_income",
6474
"tqqq_growth_income",
75+
"us_equity_combo",
76+
"us_equity_combo_leveraged",
6577
"hk_global_etf_tactical_rotation",
6678
"hk_low_vol_dividend_quality_snapshot",
6779
}
@@ -106,7 +118,7 @@ def test_load_platform_runtime_settings_requires_strategy_profile(monkeypatch):
106118
monkeypatch.setenv("IB_ACCOUNT_GROUP_CONFIG_JSON", MINIMAL_GROUP_JSON)
107119
monkeypatch.delenv("RUNTIME_TARGET_JSON", raising=False)
108120

109-
with pytest.raises(EnvironmentError, match="RUNTIME_TARGET_JSON is required"):
121+
with pytest.raises(EnvironmentError, match="RUNTIME_TARGET_JSON"):
110122
load_platform_runtime_settings(project_id_resolver=lambda: "project-1")
111123

112124

@@ -541,7 +553,7 @@ def test_load_platform_runtime_settings_reads_income_layer_overrides(monkeypatch
541553
assert settings.income_layer_max_ratio == 0.25
542554

543555

544-
def test_ibit_smart_dca_profile_is_rejected_on_ibkr(monkeypatch):
556+
def test_ibit_smart_dca_profile_is_loaded_on_ibkr(monkeypatch):
545557
monkeypatch.setenv("RUNTIME_TARGET_JSON", runtime_target_json("ibit_smart_dca"))
546558
monkeypatch.setenv("ACCOUNT_GROUP", "paper")
547559
monkeypatch.setenv("IB_ACCOUNT_GROUP_CONFIG_JSON", MINIMAL_GROUP_JSON)
@@ -552,8 +564,11 @@ def test_ibit_smart_dca_profile_is_rejected_on_ibkr(monkeypatch):
552564
monkeypatch.setenv("IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE", "0.25")
553565
monkeypatch.setenv("IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW", "true")
554566

555-
with pytest.raises(ValueError, match="Unsupported STRATEGY_PROFILE='ibit_smart_dca'"):
556-
load_platform_runtime_settings(project_id_resolver=lambda: "project-1")
567+
settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1")
568+
569+
assert settings.strategy_profile == "ibit_smart_dca"
570+
assert settings.ibit_zscore_exit_enabled is True
571+
assert settings.ibit_zscore_exit_mode == "live"
557572

558573

559574
def test_load_platform_runtime_settings_rejects_invalid_income_layer_enabled(monkeypatch):
@@ -723,13 +738,14 @@ def test_load_platform_runtime_settings_accepts_tqqq_growth_income(monkeypatch):
723738
assert settings.strategy_target_mode == "value"
724739

725740

726-
def test_load_platform_runtime_settings_rejects_nasdaq_sp500_smart_dca(monkeypatch):
741+
def test_load_platform_runtime_settings_accepts_nasdaq_sp500_smart_dca(monkeypatch):
727742
monkeypatch.setenv("RUNTIME_TARGET_JSON", runtime_target_json("nasdaq_sp500_smart_dca"))
728743
monkeypatch.setenv("ACCOUNT_GROUP", "paper")
729744
monkeypatch.setenv("IB_ACCOUNT_GROUP_CONFIG_JSON", MINIMAL_GROUP_JSON)
730745

731-
with pytest.raises(ValueError, match="Unsupported STRATEGY_PROFILE='nasdaq_sp500_smart_dca'"):
732-
load_platform_runtime_settings(project_id_resolver=lambda: "project-1")
746+
settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1")
747+
748+
assert settings.strategy_profile == "nasdaq_sp500_smart_dca"
733749

734750

735751
def test_load_platform_runtime_settings_rejects_legacy_qqq_tech_alias(monkeypatch):
@@ -758,6 +774,7 @@ def test_platform_profile_status_matrix_matches_current_ibkr_rollout():
758774
assert by_profile["global_etf_rotation"] == {
759775
"canonical_profile": "global_etf_rotation",
760776
"display_name": "Global ETF Rotation",
777+
"display_name_zh": "全球ETF轮动",
761778
"domain": "us_equity",
762779
"eligible": True,
763780
"enabled": True,
@@ -769,11 +786,12 @@ def test_platform_profile_status_matrix_matches_current_ibkr_rollout():
769786
assert by_profile["tqqq_growth_income"]["display_name"] == "TQQQ Growth Income"
770787
assert by_profile["tqqq_growth_income"]["eligible"] is True
771788
assert by_profile["tqqq_growth_income"]["enabled"] is True
772-
assert "nasdaq_sp500_smart_dca" not in by_profile
773-
assert "ibit_smart_dca" not in by_profile
789+
assert "nasdaq_sp500_smart_dca" in by_profile
790+
assert "ibit_smart_dca" in by_profile
774791
assert by_profile["hk_global_etf_tactical_rotation"] == {
775792
"canonical_profile": "hk_global_etf_tactical_rotation",
776793
"display_name": "HK Global ETF Tactical Rotation",
794+
"display_name_zh": "港股ETF战术轮动",
777795
"domain": "hk_equity",
778796
"eligible": True,
779797
"enabled": True,
@@ -814,11 +832,11 @@ def test_print_strategy_profile_status_json_matches_registry():
814832
assert by_profile["global_etf_rotation"]["requires_snapshot_artifacts"] is True
815833
assert by_profile["global_etf_rotation"]["requires_snapshot_manifest_path"] is True
816834
assert by_profile["global_etf_rotation"]["requires_strategy_config_path"] is False
817-
assert "nasdaq_sp500_smart_dca" not in by_profile
818-
assert "ibit_smart_dca" not in by_profile
835+
assert "nasdaq_sp500_smart_dca" in by_profile
836+
assert "ibit_smart_dca" in by_profile
819837
assert "tech_communication_pullback_enhancement" not in by_profile
820838
assert by_profile["russell_top50_leader_rotation"]["profile_group"] == "snapshot_backed"
821-
assert by_profile["russell_top50_leader_rotation"]["display_name_zh"] == "罗素 Top50 领涨轮动"
839+
assert by_profile["russell_top50_leader_rotation"]["display_name_zh"] == "罗素Top50领涨"
822840
assert by_profile["russell_top50_leader_rotation"]["input_mode"] == "feature_snapshot"
823841
assert by_profile["russell_top50_leader_rotation"]["requires_snapshot_artifacts"] is True
824842
assert by_profile["russell_top50_leader_rotation"]["requires_strategy_config_path"] is False
@@ -849,7 +867,7 @@ def test_print_strategy_profile_status_table_contains_expected_headers():
849867
assert "global_etf_rotation" in result.stdout
850868
assert "hk_global_etf_tactical_rotation" in result.stdout
851869
assert "Russell Top50 Leader Rotation" in result.stdout
852-
assert "罗素 Top50 领涨轮动" in result.stdout
870+
assert "罗素Top50领涨" in result.stdout
853871
assert "Tech/Communication Pullback Enhancement" not in result.stdout
854872
assert "HK Global ETF Tactical Rotation" in result.stdout
855873
assert "TQQQ Growth Income" in result.stdout

0 commit comments

Comments
 (0)