From e7a17fa36d73b935c7289fd9f9f876b707a91336 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:56:03 +0800 Subject: [PATCH] Add DCA runtime settings --- pyproject.toml | 2 +- requirements.txt | 2 +- runtime_config_support.py | 32 ++++++++++++++++++++++++++++++++ strategy_runtime.py | 9 +++++++++ tests/test_strategy_runtime.py | 20 ++++++++++++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d7ca037..99b6d08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ authors = [ dependencies = [ "firstrade==0.0.39", "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b846c9d777a450e95d23c264853997d671f47dd9", - "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@d08492ea4b4055515606ae386e59a31a943a7fec", + "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@361338f60900182e3be535cd5fd2be2b9a07b422", "google-cloud-storage", "requests", ] diff --git a/requirements.txt b/requirements.txt index 820161a..6b346b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ flask gunicorn firstrade==0.0.39 quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b846c9d777a450e95d23c264853997d671f47dd9 -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@22689494e922a8b18349562edcc6389d2faaed8f +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@361338f60900182e3be535cd5fd2be2b9a07b422 google-cloud-storage requests pytest diff --git a/runtime_config_support.py b/runtime_config_support.py index b605f8a..d1c72e7 100644 --- a/runtime_config_support.py +++ b/runtime_config_support.py @@ -56,6 +56,8 @@ class PlatformRuntimeSettings: income_layer_enabled: bool | None = None income_layer_start_usd: float | None = None income_layer_max_ratio: float | None = None + dca_mode: str | None = None + dca_base_investment_usd: float | None = None runtime_execution_window_trading_days: int | None = None feature_snapshot_path: str | None = None feature_snapshot_manifest_path: str | None = None @@ -172,6 +174,8 @@ def load_platform_runtime_settings( 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=_optional_ratio_env("INCOME_LAYER_MAX_RATIO"), + dca_mode=_optional_dca_mode_env("DCA_MODE"), + dca_base_investment_usd=_optional_positive_float_env("DCA_BASE_INVESTMENT_USD"), runtime_execution_window_trading_days=_runtime_execution_window_trading_days_env( strategy_definition.profile ), @@ -314,6 +318,34 @@ def _optional_non_negative_float_env(name: str) -> float | None: return float(value) +def _optional_positive_float_env(name: str) -> float | None: + value = resolve_optional_float_env(os.environ, name) + if value is None: + return None + if not math.isfinite(value): + raise ValueError(f"{name} must be finite, got {value}") + if value <= 0: + raise ValueError(f"{name} must be positive, got {value}") + return float(value) + + +def _optional_dca_mode_env(name: str) -> str | None: + raw_value = os.getenv(name) + if raw_value is None or str(raw_value).strip() == "": + return None + value = str(raw_value).strip().lower() + aliases = { + "ordinary": "fixed", + "ordinary_dca": "fixed", + "fixed_dca": "fixed", + "smart_dca": "smart", + } + mode = aliases.get(value, value) + if mode not in {"fixed", "smart"}: + raise ValueError(f"{name} must be fixed or smart, got {raw_value!r}") + return mode + + def _resolve_non_negative_float_env(name: str, *, default: float) -> float: value = resolve_optional_float_env(os.environ, name) if value is None: diff --git a/strategy_runtime.py b/strategy_runtime.py index ec5a5bd..2a33ca0 100644 --- a/strategy_runtime.py +++ b/strategy_runtime.py @@ -24,6 +24,7 @@ ) _FEATURE_SNAPSHOT_INPUT = "feature_snapshot" +DCA_PROFILES = frozenset({"nasdaq_sp500_smart_dca", "ibit_smart_dca"}) @dataclass(frozen=True) @@ -152,6 +153,14 @@ def _build_runtime_overrides(profile: str, runtime_settings: PlatformRuntimeSett overrides["income_layer_start_usd"] = income_layer_start_usd if income_layer_max_ratio is not None: overrides["income_layer_max_ratio"] = income_layer_max_ratio + if profile in DCA_PROFILES: + dca_mode = getattr(runtime_settings, "dca_mode", None) + dca_base_investment_usd = getattr(runtime_settings, "dca_base_investment_usd", None) + if dca_mode is not None: + overrides["investment_amount_mode"] = "fixed" + overrides["smart_multiplier_enabled"] = dca_mode == "smart" + if dca_base_investment_usd is not None: + overrides["base_investment_usd"] = dca_base_investment_usd if profile == "tqqq_growth_income": if runtime_settings.income_threshold_usd is not None: overrides["income_threshold_usd"] = runtime_settings.income_threshold_usd diff --git a/tests/test_strategy_runtime.py b/tests/test_strategy_runtime.py index eb7e565..173a15a 100644 --- a/tests/test_strategy_runtime.py +++ b/tests/test_strategy_runtime.py @@ -61,3 +61,23 @@ def test_income_layer_overrides_apply_to_runtime_config(): "income_layer_start_usd": 250000.0, "income_layer_max_ratio": 0.25, } + + +def test_dca_overrides_apply_to_runtime_config(): + settings = _runtime_settings( + strategy_profile="nasdaq_sp500_smart_dca", + dca_mode="smart", + dca_base_investment_usd=500.0, + ) + + assert _build_runtime_overrides("nasdaq_sp500_smart_dca", settings) == { + "investment_amount_mode": "fixed", + "smart_multiplier_enabled": True, + "base_investment_usd": 500.0, + } + + +def test_dca_overrides_ignore_non_dca_profiles(): + settings = _runtime_settings(dca_mode="smart", dca_base_investment_usd=500.0) + + assert _build_runtime_overrides("global_etf_rotation", settings) == {}