feat(reports): record the analysis date and what produced a run (#752)

- TradingAgentsGraph.run_settings(): version, provider, models, analysts, debate rounds, language and vendors; no endpoints, keys or paths
- complete_report.md opens with the analysis date and those settings; the saved state log carries them as run_settings
This commit is contained in:
Yijia-Xiao
2026-09-24 19:38:41 +00:00
parent d91d1f4cb4
commit a1b3b5bab8
5 changed files with 115 additions and 10 deletions
+2 -1
View File
@@ -381,7 +381,8 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None):
).strip() ).strip()
save_path = Path(save_path_str) save_path = Path(save_path_str)
try: try:
report_file = write_report_tree(final_state, selections["ticker"], save_path) report_file = write_report_tree(final_state, selections["ticker"], save_path,
settings=graph.run_settings())
console.print(f"\n[green]✓ Report saved to:[/green] {save_path.resolve()}") console.print(f"\n[green]✓ Report saved to:[/green] {save_path.resolve()}")
console.print(f" [dim]Complete report:[/dim] {report_file.name}") console.print(f" [dim]Complete report:[/dim] {report_file.name}")
except Exception as e: except Exception as e:
+5 -1
View File
@@ -61,7 +61,10 @@ def _bare_graph(tmp_path):
from tradingagents.graph.trading_graph import TradingAgentsGraph from tradingagents.graph.trading_graph import TradingAgentsGraph
graph = object.__new__(TradingAgentsGraph) graph = object.__new__(TradingAgentsGraph)
graph.config = {"results_dir": str(tmp_path)} graph.config = {"results_dir": str(tmp_path), "llm_provider": "openai", "deep_think_llm": "d",
"quick_think_llm": "q", "max_debate_rounds": 1, "max_risk_discuss_rounds": 1,
"output_language": "English", "data_vendors": {}, "tool_vendors": {}}
graph.selected_analysts = ("market",)
return graph return graph
@@ -87,6 +90,7 @@ def test_the_state_log_names_each_field_as_the_state_does(tmp_path):
assert logged["investment_plan"] == "计划" assert logged["investment_plan"] == "计划"
assert "trader_investment_decision" not in logged assert "trader_investment_decision" not in logged
assert "judge_decision" not in json.dumps(logged) assert "judge_decision" not in json.dumps(logged)
assert logged["run_settings"]["deep_think_llm"] == "d"
@pytest.mark.unit @pytest.mark.unit
+57 -3
View File
@@ -21,6 +21,11 @@ def _state():
} }
SETTINGS = {"version": "0.5.2", "llm_provider": "openai", "deep_think_llm": "gpt-6-sol",
"quick_think_llm": "gpt-6-luna", "analysts": ["market", "news"], "max_debate_rounds": 1,
"max_risk_discuss_rounds": 2, "data_vendors": {"core_stock_apis": "yfinance"}}
@pytest.mark.unit @pytest.mark.unit
def test_write_report_tree_creates_files(tmp_path): def test_write_report_tree_creates_files(tmp_path):
out = write_report_tree(_state(), "AAPL", tmp_path) out = write_report_tree(_state(), "AAPL", tmp_path)
@@ -37,16 +42,65 @@ def test_write_report_tree_creates_files(tmp_path):
@pytest.mark.unit @pytest.mark.unit
def test_save_reports_explicit_path(tmp_path): def test_save_reports_explicit_path(tmp_path):
# Unbound: with an explicit save_path, the method doesn't touch self/config. graph = SimpleNamespace(run_settings=lambda: SETTINGS)
out = TradingAgentsGraph.save_reports(None, _state(), "AAPL", save_path=tmp_path) out = TradingAgentsGraph.save_reports(graph, _state(), "AAPL", save_path=tmp_path)
assert (tmp_path / "complete_report.md").exists() assert (tmp_path / "complete_report.md").exists()
assert out == tmp_path / "complete_report.md" assert out == tmp_path / "complete_report.md"
@pytest.mark.unit @pytest.mark.unit
def test_save_reports_defaults_under_results_dir(tmp_path): def test_save_reports_defaults_under_results_dir(tmp_path):
mock_self = SimpleNamespace(config={"results_dir": str(tmp_path)}) mock_self = SimpleNamespace(config={"results_dir": str(tmp_path)}, run_settings=lambda: SETTINGS)
out = TradingAgentsGraph.save_reports(mock_self, _state(), "AAPL") out = TradingAgentsGraph.save_reports(mock_self, _state(), "AAPL")
assert out.exists() assert out.exists()
assert out.parent.parent.name == "reports" # results_dir/reports/AAPL_<stamp>/... assert out.parent.parent.name == "reports" # results_dir/reports/AAPL_<stamp>/...
assert out.parent.name.startswith("AAPL_") assert out.parent.name.startswith("AAPL_")
@pytest.mark.unit
def test_the_report_names_the_analysis_date_and_what_produced_it(tmp_path):
state = dict(_state(), trade_date="2026-09-23")
header = write_report_tree(state, "NVDA", tmp_path, settings=SETTINGS).read_text().split("## ")[0]
assert "Analysis date: 2026-09-23" in header
assert "TradingAgents 0.5.2" in header
assert "openai, deep gpt-6-sol, quick gpt-6-luna" in header
assert "Analysts: market, news" in header
assert "research debate rounds 1, risk debate rounds 2" in header
assert "core_stock_apis yfinance" in header
@pytest.mark.unit
def test_run_settings_record_the_run_without_endpoints_or_paths():
from tradingagents.graph.trading_graph import TradingAgentsGraph
graph = object.__new__(TradingAgentsGraph)
graph.selected_analysts = ("market", "news")
graph.config = {"llm_provider": "openai", "deep_think_llm": "gpt-6-sol", "quick_think_llm": "gpt-6-luna",
"max_debate_rounds": 1, "max_risk_discuss_rounds": 1, "output_language": "English",
"data_vendors": {"core_stock_apis": "yfinance"}, "tool_vendors": {},
"backend_url": "https://user:secret@relay.example/v1", "results_dir": "/home/me/results"}
settings = graph.run_settings()
assert settings["analysts"] == ["market", "news"]
assert settings["deep_think_llm"] == "gpt-6-sol"
assert "secret" not in str(settings) and "/home/me" not in str(settings)
@pytest.mark.unit
def test_a_partial_settings_dict_still_writes_the_report(tmp_path):
out = write_report_tree(_state(), "AAPL", tmp_path, settings={"llm_provider": "openai"})
assert "openai" in out.read_text()
@pytest.mark.unit
def test_run_settings_name_the_version_of_the_running_code():
import tradingagents
from tradingagents.graph.trading_graph import TradingAgentsGraph
graph = object.__new__(TradingAgentsGraph)
graph.selected_analysts, graph.config = ("market",), {}
assert graph.run_settings()["version"] == tradingagents.__version__
+23 -1
View File
@@ -6,6 +6,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import tradingagents
from tradingagents.agents.context import build_instrument_context, resolve_instrument_identity from tradingagents.agents.context import build_instrument_context, resolve_instrument_identity
from tradingagents.agents.rating import run_rating from tradingagents.agents.rating import run_rating
from tradingagents.dataflows.config import run_config, set_config from tradingagents.dataflows.config import run_config, set_config
@@ -241,6 +242,26 @@ class TradingAgentsGraph:
self._run_signature(asset_type, portfolio), self._run_signature(asset_type, portfolio),
) )
def run_settings(self) -> dict:
"""What produces this graph's runs, for the saved report and state log.
An allowlist: endpoints (a backend_url can carry credentials), keys and
local paths are never recorded.
"""
cfg = self.config
return {
"version": tradingagents.__version__,
"llm_provider": cfg.get("llm_provider"),
"deep_think_llm": cfg.get("deep_think_llm"),
"quick_think_llm": cfg.get("quick_think_llm"),
"analysts": list(self.selected_analysts),
"max_debate_rounds": cfg.get("max_debate_rounds"),
"max_risk_discuss_rounds": cfg.get("max_risk_discuss_rounds"),
"output_language": cfg.get("output_language"),
"data_vendors": dict(cfg.get("data_vendors") or {}),
"tool_vendors": dict(cfg.get("tool_vendors") or {}),
}
def save_reports(self, final_state, ticker, save_path=None) -> Path: def save_reports(self, final_state, ticker, save_path=None) -> Path:
"""Write the markdown report tree for a completed run, like the CLI does. """Write the markdown report tree for a completed run, like the CLI does.
@@ -254,7 +275,7 @@ class TradingAgentsGraph:
/ "reports" / "reports"
/ f"{safe_ticker_component(ticker)}_{stamp}" / f"{safe_ticker_component(ticker)}_{stamp}"
) )
return write_report_tree(final_state, ticker, save_path) return write_report_tree(final_state, ticker, save_path, settings=self.run_settings())
def create_run_state(self, company_name, trade_date, asset_type: str = "stock", portfolio=None): def create_run_state(self, company_name, trade_date, asset_type: str = "stock", portfolio=None):
"""Build a run's initial state; propagate() and the CLI both start here. """Build a run's initial state; propagate() and the CLI both start here.
@@ -370,6 +391,7 @@ class TradingAgentsGraph:
"investment_plan": final_state["investment_plan"], "investment_plan": final_state["investment_plan"],
"final_trade_decision": final_state["final_trade_decision"], "final_trade_decision": final_state["final_trade_decision"],
"final_rating": run_rating(final_state), "final_rating": run_rating(final_state),
"run_settings": self.run_settings(),
} }
# A ticker that would escape the results directory is rejected. # A ticker that would escape the results directory is rejected.
+28 -4
View File
@@ -10,8 +10,31 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
def write_report_tree(final_state: dict, ticker: str, save_path) -> Path: def _header(ticker: str, final_state: dict, settings: dict | None) -> str:
"""Save a completed run's reports to ``save_path``; return the complete-report path.""" """The report's title and what produced it: analysis date, version, models, analysts, vendors."""
lines = [f"# Trading Analysis Report: {ticker}", ""]
if final_state.get("trade_date"):
lines.append(f"- Analysis date: {final_state['trade_date']}")
lines.append(f"- Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if settings:
s = settings.get
lines.append(f"- TradingAgents {s('version', '?')}: {s('llm_provider', '?')}, "
f"deep {s('deep_think_llm', '?')}, quick {s('quick_think_llm', '?')}")
lines.append(f"- Analysts: {', '.join(s('analysts') or [])}; "
f"research debate rounds {s('max_debate_rounds', '?')}, "
f"risk debate rounds {s('max_risk_discuss_rounds', '?')}")
vendors = {**(s("data_vendors") or {}), **(s("tool_vendors") or {})}
if vendors:
lines.append("- Data vendors: " + ", ".join(f"{k} {v}" for k, v in vendors.items()))
return "\n".join(lines) + "\n\n"
def write_report_tree(final_state: dict, ticker: str, save_path, settings: dict | None = None) -> Path:
"""Save a completed run's reports to ``save_path``; return the complete-report path.
``settings`` (``TradingAgentsGraph.run_settings()``) adds what produced the run
to the report's header.
"""
save_path = Path(save_path) save_path = Path(save_path)
save_path.mkdir(parents=True, exist_ok=True) save_path.mkdir(parents=True, exist_ok=True)
sections = [] sections = []
@@ -96,6 +119,7 @@ def write_report_tree(final_state: dict, ticker: str, save_path) -> Path:
sections.append(f"## V. Portfolio Manager Decision\n\n### Portfolio Manager\n{final_state['final_trade_decision']}") sections.append(f"## V. Portfolio Manager Decision\n\n### Portfolio Manager\n{final_state['final_trade_decision']}")
# Write consolidated report # Write consolidated report
header = f"# Trading Analysis Report: {ticker}\n\nGenerated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" (save_path / "complete_report.md").write_text(
(save_path / "complete_report.md").write_text(header + "\n\n".join(sections), encoding="utf-8") _header(ticker, final_state, settings) + "\n\n".join(sections), encoding="utf-8"
)
return save_path / "complete_report.md" return save_path / "complete_report.md"