diff --git a/src/ml4t/backtest/config.py b/src/ml4t/backtest/config.py index 4daffdc..e7007b5 100644 --- a/src/ml4t/backtest/config.py +++ b/src/ml4t/backtest/config.py @@ -291,7 +291,7 @@ def _coerce_margin_schedule( try: initial, maintenance = value coerced[asset] = (float(initial), float(maintenance)) - except (TypeError, ValueError): + except (OverflowError, TypeError, ValueError): coerced[asset] = value return coerced @@ -306,11 +306,26 @@ def _margin_schedule_to_dict( try: initial, maintenance = value serialized[asset] = [float(initial), float(maintenance)] - except (TypeError, ValueError): - serialized[asset] = serialize_artifact_value(value) + except (OverflowError, TypeError, ValueError): + serialized[asset] = _serialize_invalid_margin_schedule_value(value) return serialized +def _serialize_invalid_margin_schedule_value(value: Any) -> Any: + try: + values = list(value) + except TypeError: + return _serialize_invalid_margin_scalar(value) + return [_serialize_invalid_margin_scalar(item) for item in values] + + +def _serialize_invalid_margin_scalar(value: Any) -> Any: + serialized = serialize_artifact_value(value) + if isinstance(serialized, str | int | float | bool) or serialized is None: + return serialized + return repr(serialized) + + def _validate_margin_pct_schedule( schedule: dict[str, tuple[float, float]] | None, ) -> list[str]: @@ -328,7 +343,7 @@ def _validate_margin_pct_schedule( try: initial = float(initial) maintenance = float(maintenance) - except (TypeError, ValueError): + except (OverflowError, TypeError, ValueError): issues.append(f"margin_pct_schedule[{asset!r}] values must be numeric") continue if not 0.0 < initial <= 1.0: diff --git a/tests/test_config_wiring.py b/tests/test_config_wiring.py index d00d348..0e987d5 100644 --- a/tests/test_config_wiring.py +++ b/tests/test_config_wiring.py @@ -1020,6 +1020,22 @@ def test_invalid_margin_pct_schedule_export_does_not_raise(self): data = config.to_dict() assert data["account"]["margin_pct_schedule"] == {"ES": [0.05]} + def test_margin_pct_schedule_float_overflow_is_reported(self, tmp_path): + class OverflowFloat: + def __float__(self): + raise OverflowError("too large") + + config = BacktestConfig( + margin_pct_schedule={"ES": (OverflowFloat(), 0.05)} # type: ignore[dict-item] + ) + + issues = config.validate(warn=False) + data = config.to_dict() + + assert any("values must be numeric" in issue for issue in issues) + assert isinstance(data["account"]["margin_pct_schedule"]["ES"][0], str) + config.to_yaml(tmp_path / "invalid.yaml") + def test_validate_rejects_invalid_margin_pct_rates(self): config = BacktestConfig(margin_pct_schedule={"ES": (0.03, 0.05)}) issues = config.validate(warn=False)