import hashlib import json import tempfile import unittest from datetime import datetime from pathlib import Path from quant60.backtest import BacktestEngine, verify_artifact_manifest from quant60.cli import main from quant60.config import config_from_dict, load_config from quant60.ledger import HashChainLedger from quant60.synthetic import generate_synthetic_bars PROJECT = Path(__file__).resolve().parents[1] class BacktestCliTests(unittest.TestCase): def test_empty_config_uses_valid_defaults(self): config = config_from_dict({}) config.validate() def test_non_finite_config_values_are_rejected(self): cases = ( {"initial_cash": "NaN"}, {"fees": {"commission_rate": float("inf")}}, {"execution": {"participation_rate": float("nan")}}, {"execution": {"slippage_bps": float("inf")}}, {"strategy": {"max_weight": float("nan")}}, {"strategy": {"gross_target": float("inf")}}, {"strategy": {"cash_buffer": float("nan")}}, {"initial_cash": True}, {"execution": {"lot_size": 100.0}}, {"execution": {"cancel_open_orders_at_end": 1}}, {"strategy": {"lookback": True}}, {"strategy": {"rebalance_every": 5.0}}, {"synthetic": {"trading_days": 90.0}}, {"symbols": "600000.XSHG"}, ) for raw in cases: with self.subTest(raw=raw): with self.assertRaises(ValueError): config_from_dict(raw) def test_backtest_is_byte_deterministic(self): config = load_config(PROJECT / "configs" / "baseline.json") bars = generate_synthetic_bars( config.symbols, config.synthetic.start_date, config.synthetic.trading_days, config.synthetic.seed, config.synthetic.base_volume, ) first = BacktestEngine(config).run(bars) second = BacktestEngine(config).run(bars) self.assertEqual(first.run_id, second.run_id) self.assertEqual(first.report, second.report) with tempfile.TemporaryDirectory() as left, tempfile.TemporaryDirectory() as right: left_paths = first.write_artifacts(left) right_paths = second.write_artifacts(right) for key in left_paths: left_hash = hashlib.sha256(left_paths[key].read_bytes()).hexdigest() right_hash = hashlib.sha256(right_paths[key].read_bytes()).hexdigest() self.assertEqual(left_hash, right_hash, key) self.assertTrue(HashChainLedger.read_jsonl(left_paths["events"]).verify()) report = json.loads(left_paths["report"].read_text(encoding="utf-8")) self.assertFalse(report["investment_value_claim"]) self.assertGreater(report["fill_count"], 0) records = [ json.loads(line) for line in left_paths["events"].read_text( encoding="utf-8" ).splitlines() ] timestamps = [ datetime.fromisoformat(record["timestamp"]) for record in records ] self.assertEqual(timestamps, sorted(timestamps)) def test_decisions_use_first_trading_session_close_of_each_week(self): config = load_config(PROJECT / "configs" / "baseline.json") bars = generate_synthetic_bars( config.symbols, config.synthetic.start_date, config.synthetic.trading_days, config.synthetic.seed, config.synthetic.base_volume, ) result = BacktestEngine(config).run(bars) sessions = sorted({item.trading_date for item in bars}) first_by_week = {} for session in sessions: first_by_week.setdefault(session.isocalendar()[:2], session) decision_dates = { datetime.fromisoformat(item["as_of"]).date() for item in result.targets } self.assertTrue(decision_dates) for decision_date in decision_dates: self.assertEqual( decision_date, first_by_week[decision_date.isocalendar()[:2]], ) def test_incomplete_market_session_fails_closed(self): config = load_config(PROJECT / "configs" / "baseline.json") bars = generate_synthetic_bars( config.symbols, config.synthetic.start_date, config.synthetic.trading_days, config.synthetic.seed, config.synthetic.base_volume, ) removed = bars.pop(len(config.symbols) * 30) with self.assertRaisesRegex( ValueError, "incomplete market-data session.*" + removed.symbol.replace(".", r"\."), ): BacktestEngine(config).run(bars) def test_cli_smoke_and_ledger_verify(self): with tempfile.TemporaryDirectory() as directory: output = Path(directory) / "run" self.assertEqual( main( [ "smoke", "--config", str(PROJECT / "configs" / "baseline.json"), "--output", str(output), ] ), 0, ) self.assertEqual(main(["verify-ledger", str(output / "events.jsonl")]), 0) self.assertEqual( main(["verify-manifest", str(output / "manifest.json")]), 0, ) self.assertTrue( verify_artifact_manifest(output / "manifest.json")["ok"] ) def test_manifest_verifier_rejects_tampered_or_incomplete_runs(self): config = load_config(PROJECT / "configs" / "baseline.json") bars = generate_synthetic_bars( config.symbols, config.synthetic.start_date, config.synthetic.trading_days, config.synthetic.seed, config.synthetic.base_volume, ) with tempfile.TemporaryDirectory() as directory: output = Path(directory) / "run" BacktestEngine(config).run(bars).write_artifacts(output) report_path = output / "report.json" report_path.write_bytes(report_path.read_bytes() + b" ") with self.assertRaisesRegex(ValueError, "report artifact hash mismatch"): verify_artifact_manifest(output / "manifest.json") (output / ".quant60-incomplete").write_text( "interrupted\n", encoding="ascii", ) with self.assertRaisesRegex(ValueError, "publication is incomplete"): verify_artifact_manifest(output / "manifest.json") second_output = Path(directory) / "second-run" BacktestEngine(config).run(bars).write_artifacts(second_output) manifest_path = second_output / "manifest.json" unexpected = second_output / "untracked-result.txt" unexpected.write_text("not part of the run\n", encoding="utf-8") with self.assertRaisesRegex( ValueError, "artifact directory is not an exact completed set", ): verify_artifact_manifest(manifest_path) unexpected.unlink() manifest = json.loads(manifest_path.read_text(encoding="utf-8")) manifest["engine_sources"]["portable_core.py"] = "0" * 64 manifest_path.write_text( json.dumps(manifest, sort_keys=True), encoding="utf-8", ) with self.assertRaisesRegex( ValueError, "engine_sources aggregate hash mismatch", ): verify_artifact_manifest(manifest_path) def test_cli_backtest_alias(self): with tempfile.TemporaryDirectory() as directory: output = Path(directory) / "run" self.assertEqual( main( [ "backtest", "--config", str(PROJECT / "configs" / "baseline.json"), "--output", str(output), ] ), 0, ) self.assertTrue((output / "report.json").is_file()) if __name__ == "__main__": unittest.main()