Skip to content

Commit 6a330b4

Browse files
authored
Add DCA runtime settings (#183)
1 parent 2c7da05 commit 6a330b4

4 files changed

Lines changed: 79 additions & 1 deletion

File tree

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
flask
22
gunicorn
33
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b846c9d777a450e95d23c264853997d671f47dd9
4-
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@22689494e922a8b18349562edcc6389d2faaed8f
4+
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@361338f60900182e3be535cd5fd2be2b9a07b422
55
hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@4007746ac21379f7ce7cf8e999d2bb37123f6767
66
pandas
77
numpy

runtime_config_support.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ class PlatformRuntimeSettings:
176176
income_layer_enabled: bool | None = None
177177
income_layer_start_usd: float | None = None
178178
income_layer_max_ratio: float | None = None
179+
dca_mode: str | None = None
180+
dca_base_investment_usd: float | None = None
179181
account_group: str = DEFAULT_ACCOUNT_GROUP
180182
service_name: str | None = None
181183
account_ids: tuple[str, ...] = ()
@@ -378,6 +380,8 @@ def load_platform_runtime_settings(
378380
income_layer_enabled=resolve_optional_bool_env("INCOME_LAYER_ENABLED"),
379381
income_layer_start_usd=resolve_optional_non_negative_float_env("INCOME_LAYER_START_USD"),
380382
income_layer_max_ratio=resolve_optional_ratio_env("INCOME_LAYER_MAX_RATIO"),
383+
dca_mode=resolve_optional_dca_mode_env("DCA_MODE"),
384+
dca_base_investment_usd=resolve_optional_positive_float_env("DCA_BASE_INVESTMENT_USD"),
381385
account_group=account_group,
382386
service_name=group_config.service_name,
383387
account_ids=group_config.account_ids,
@@ -519,6 +523,33 @@ def resolve_optional_non_negative_float_env(name: str) -> float | None:
519523
return resolve_non_negative_float_env(name, default=0.0)
520524

521525

526+
def resolve_optional_positive_float_env(name: str) -> float | None:
527+
raw_value = os.getenv(name)
528+
if raw_value is None or str(raw_value).strip() == "":
529+
return None
530+
value = resolve_non_negative_float_env(name, default=0.0)
531+
if value <= 0.0:
532+
raise ValueError(f"{name} must be positive, got {value}")
533+
return value
534+
535+
536+
def resolve_optional_dca_mode_env(name: str) -> str | None:
537+
raw_value = os.getenv(name)
538+
if raw_value is None or str(raw_value).strip() == "":
539+
return None
540+
value = str(raw_value).strip().lower()
541+
aliases = {
542+
"ordinary": "fixed",
543+
"ordinary_dca": "fixed",
544+
"fixed_dca": "fixed",
545+
"smart_dca": "smart",
546+
}
547+
mode = aliases.get(value, value)
548+
if mode not in {"fixed", "smart"}:
549+
raise ValueError(f"{name} must be fixed or smart, got {raw_value!r}")
550+
return mode
551+
552+
522553
def resolve_optional_bool_env(name: str) -> bool | None:
523554
raw_value = os.getenv(name)
524555
if raw_value is None or str(raw_value).strip() == "":

strategy_runtime.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
DEFAULT_CASH_RESERVE_RATIO = 0.0
4444
DEFAULT_REBALANCE_THRESHOLD_RATIO = 0.02
4545
_FEATURE_SNAPSHOT_INPUT = "feature_snapshot"
46+
DCA_PROFILES = frozenset({"nasdaq_sp500_smart_dca", "ibit_smart_dca"})
4647
_MARKET_HISTORY_INPUT = "market_history"
4748
_BENCHMARK_HISTORY_INPUT = "benchmark_history"
4849
_DERIVED_INDICATORS_INPUT = "derived_indicators"
@@ -929,4 +930,20 @@ def _build_runtime_overrides(runtime_settings: PlatformRuntimeSettings) -> dict[
929930
overrides["income_layer_start_usd"] = income_layer_start_usd
930931
if income_layer_max_ratio is not None:
931932
overrides["income_layer_max_ratio"] = income_layer_max_ratio
933+
_apply_dca_runtime_overrides(runtime_settings, overrides)
932934
return overrides
935+
936+
937+
def _apply_dca_runtime_overrides(
938+
runtime_settings: PlatformRuntimeSettings,
939+
overrides: dict[str, Any],
940+
) -> None:
941+
if runtime_settings.strategy_profile not in DCA_PROFILES:
942+
return
943+
dca_mode = getattr(runtime_settings, "dca_mode", None)
944+
dca_base_investment_usd = getattr(runtime_settings, "dca_base_investment_usd", None)
945+
if dca_mode is not None:
946+
overrides["investment_amount_mode"] = "fixed"
947+
overrides["smart_multiplier_enabled"] = dca_mode == "smart"
948+
if dca_base_investment_usd is not None:
949+
overrides["base_investment_usd"] = dca_base_investment_usd

tests/test_strategy_runtime.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ def _build_runtime_settings(
3333
income_layer_enabled: bool | None = None,
3434
income_layer_start_usd: float | None = None,
3535
income_layer_max_ratio: float | None = None,
36+
dca_mode: str | None = None,
37+
dca_base_investment_usd: float | None = None,
3638
) -> PlatformRuntimeSettings:
3739
return PlatformRuntimeSettings(
3840
project_id=None,
@@ -57,6 +59,8 @@ def _build_runtime_settings(
5759
income_layer_enabled=income_layer_enabled,
5860
income_layer_start_usd=income_layer_start_usd,
5961
income_layer_max_ratio=income_layer_max_ratio,
62+
dca_mode=dca_mode,
63+
dca_base_investment_usd=dca_base_investment_usd,
6064
account_group="default",
6165
service_name=None,
6266
account_ids=(),
@@ -299,6 +303,32 @@ def evaluate(self, ctx):
299303
assert runtime.status_icon == "🧲"
300304

301305

306+
def test_dca_overrides_apply_to_runtime_config():
307+
settings = _build_runtime_settings(
308+
profile="nasdaq_sp500_smart_dca",
309+
display_name="Nasdaq 100 / S&P 500 DCA",
310+
target_mode="value",
311+
dca_mode="smart",
312+
dca_base_investment_usd=500.0,
313+
)
314+
315+
assert strategy_runtime_module._build_runtime_overrides(settings) == {
316+
"investment_amount_mode": "fixed",
317+
"smart_multiplier_enabled": True,
318+
"base_investment_usd": 500.0,
319+
}
320+
321+
322+
def test_dca_overrides_are_ignored_for_non_dca_profiles():
323+
settings = _build_runtime_settings(
324+
profile="tqqq_growth_income",
325+
dca_mode="smart",
326+
dca_base_investment_usd=500.0,
327+
)
328+
329+
assert strategy_runtime_module._build_runtime_overrides(settings) == {}
330+
331+
302332
def test_option_chain_fetch_uses_merged_manifest_overlay_defaults(monkeypatch):
303333
observed = {}
304334

0 commit comments

Comments
 (0)