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
11 changes: 11 additions & 0 deletions src/ml4t/backtest/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)]
Expand Down Expand Up @@ -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):
Expand Down
21 changes: 15 additions & 6 deletions tests/test_config_wiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -1021,24 +1021,33 @@ 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}"
Comment thread
stefan-jansen marked this conversation as resolved.

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]
}
)

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:
Expand Down