Files
2026-07-13 13:26:28 +08:00

104 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for ``case_studies.utils.backtest_loaders.warmup_periods_for`` +
``_calendar_days_per_period`` — the helpers that replaced the hardcoded
``warmup_periods=126`` constant duplicated across 16 call-sites in 5 CSes.
These tests close P2.8 of the roborev cleanup (review #2510 / #2511).
"""
from __future__ import annotations
import yaml
from case_studies.utils.backtest_loaders import (
_calendar_days_per_period,
_load_case_setup_yaml,
warmup_periods_for,
)
# The expected per-CS warmup is the max over ``execution.allocator_lookback``
# and any per-sweep allocator ``vol_window`` / ``lookback`` overrides. These
# expectations are anchored on the current setup.yaml values; if a CS
# tunes its allocator lookbacks, update the expected value here.
_EXPECTED_WARMUP: dict[str, int] = {
"etfs": 63,
"crypto_perps_funding": 240,
"nasdaq100_microstructure": 520,
"us_equities_panel": 126, # mvo_ledoit_wolf lookback=126 > allocator_lookback=63
"us_firm_characteristics": 12,
"fx_pairs": 63,
"cme_futures": 63,
"sp500_options": 63,
"sp500_equity_option_analytics": 126, # mvo_ledoit_wolf lookback=126
}
def test_warmup_periods_for_matches_setup_yaml() -> None:
for cs, expected in _EXPECTED_WARMUP.items():
actual = warmup_periods_for(cs)
assert actual == expected, (
f"warmup_periods_for({cs}) = {actual}, expected {expected} "
f"(max of execution.allocator_lookback + sweep allocator overrides)"
)
def test_warmup_periods_for_unknown_returns_zero(tmp_path) -> None:
# No setup.yaml → defaults to 0 (the unbounded fallback inside
# load_backtest_prices_for then skips the prefix-day computation).
assert warmup_periods_for("__nonexistent_cs__") == 0
def test_warmup_periods_for_picks_max_over_overrides(tmp_path, monkeypatch) -> None:
"""When a per-allocator override exceeds allocator_lookback, the helper
must surface the override rather than the CS-level default."""
fake_setup = {
"execution": {"allocator_lookback": 50},
"backtest": {
"sweep": {
"allocators": [
{"name": "equal_weight"},
{"name": "iv", "vol_window": 200},
{"name": "mvo_lw", "lookback": 100},
]
}
},
}
cs_dir = tmp_path / "fake_cs" / "config"
cs_dir.mkdir(parents=True)
(cs_dir / "setup.yaml").write_text(yaml.safe_dump(fake_setup))
# Drop the cache so the synthetic CS gets a fresh read.
_load_case_setup_yaml.cache_clear()
from case_studies.utils.backtest_loaders import warmup_periods_for as wpf
from utils.paths import get_case_study_dir as orig_get_dir
def fake_get_dir(cs: str):
if cs == "fake_cs":
return tmp_path / "fake_cs"
return orig_get_dir(cs)
monkeypatch.setattr("case_studies.utils.backtest_loaders.get_case_study_dir", fake_get_dir)
_load_case_setup_yaml.cache_clear()
assert wpf("fake_cs") == 200
def test_calendar_days_per_period_cadence_aware() -> None:
# Daily NYSE cadence: 1.5× (weekend + holiday allowance)
assert abs(_calendar_days_per_period("fx_pairs") - 1.5) < 1e-9
assert abs(_calendar_days_per_period("us_equities_panel") - 1.5) < 1e-9
# Weekly cadence: 7 calendar days per bar
assert abs(_calendar_days_per_period("cme_futures") - 7.0) < 1e-9
assert abs(_calendar_days_per_period("sp500_equity_option_analytics") - 7.0) < 1e-9
# 8-hour cadence: ~0.333 day per bar (3 bars / 24h day)
assert abs(_calendar_days_per_period("crypto_perps_funding") - 1.0 / 3.0) < 1e-9
# 15-minute cadence: ~0.054 day per bar (1/26 trading day × 1.4 calendar buffer)
assert _calendar_days_per_period("nasdaq100_microstructure") < 0.1
# Monthly cadence: ~31 calendar days per bar
assert abs(_calendar_days_per_period("us_firm_characteristics") - 31.0) < 1e-9
def test_calendar_days_per_period_default_for_unknown_cs() -> None:
# Falls back to the daily 1.5× heuristic when no setup.yaml is present
# or the cadence token isn't in the lookup table.
assert _calendar_days_per_period("__nonexistent_cs__") == 1.5