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
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
flask
gunicorn
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
hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@4007746ac21379f7ce7cf8e999d2bb37123f6767
pandas
requests
Expand Down
32 changes: 32 additions & 0 deletions runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,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
Expand Down Expand Up @@ -291,6 +293,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"),
Comment on lines +296 to +297

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 Wire DCA overrides into Cloud Run env sync

When these new DCA_MODE / DCA_BASE_INVESTMENT_USD settings are configured as GitHub environment variables and the standard .github/workflows/sync-cloud-run-env.yml workflow is used, they are never passed to Cloud Run: the workflow's optional override env block currently only exposes the income vars at lines 156-161, and the update/remove block only syncs those income vars at lines 934-955. As a result the new fields remain None in managed deployments unless operators manually edit the Cloud Run service outside the repo workflow, so the DCA runtime controls added here have no effect in the normal deployment path.

Useful? React with 👍 / 👎.

runtime_execution_window_trading_days=_runtime_execution_window_trading_days_env(
strategy_definition.profile
),
Expand Down Expand Up @@ -413,6 +417,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:
Expand Down
18 changes: 18 additions & 0 deletions strategy_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@


_FEATURE_SNAPSHOT_INPUT = "feature_snapshot"
DCA_PROFILES = frozenset({"nasdaq_sp500_smart_dca", "ibit_smart_dca"})


@dataclass(frozen=True)
Expand Down Expand Up @@ -175,6 +176,7 @@ 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
_apply_dca_runtime_overrides(profile, runtime_settings, overrides)
if profile == "tqqq_growth_income":
if runtime_settings.income_threshold_usd is not None:
overrides["income_threshold_usd"] = runtime_settings.income_threshold_usd
Expand All @@ -188,6 +190,22 @@ def _build_runtime_overrides(profile: str, runtime_settings: PlatformRuntimeSett
return overrides


def _apply_dca_runtime_overrides(
profile: str,
runtime_settings: PlatformRuntimeSettings,
overrides: dict[str, Any],
) -> None:
if profile not in DCA_PROFILES:
return
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


def load_strategy_runtime(
raw_profile: str | None,
*,
Expand Down
49 changes: 49 additions & 0 deletions tests/test_strategy_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ def evaluate(self, ctx):
return StrategyDecision(diagnostics={"signal_description": "tqqq"})


class _NasdaqSp500DcaEntrypoint:
manifest = StrategyManifest(
profile="nasdaq_sp500_smart_dca",
domain="us_equity",
display_name="Nasdaq 100 / S&P 500 DCA",
description="test entrypoint",
required_inputs=frozenset({"market_history", "portfolio_snapshot"}),
default_config={
"managed_symbols": ("QQQM", "SPLG"),
"investment_amount_mode": "fixed",
"smart_multiplier_enabled": False,
"base_investment_usd": 1000.0,
},
)

def evaluate(self, ctx):
self.ctx = ctx
return StrategyDecision(diagnostics={"signal_description": "dca"})


class _SemiconductorEntrypoint:
def __init__(self):
self.manifest = StrategyManifest(
Expand Down Expand Up @@ -103,6 +123,8 @@ def _build_runtime_settings(
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,
) -> PlatformRuntimeSettings:
return PlatformRuntimeSettings(
Expand All @@ -124,6 +146,8 @@ def _build_runtime_settings(
income_layer_enabled=income_layer_enabled,
income_layer_start_usd=income_layer_start_usd,
income_layer_max_ratio=income_layer_max_ratio,
dca_mode=dca_mode,
dca_base_investment_usd=dca_base_investment_usd,
runtime_execution_window_trading_days=runtime_execution_window_trading_days,
feature_snapshot_path=feature_snapshot_path,
feature_snapshot_manifest_path=None,
Expand Down Expand Up @@ -280,6 +304,31 @@ def test_load_strategy_runtime_applies_tqqq_income_overrides_from_settings(self)
self.assertEqual(runtime.merged_runtime_config["income_layer_start_usd"], 250000.0)
self.assertEqual(runtime.merged_runtime_config["income_layer_max_ratio"], 0.25)

def test_load_strategy_runtime_applies_dca_overrides_from_settings(self):
entrypoint = _NasdaqSp500DcaEntrypoint()

with patch.object(strategy_runtime_module, "load_strategy_entrypoint_for_profile", return_value=entrypoint):
with patch.object(
strategy_runtime_module,
"load_strategy_runtime_adapter_for_profile",
return_value=StrategyRuntimeAdapter(portfolio_input_name="portfolio_snapshot"),
):
runtime = strategy_runtime_module.load_strategy_runtime(
"nasdaq_sp500_smart_dca",
runtime_settings=_build_runtime_settings(
"nasdaq_sp500_smart_dca",
dca_mode="smart",
dca_base_investment_usd=500.0,
),
)

self.assertEqual(runtime.runtime_overrides["investment_amount_mode"], "fixed")
self.assertTrue(runtime.runtime_overrides["smart_multiplier_enabled"])
self.assertEqual(runtime.runtime_overrides["base_investment_usd"], 500.0)
self.assertEqual(runtime.merged_runtime_config["investment_amount_mode"], "fixed")
self.assertTrue(runtime.merged_runtime_config["smart_multiplier_enabled"])
self.assertEqual(runtime.merged_runtime_config["base_investment_usd"], 500.0)

def test_load_strategy_runtime_applies_tech_execution_window_overrides_from_settings(self):
entrypoint = _TechEntrypoint()

Expand Down