diff --git a/src/ml4t/backtest/config.py b/src/ml4t/backtest/config.py index 6d5f6c1..d7d110e 100644 --- a/src/ml4t/backtest/config.py +++ b/src/ml4t/backtest/config.py @@ -288,6 +288,9 @@ def _coerce_margin_schedule( return None coerced: dict[str, Any] = {} for asset, value in schedule.items(): + if not isinstance(value, list | tuple): + coerced[asset] = value + continue try: initial, maintenance = value coerced[asset] = (float(initial), float(maintenance)) @@ -303,6 +306,9 @@ def _margin_schedule_to_dict( return None serialized: dict[str, Any] = {} for asset, value in schedule.items(): + if not isinstance(value, list | tuple): + serialized[asset] = _serialize_invalid_margin_scalar(value) + continue try: initial, maintenance = value serialized[asset] = [float(initial), float(maintenance)] @@ -331,6 +337,11 @@ def _validate_margin_pct_schedule( if schedule is None: return issues for asset, value in schedule.items(): + if not isinstance(value, list | tuple): + issues.append( + f"margin_pct_schedule[{asset!r}] must be a two-item (initial, maintenance) sequence" + ) + continue try: initial, maintenance = value except (TypeError, ValueError): diff --git a/tests/test_config_wiring.py b/tests/test_config_wiring.py index 6289383..c7923d0 100644 --- a/tests/test_config_wiring.py +++ b/tests/test_config_wiring.py @@ -1021,15 +1021,23 @@ def test_invalid_margin_pct_schedule_export_does_not_raise(self): assert data["account"]["margin_pct_schedule"] == {"ES": [0.05]} def test_invalid_margin_pct_schedule_export_keeps_non_sequences_scalar(self): - def values(): - yield "initial" - yield "maintenance" - yield "extra" + class CountingIterator: + def __init__(self): + self.read_count = 0 + + def __iter__(self): + return self + + def __next__(self): + self.read_count += 1 + return f"value-{self.read_count}" + + iterator = CountingIterator() config = BacktestConfig( margin_pct_schedule={ "DICT": {"initial": 0.05, "maintenance": 0.035}, # type: ignore[dict-item] - "GEN": values(), # type: ignore[dict-item] + "ITER": iterator, # type: ignore[dict-item] "STR": "im", # type: ignore[dict-item] } ) @@ -1037,8 +1045,9 @@ def values(): data = config.to_dict() assert isinstance(data["account"]["margin_pct_schedule"]["DICT"], str) - assert isinstance(data["account"]["margin_pct_schedule"]["GEN"], str) + assert isinstance(data["account"]["margin_pct_schedule"]["ITER"], str) assert data["account"]["margin_pct_schedule"]["STR"] == "im" + assert iterator.read_count == 0 def test_margin_pct_schedule_float_overflow_is_reported(self, tmp_path): class OverflowFloat: