"""Unit tests for libs/backtest/manifests.py.""" from __future__ import annotations import json import tempfile from pathlib import Path import pytest from libs.backtest.domain import BacktestConfig, ExperimentManifest REGISTERED_ALIAS_SNAPSHOT_ID = "midlarge-liquid-long-v1_bucketfix_full_audit_tier3" REGISTERED_CANONICAL_SNAPSHOT_ID = "midlarge-liquid-long-v1_bucketfix_full_audit_canonical" def _write_json(path: Path, data: dict) -> None: path.write_text(json.dumps(data)) VALID_BASE_CONFIG = { "strategy_name": "test_strategy", "dataset_snapshot_id": REGISTERED_ALIAS_SNAPSHOT_ID, "universe": { "min_price": 5.0, "min_avg_dollar_volume": 1_000_000, "exclude_asset_types": [], }, "signal": { "score_threshold": 0.5, "max_candidates_per_day": 5, "execution_timing": "next_open", }, "risk": { "per_trade_risk_pct": 0.01, "max_daily_new_risk_pct": 0.03, "max_positions": 10, "max_positions_per_sector": 3, }, "execution": { "entry_fill_model": "next_open", "exit_fill_model": "daily_bar_approximation", "slippage_bps_base": 10.0, "same_bar_priority": "stop_first_conservative", }, "reporting": { "write_trade_blotter": True, "write_equity_curve": True, "write_metrics_summary": True, }, } class TestLoadBaseConfig: def test_load_valid_config(self, tmp_path): from libs.backtest.manifests import load_base_config cfg_file = tmp_path / "defaults.json" _write_json(cfg_file, VALID_BASE_CONFIG) loaded = load_base_config(cfg_file) assert loaded["strategy_name"] == "test_strategy" def test_missing_file_raises(self, tmp_path): from libs.backtest.manifests import load_base_config with pytest.raises(FileNotFoundError): load_base_config(tmp_path / "nonexistent.json") class TestDeepMerge: def test_simple_override(self): from libs.backtest.manifests import deep_merge base = {"a": 1, "b": 2} overrides = {"b": 99} merged = deep_merge(base, overrides) assert merged["a"] == 1 assert merged["b"] == 99 def test_nested_merge(self): from libs.backtest.manifests import deep_merge base = {"risk": {"max_positions": 10, "per_trade_risk_pct": 0.01}} overrides = {"risk": {"max_positions": 5}} merged = deep_merge(base, overrides) assert merged["risk"]["max_positions"] == 5 assert merged["risk"]["per_trade_risk_pct"] == 0.01 def test_does_not_mutate_base(self): from libs.backtest.manifests import deep_merge base = {"a": {"b": 1}} overrides = {"a": {"c": 2}} deep_merge(base, overrides) assert "c" not in base["a"] def test_override_wins(self): from libs.backtest.manifests import deep_merge merged = deep_merge({"x": 1}, {"x": 2}) assert merged["x"] == 2 class TestLoadManifest: def test_valid_manifest(self, tmp_path): from libs.backtest.manifests import load_manifest manifest_data = { "experiment_name": "test_exp", "dataset_snapshot_id": REGISTERED_ALIAS_SNAPSHOT_ID, "base_config": "configs/backtest/defaults.json", "overrides": {}, } f = tmp_path / "manifest.json" _write_json(f, manifest_data) m = load_manifest(f) assert m.experiment_name == "test_exp" def test_missing_file_raises(self, tmp_path): from libs.backtest.manifests import load_manifest with pytest.raises(FileNotFoundError): load_manifest(tmp_path / "nope.json") class TestResolveConfig: def test_basic_resolve(self, tmp_path): from libs.backtest.manifests import load_manifest, resolve_config cfg_file = tmp_path / "defaults.json" _write_json(cfg_file, VALID_BASE_CONFIG) manifest_data = { "experiment_name": "test", "dataset_snapshot_id": REGISTERED_ALIAS_SNAPSHOT_ID, "base_config": str(cfg_file), "overrides": {}, } m_file = tmp_path / "manifest.json" _write_json(m_file, manifest_data) manifest = load_manifest(m_file) config = resolve_config(manifest) assert isinstance(config, BacktestConfig) assert config.strategy_name == "test_strategy" assert config.dataset_snapshot_id == REGISTERED_CANONICAL_SNAPSHOT_ID assert config.requested_snapshot_id == REGISTERED_ALIAS_SNAPSHOT_ID assert config.canonical_snapshot_id == REGISTERED_CANONICAL_SNAPSHOT_ID def test_overrides_applied(self, tmp_path): from libs.backtest.manifests import load_manifest, resolve_config cfg_file = tmp_path / "defaults.json" _write_json(cfg_file, VALID_BASE_CONFIG) manifest_data = { "experiment_name": "test", "dataset_snapshot_id": REGISTERED_ALIAS_SNAPSHOT_ID, "base_config": str(cfg_file), "overrides": {"risk": {"max_positions": 3}}, } m_file = tmp_path / "manifest.json" _write_json(m_file, manifest_data) manifest = load_manifest(m_file) config = resolve_config(manifest) assert config.risk.max_positions == 3 assert config.risk.per_trade_risk_pct == 0.01 # from base def test_snapshot_id_override(self, tmp_path): from libs.backtest.manifests import load_manifest, resolve_config cfg_file = tmp_path / "defaults.json" _write_json(cfg_file, VALID_BASE_CONFIG) manifest_data = { "experiment_name": "test", "dataset_snapshot_id": REGISTERED_ALIAS_SNAPSHOT_ID, "base_config": str(cfg_file), "overrides": {}, } m_file = tmp_path / "manifest.json" _write_json(m_file, manifest_data) manifest = load_manifest(m_file) config = resolve_config( manifest, snapshot_id_override="midlarge-liquid-long-v1_bucketfix_full_audit_tier3tech", ) assert config.dataset_snapshot_id == REGISTERED_CANONICAL_SNAPSHOT_ID assert config.requested_snapshot_id == "midlarge-liquid-long-v1_bucketfix_full_audit_tier3tech" assert config.canonical_snapshot_id == REGISTERED_CANONICAL_SNAPSHOT_ID def test_strategy_engines_injected_from_manifest(self, tmp_path): from libs.backtest.manifests import load_manifest, resolve_config cfg_file = tmp_path / "defaults.json" _write_json(cfg_file, VALID_BASE_CONFIG) manifest_data = { "experiment_name": "test", "dataset_snapshot_id": REGISTERED_ALIAS_SNAPSHOT_ID, "base_config": str(cfg_file), "overrides": {}, "strategy_engines": [ { "engine_id": "earnings_same_day_short_v1", "event_types": ["earnings_release"], "timing_class": "same_day", "direction": "short_only", "entry_timing_policy": "next_open", "max_holding_days": 3, "engine_risk_budget_pct": 0.4, } ], } m_file = tmp_path / "manifest.json" _write_json(m_file, manifest_data) manifest = load_manifest(m_file) config = resolve_config(manifest) assert len(config.strategy_engines) == 1 assert config.strategy_engines[0].engine_id == "earnings_same_day_short_v1" def test_unknown_snapshot_id_raises(self, tmp_path): from libs.backtest.manifests import load_manifest, resolve_config cfg_file = tmp_path / "defaults.json" _write_json(cfg_file, VALID_BASE_CONFIG) manifest_data = { "experiment_name": "test", "dataset_snapshot_id": "snap_unknown_for_test", "base_config": str(cfg_file), "overrides": {}, } m_file = tmp_path / "manifest.json" _write_json(m_file, manifest_data) manifest = load_manifest(m_file) with pytest.raises(ValueError, match="Unknown snapshot id"): resolve_config(manifest) class TestGenerateRunId: def test_format(self): from libs.backtest.manifests import generate_run_id cfg = BacktestConfig(strategy_name="my_strategy", dataset_snapshot_id="snap_2026_01_01") run_id = generate_run_id(cfg) assert run_id.startswith("bt_") parts = run_id.split("_") assert len(parts) >= 4 def test_deterministic_for_same_config(self): """Two calls with same config at same time should have same hash suffix.""" from libs.backtest.manifests import generate_run_id cfg = BacktestConfig(strategy_name="test", dataset_snapshot_id="snap_001") id1 = generate_run_id(cfg) id2 = generate_run_id(cfg) # Hash suffix should be identical assert id1.split("_")[-1] == id2.split("_")[-1]