mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-27 15:02:39 +03:00
fix(graph): stop keeping every run's full state in the graph
- a graph reused across a backtest grid held each run's complete state for its whole life - the state log is written from the run's own state; the ticker attribute it read is removed
This commit is contained in:
+19
-14
@@ -35,21 +35,12 @@ def test_the_other_shapes_are_unchanged(value, expected):
|
|||||||
assert extract_content_string(value) == expected
|
assert extract_content_string(value) == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
def _state(ticker, final="评级: 买入"):
|
||||||
def test_the_state_log_keeps_non_ascii_readable(tmp_path):
|
return {
|
||||||
"""Reports can be in any language; the log is read by a person."""
|
"company_of_interest": ticker, "trade_date": "2026-09-01",
|
||||||
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",
|
|
||||||
"market_report": "市场", "sentiment_report": "情绪", "news_report": "新闻",
|
"market_report": "市场", "sentiment_report": "情绪", "news_report": "新闻",
|
||||||
"fundamentals_report": "基本面", "investment_plan": "计划",
|
"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": "",
|
"investment_debate_state": {"bull_history": "", "bear_history": "", "history": "",
|
||||||
"current_response": "", "judge_decision": "", "count": 0},
|
"current_response": "", "judge_decision": "", "count": 0},
|
||||||
"risk_debate_state": {"aggressive_history": "", "conservative_history": "",
|
"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": "",
|
"latest_speaker": "", "current_aggressive_response": "",
|
||||||
"current_conservative_response": "", "current_neutral_response": "",
|
"current_conservative_response": "", "current_neutral_response": "",
|
||||||
"count": 0},
|
"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")
|
written = next(tmp_path.rglob("full_states_log*.json")).read_text(encoding="utf-8")
|
||||||
assert "买入" in written
|
assert "买入" in written
|
||||||
|
|||||||
@@ -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)
|
_graph(tmp_path / "fresh", monkeypatch, full_run).propagate("NVDA", TRADE_DATE)
|
||||||
# The resumed run makes only the calls the interrupted one had not completed.
|
# The resumed run makes only the calls the interrupted one had not completed.
|
||||||
assert resumed_calls == len(full_run.calls) - (model.fail_at - 1)
|
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
|
||||||
|
|||||||
@@ -932,7 +932,6 @@ class TestLegacyRemoval:
|
|||||||
}
|
}
|
||||||
mock_graph = MagicMock()
|
mock_graph = MagicMock()
|
||||||
mock_graph.memory_log = TradingMemoryLog({"memory_log_path": str(tmp_path / "mem.md")})
|
mock_graph.memory_log = TradingMemoryLog({"memory_log_path": str(tmp_path / "mem.md")})
|
||||||
mock_graph.log_states_dict = {}
|
|
||||||
mock_graph.debug = False
|
mock_graph.debug = False
|
||||||
mock_graph.config = {"results_dir": str(tmp_path)}
|
mock_graph.config = {"results_dir": str(tmp_path)}
|
||||||
mock_graph.graph.invoke.return_value = fake_state
|
mock_graph.graph.invoke.return_value = fake_state
|
||||||
|
|||||||
@@ -167,8 +167,6 @@ class TradingAgentsGraph:
|
|||||||
|
|
||||||
# State tracking
|
# State tracking
|
||||||
self.curr_state = None
|
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.
|
# Graph-shape-affecting run choices, kept for the checkpoint signature.
|
||||||
self.selected_analysts = tuple(selected_analysts)
|
self.selected_analysts = tuple(selected_analysts)
|
||||||
@@ -440,7 +438,6 @@ class TradingAgentsGraph:
|
|||||||
PortfolioRating enum.
|
PortfolioRating enum.
|
||||||
"""
|
"""
|
||||||
trade_date = _validate_trade_date(trade_date)
|
trade_date = _validate_trade_date(trade_date)
|
||||||
self.ticker = company_name
|
|
||||||
|
|
||||||
with run_config(self.config), \
|
with run_config(self.config), \
|
||||||
self.checkpoint_scope(company_name, trade_date, asset_type, portfolio) as thread_id_value:
|
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"])
|
return final_state, self.process_signal(final_state["final_trade_decision"])
|
||||||
|
|
||||||
def _log_state(self, trade_date, final_state):
|
def _log_state(self, trade_date, final_state):
|
||||||
"""Log the final state to a JSON file."""
|
"""Write a run's final state to JSON under the run's own ticker."""
|
||||||
self.log_states_dict[str(trade_date)] = {
|
entry = {
|
||||||
"company_of_interest": final_state["company_of_interest"],
|
"company_of_interest": final_state["company_of_interest"],
|
||||||
"trade_date": final_state["trade_date"],
|
"trade_date": final_state["trade_date"],
|
||||||
"market_report": final_state["market_report"],
|
"market_report": final_state["market_report"],
|
||||||
@@ -648,16 +645,15 @@ class TradingAgentsGraph:
|
|||||||
"final_trade_decision": final_state["final_trade_decision"],
|
"final_trade_decision": final_state["final_trade_decision"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Save to file. Reject ticker values that would escape the
|
# A ticker that would escape the results directory is rejected.
|
||||||
# results directory when joined as a path component.
|
safe_ticker = safe_ticker_component(final_state["company_of_interest"])
|
||||||
safe_ticker = safe_ticker_component(self.ticker)
|
|
||||||
directory = Path(self.config["results_dir"]) / safe_ticker / "TradingAgentsStrategy_logs"
|
directory = Path(self.config["results_dir"]) / safe_ticker / "TradingAgentsStrategy_logs"
|
||||||
directory.mkdir(parents=True, exist_ok=True)
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
log_path = directory / f"full_states_log_{trade_date}.json"
|
log_path = directory / f"full_states_log_{trade_date}.json"
|
||||||
with open(log_path, "w", encoding="utf-8") as f:
|
with open(log_path, "w", encoding="utf-8") as f:
|
||||||
# Reports can be in any language and this file is read by a person.
|
# 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):
|
def process_signal(self, full_signal):
|
||||||
"""Process a signal to extract the core decision."""
|
"""Process a signal to extract the core decision."""
|
||||||
|
|||||||
Reference in New Issue
Block a user