fix(backtest): give each sweep started without a run_id its own run (#1549)

- the stamped run directory is claimed atomically; a second sweep in the same second takes the next suffix instead of resuming the first
This commit is contained in:
Yijia-Xiao
2026-10-27 07:58:10 +00:00
parent 1c0e3e37fc
commit 28d32653d3
2 changed files with 40 additions and 5 deletions
+17
View File
@@ -155,6 +155,23 @@ def test_the_analyst_set_under_test_is_the_one_that_runs(tmp_path):
assert _FakeGraph.instances[-1].analysts == ["market", "news"]
@pytest.mark.unit
def test_sweeps_started_in_the_same_second_get_their_own_runs(tmp_path, monkeypatch):
import tradingagents.backtest as backtest
class _Frozen(backtest.datetime):
@classmethod
def now(cls, tz=None):
return cls(2026, 1, 20, 9, 30, 0)
monkeypatch.setattr(backtest, "datetime", _Frozen)
first = run_backtest(["NVDA"], ["2026-01-05"], _config(tmp_path))
second = run_backtest(["NVDA"], ["2026-01-05"], _config(tmp_path))
assert first.run_id == "20260120_093000" and second.run_id == "20260120_093000_2"
assert first.cells_run == second.cells_run == 1
@pytest.mark.unit
def test_a_run_id_cannot_escape_the_results_directory(tmp_path):
"""run_id becomes a path segment, so it is validated like a ticker is."""
+23 -5
View File
@@ -16,6 +16,7 @@ cell rather than a position carried forward.
from __future__ import annotations
import itertools
import logging
from collections.abc import Callable
from dataclasses import dataclass, field
@@ -126,6 +127,19 @@ class BacktestSummary:
return "\n".join(lines)
def _new_run(runs: Path) -> str:
"""A new run's id, stamped now, and its directory, claimed so no other sweep shares it."""
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
runs.mkdir(parents=True, exist_ok=True)
for n in itertools.count(1):
run_id = stamp if n == 1 else f"{stamp}_{n}"
try:
(runs / run_id).mkdir()
return run_id
except FileExistsError:
continue
def run_backtest(
tickers: list[str],
dates: list[str],
@@ -143,11 +157,15 @@ def run_backtest(
interrupted sweep resumes by being run again. ``progress(done, total,
ticker, date)`` is called before each cell that runs.
"""
# run_id becomes a path segment, so it is validated like a ticker: an
# absolute or dotted value would otherwise place the run outside results_dir.
run_id = safe_ticker_component(run_id or datetime.now().strftime("%Y%m%d_%H%M%S"))
run_dir = Path(config["results_dir"]) / "backtest" / run_id
run_dir.mkdir(parents=True, exist_ok=True)
runs = Path(config["results_dir"]) / "backtest"
if run_id:
# run_id becomes a path segment, so it is validated like a ticker: an
# absolute or dotted value would otherwise place the run outside results_dir.
run_id = safe_ticker_component(run_id)
(runs / run_id).mkdir(parents=True, exist_ok=True)
else:
run_id = _new_run(runs)
run_dir = runs / run_id
run_config = {**config, "results_dir": str(run_dir),
"memory_log_path": str(run_dir / "trading_memory.md")}