Files
ForexBot2/scripts/capture_baseline.py
T
Harwinder Sekhon 1c264d5315 baseline: byte-for-byte backtest reference at da61cff + compare tooling
Pin reference outputs of backtest/gpu_backtest.py (XAU/USD, window
2025-08-04..2026-08-04, 4 primary TFs x spread 0.30/0.65 = 8 runs) under
baseline/da61cff/: trade books, stdout summaries, derived equity curves
and metrics, plus a manifest with exact argv, input-parquet and output
hashes, backend, and tool versions.

scripts/capture_baseline.py captures (cache-warm only, aborts rather
than fetch live); scripts/compare_baseline.py re-runs the identical
commands and byte-diffs every output, PASS/FAIL per file. Verified
32/32 files PASS on full re-run at this commit; m5_s065 reproduces the
pinned HEAD reference exactly (555 trades, +98.64R, PF 1.483). No
signal, execution, or scanner logic touched; 359 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J3mZCQRHnJKt7qXQFG3RhB
2026-08-07 16:34:14 -04:00

184 lines
7.4 KiB
Python
Raw 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.
#!/usr/bin/env python
"""Capture the byte-for-byte backtest baseline at the current commit.
Runs the 8-run matrix (M5/M15/H1/H4 × spread 0.30/0.65, window
2025-08-04 → 2026-08-04) through backtest/gpu_backtest.py and pins every
output under baseline/<commit_short>/:
baseline/<commit>/
manifest.json commands, input hashes, output hashes, env
README.md layout + how to verify
<run_key>/
trades.csv full trade book (--export-csv)
summary.txt backtester stdout (byte-comparable)
equity.csv derived cumulative-R curve
metrics.json derived headline + grouped stats
run.log backtester stderr (provenance only)
Aborts before running anything if the tracked tree is dirty or any needed
cache parquet is missing (a cache miss would fetch live — wrong data, and
the local .env Oanda token is stale). Verify afterwards with
scripts/compare_baseline.py — it must print all-PASS at the same commit.
Usage:
venv/bin/python scripts/capture_baseline.py [--runs m5_s030,...] [--force]
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import baseline_common as bc
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--runs", default="",
help="comma-separated run keys to capture (default: all)")
ap.add_argument("--force", action="store_true",
help="overwrite existing run dirs / manifest")
args = ap.parse_args()
git = bc.git_info()
if git["dirty"]:
print("ABORT: tracked working tree is dirty — a baseline must be "
"attributable to exactly one commit. Commit or stash first.")
return 2
specs = bc.run_specs()
if args.runs:
wanted = [k.strip() for k in args.runs.split(",") if k.strip()]
unknown = [k for k in wanted if k not in specs]
if unknown:
print(f"ABORT: unknown run key(s) {unknown}; "
f"valid: {', '.join(specs)}")
return 2
specs = {k: specs[k] for k in wanted}
base_dir = bc.BASELINE_ROOT / git["commit_short"]
if not args.force:
clashes = [k for k in specs if (base_dir / k).exists()]
if clashes:
print(f"ABORT: {base_dir} already has run(s) {clashes} "
"(use --force to overwrite)")
return 2
# Pre-flight: every parquet the runs will read must already exist and
# postdate the raw-cache format change. Never trigger a network fetch.
problems = []
cache_hashes = {}
for name in bc.all_expected_cache_files(specs):
path = bc.CACHE_DIR / name
if not path.exists():
problems.append(f"missing cache parquet: {name}")
elif path.stat().st_mtime < bc.RAW_CACHE_EPOCH:
problems.append(f"pre-raw-format cache parquet (cleaned bars): "
f"{name}")
else:
cache_hashes[name] = bc.sha256_file(path)
if problems:
print("ABORT: cache pre-flight failed — refusing to run "
"(a miss would fetch live data):")
for p in problems:
print(f" - {p}")
return 2
print(f"Pre-flight OK: {len(cache_hashes)} cache parquets present "
f"and hashed. Capturing {len(specs)} run(s) at {git['commit_short']}.")
manifest_runs = {}
backend = None
for i, (key, spec) in enumerate(specs.items(), 1):
run_dir = base_dir / key
print(f"[{i}/{len(specs)}] {key}: {spec['primary_tf']} "
f"spread {spec['spread']} ...", flush=True)
bc.run_backtest(spec["argv"], run_dir)
log_problems, run_backend = bc.check_run_log(run_dir,
spec["primary_tf"])
if log_problems:
print(f"ABORT: {key} failed stderr sanity checks:")
for p in log_problems:
print(f" - {p}")
print(f" log: {run_dir / 'run.log'}")
return 2
if backend is None:
backend = run_backend
elif run_backend != backend:
print(f"ABORT: backend flipped mid-capture "
f"({backend!r} → {run_backend!r})")
return 2
bc.derive_equity(run_dir / "trades.csv", run_dir / "equity.csv")
metrics = bc.derive_metrics(run_dir / "trades.csv",
run_dir / "metrics.json")
manifest_runs[key] = {
"argv": spec["argv"],
"primary_tf": spec["primary_tf"],
"cache_files": bc.expected_cache_files(spec["primary_tf"]),
"files": {f: bc.sha256_file(run_dir / f)
for f in bc.COMPARED_FILES},
}
o = metrics["overall"]
print(f" {o['trades']} trades, {o['total_r']:+.2f}R, "
f"PF {o['profit_factor']}, maxDD {o['max_dd_r']:.2f}R")
manifest = {
"created_utc": bc.utc_now_iso(),
"commit": git["commit"],
"commit_short": git["commit_short"],
"dirty": git["dirty"],
"window": {"start": bc.WINDOW_START, "end": bc.WINDOW_END},
"backend": backend,
"env_overrides": {"CUDA_PATH": "removed", "PYTHONHASHSEED": "0"},
"versions": bc.tool_versions(),
"config_inputs": bc.config_inputs(),
"cache_files": cache_hashes,
"compared_files": list(bc.COMPARED_FILES),
"runs": manifest_runs,
}
manifest_path = base_dir / "manifest.json"
if manifest_path.exists():
# Partial re-capture with --runs: merge into the existing manifest
# (only the just-captured keys are replaced; same commit is
# guaranteed by the dir name).
old = json.loads(manifest_path.read_text())
old["runs"].update(manifest_runs)
old["cache_files"].update(cache_hashes)
old["created_utc"] = manifest["created_utc"]
manifest = old
manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True)
+ "\n")
_write_readme(base_dir)
print(f"\nBaseline captured: {base_dir}")
print("Verify now: venv/bin/python scripts/compare_baseline.py "
"(must be all-PASS at this commit)")
return 0
def _write_readme(base_dir: Path) -> None:
(base_dir / "README.md").write_text(f"""\
# Backtest baseline — commit {base_dir.name}
Reference outputs of `backtest/gpu_backtest.py` for XAU/USD, window
{bc.WINDOW_START} → {bc.WINDOW_END}, 4 primary TFs × spreads 0.30/0.65.
Captured by `scripts/capture_baseline.py`; verify with
`venv/bin/python scripts/compare_baseline.py` (re-runs every command and
byte-diffs the outputs, PASS/FAIL per file).
Per run dir: `trades.csv` (full book), `summary.txt` (backtester stdout),
`equity.csv` + `metrics.json` (derived from the book). Those four are
byte-compared. `run.log` is the backtester's stderr — timestamped, kept
for provenance only, never compared.
`manifest.json` records the exact argv per run, sha256 of every input
parquet and output file, backend, tool versions, and the non-secret
config inputs that alter results. Input data must come from
`backtest/cache/` — if those parquets change, comparisons are no longer
attributable to code changes and the compare script aborts.
""")
if __name__ == "__main__":
sys.exit(main())