diff --git a/tests/test_cli_display.py b/tests/test_cli_display.py index 6c7eadd65..078f3729b 100644 --- a/tests/test_cli_display.py +++ b/tests/test_cli_display.py @@ -35,21 +35,12 @@ def test_the_other_shapes_are_unchanged(value, expected): assert extract_content_string(value) == expected -@pytest.mark.unit -def test_the_state_log_keeps_non_ascii_readable(tmp_path): - """Reports can be in any language; the log is read by a person.""" - from tradingagents.graph.trading_graph import TradingAgentsGraph - - graph = object.__new__(TradingAgentsGraph) - graph.config = {"results_dir": str(tmp_path)} - graph.ticker = "600519.SS" - graph.log_states_dict = {} - - graph._log_state("2026-09-01", { - "company_of_interest": "600519.SS", "trade_date": "2026-09-01", +def _state(ticker, final="评级: 买入"): + return { + "company_of_interest": ticker, "trade_date": "2026-09-01", "market_report": "市场", "sentiment_report": "情绪", "news_report": "新闻", "fundamentals_report": "基本面", "investment_plan": "计划", - "trader_investment_plan": "交易计划", "final_trade_decision": "评级: 买入", + "trader_investment_plan": "交易计划", "final_trade_decision": final, "investment_debate_state": {"bull_history": "", "bear_history": "", "history": "", "current_response": "", "judge_decision": "", "count": 0}, "risk_debate_state": {"aggressive_history": "", "conservative_history": "", @@ -57,7 +48,21 @@ def test_the_state_log_keeps_non_ascii_readable(tmp_path): "latest_speaker": "", "current_aggressive_response": "", "current_conservative_response": "", "current_neutral_response": "", "count": 0}, - }) + } + + +def _bare_graph(tmp_path): + from tradingagents.graph.trading_graph import TradingAgentsGraph + + graph = object.__new__(TradingAgentsGraph) + graph.config = {"results_dir": str(tmp_path)} + return graph + + +@pytest.mark.unit +def test_the_state_log_keeps_non_ascii_readable(tmp_path): + """Reports can be in any language; the log is read by a person.""" + _bare_graph(tmp_path)._log_state("2026-09-01", _state("600519.SS")) written = next(tmp_path.rglob("full_states_log*.json")).read_text(encoding="utf-8") assert "买入" in written diff --git a/tests/test_graph_end_to_end.py b/tests/test_graph_end_to_end.py index 52719b162..1a3f50a15 100644 --- a/tests/test_graph_end_to_end.py +++ b/tests/test_graph_end_to_end.py @@ -153,3 +153,16 @@ def test_an_interrupted_run_resumes_from_its_checkpoint(tmp_path, monkeypatch, o _graph(tmp_path / "fresh", monkeypatch, full_run).propagate("NVDA", TRADE_DATE) # The resumed run makes only the calls the interrupted one had not completed. assert resumed_calls == len(full_run.calls) - (model.fail_at - 1) + + +@pytest.mark.unit +def test_a_graph_reused_across_runs_keeps_no_run_state(tmp_path, monkeypatch, offline): + """A backtest reuses one graph over its whole grid; holding every run's full + state would grow without bound.""" + graph = _graph(tmp_path, monkeypatch, ScriptedModel()) + for trade_date in ("2026-01-08", TRADE_DATE): + graph.propagate("NVDA", trade_date) + + held = [v for v in vars(graph).values() if isinstance(v, dict) and TRADE_DATE in v] + assert held == [] + assert len(list(tmp_path.glob("results/NVDA/TradingAgentsStrategy_logs/*.json"))) == 2 diff --git a/tests/test_memory_log.py b/tests/test_memory_log.py index 42dc89c12..1425ed03c 100644 --- a/tests/test_memory_log.py +++ b/tests/test_memory_log.py @@ -932,7 +932,6 @@ class TestLegacyRemoval: } mock_graph = MagicMock() mock_graph.memory_log = TradingMemoryLog({"memory_log_path": str(tmp_path / "mem.md")}) - mock_graph.log_states_dict = {} mock_graph.debug = False mock_graph.config = {"results_dir": str(tmp_path)} mock_graph.graph.invoke.return_value = fake_state diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index a8c8bef88..75a14661b 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -167,8 +167,6 @@ class TradingAgentsGraph: # State tracking self.curr_state = None - self.ticker = None - self.log_states_dict = {} # date to full state dict # Graph-shape-affecting run choices, kept for the checkpoint signature. self.selected_analysts = tuple(selected_analysts) @@ -440,7 +438,6 @@ class TradingAgentsGraph: PortfolioRating enum. """ trade_date = _validate_trade_date(trade_date) - self.ticker = company_name with run_config(self.config), \ self.checkpoint_scope(company_name, trade_date, asset_type, portfolio) as thread_id_value: @@ -617,8 +614,8 @@ class TradingAgentsGraph: return final_state, self.process_signal(final_state["final_trade_decision"]) def _log_state(self, trade_date, final_state): - """Log the final state to a JSON file.""" - self.log_states_dict[str(trade_date)] = { + """Write a run's final state to JSON under the run's own ticker.""" + entry = { "company_of_interest": final_state["company_of_interest"], "trade_date": final_state["trade_date"], "market_report": final_state["market_report"], @@ -648,16 +645,15 @@ class TradingAgentsGraph: "final_trade_decision": final_state["final_trade_decision"], } - # Save to file. Reject ticker values that would escape the - # results directory when joined as a path component. - safe_ticker = safe_ticker_component(self.ticker) + # A ticker that would escape the results directory is rejected. + safe_ticker = safe_ticker_component(final_state["company_of_interest"]) directory = Path(self.config["results_dir"]) / safe_ticker / "TradingAgentsStrategy_logs" directory.mkdir(parents=True, exist_ok=True) log_path = directory / f"full_states_log_{trade_date}.json" with open(log_path, "w", encoding="utf-8") as f: # Reports can be in any language and this file is read by a person. - json.dump(self.log_states_dict[str(trade_date)], f, indent=4, ensure_ascii=False) + json.dump(entry, f, indent=4, ensure_ascii=False) def process_signal(self, full_signal): """Process a signal to extract the core decision."""