diff --git a/src/quant_platform_kit/common/runtime_config.py b/src/quant_platform_kit/common/runtime_config.py index 907bd271..ad93034f 100644 --- a/src/quant_platform_kit/common/runtime_config.py +++ b/src/quant_platform_kit/common/runtime_config.py @@ -39,6 +39,38 @@ def resolve_bool_value(raw_value: str | None) -> bool: return str(raw_value or "").strip().lower() in {"1", "true", "yes", "y", "on"} +def resolve_optional_bool_env( + env: Mapping[str, str | None], + name: str, +) -> bool | None: + raw_value = env.get(name) + if raw_value is None or str(raw_value).strip() == "": + return None + return resolve_bool_value(raw_value) + + +def resolve_cash_only_execution_env( + env: Mapping[str, str | None], + *, + platform_env_prefix: str | None = None, + legacy_name: str = "CASH_ONLY_EXECUTION", + default: bool = True, +) -> bool: + """Resolve cash-only execution with platform-specific override precedence.""" + candidates: list[str] = [] + prefix = str(platform_env_prefix or "").strip().upper() + if prefix: + candidates.append(f"{prefix}_CASH_ONLY_EXECUTION") + legacy = str(legacy_name or "").strip() + if legacy: + candidates.append(legacy) + for name in candidates: + value = resolve_optional_bool_env(env, name) + if value is not None: + return value + return default + + def resolve_dry_run_env( env: Mapping[str, str | None], name: str, diff --git a/tests/test_runtime_config.py b/tests/test_runtime_config.py index 234f4325..4acdaaf6 100644 --- a/tests/test_runtime_config.py +++ b/tests/test_runtime_config.py @@ -7,8 +7,10 @@ from quant_platform_kit.common.runtime_config import ( first_non_empty, resolve_bool_value, + resolve_cash_only_execution_env, resolve_dry_run_env, resolve_float_env, + resolve_optional_bool_env, resolve_optional_float_env, resolve_quantity_step_env, resolve_strategy_config_path, @@ -200,6 +202,28 @@ def test_resolve_strategy_runtime_path_settings_prefers_env_over_derived_paths(s self.assertEqual(settings.strategy_config_source, "env") self.assertIsNone(settings.reconciliation_output_path) + def test_resolve_cash_only_execution_env_prefers_platform_override(self) -> None: + env = { + "CASH_ONLY_EXECUTION": "false", + "IBKR_CASH_ONLY_EXECUTION": "true", + "SCHWAB_CASH_ONLY_EXECUTION": "false", + } + self.assertTrue( + resolve_cash_only_execution_env(env, platform_env_prefix="IBKR") + ) + self.assertFalse( + resolve_cash_only_execution_env(env, platform_env_prefix="SCHWAB") + ) + self.assertFalse(resolve_cash_only_execution_env(env)) + self.assertTrue( + resolve_cash_only_execution_env({}, platform_env_prefix="IBKR") + ) + + def test_resolve_optional_bool_env_treats_blank_as_unset(self) -> None: + self.assertIsNone(resolve_optional_bool_env({"FLAG": ""}, "FLAG")) + self.assertIsNone(resolve_optional_bool_env({}, "FLAG")) + self.assertFalse(resolve_optional_bool_env({"FLAG": "false"}, "FLAG")) + if __name__ == "__main__": unittest.main()