mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-27 06:56:39 +03:00
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:
+2
-1
@@ -381,7 +381,8 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None):
|
||||
).strip()
|
||||
save_path = Path(save_path_str)
|
||||
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" [dim]Complete report:[/dim] {report_file.name}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -61,7 +61,10 @@ def _bare_graph(tmp_path):
|
||||
from tradingagents.graph.trading_graph import 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
|
||||
|
||||
|
||||
@@ -87,6 +90,7 @@ def test_the_state_log_names_each_field_as_the_state_does(tmp_path):
|
||||
assert logged["investment_plan"] == "计划"
|
||||
assert "trader_investment_decision" not in logged
|
||||
assert "judge_decision" not in json.dumps(logged)
|
||||
assert logged["run_settings"]["deep_think_llm"] == "d"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
||||
+57
-3
@@ -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
|
||||
def test_write_report_tree_creates_files(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
|
||||
def test_save_reports_explicit_path(tmp_path):
|
||||
# Unbound: with an explicit save_path, the method doesn't touch self/config.
|
||||
out = TradingAgentsGraph.save_reports(None, _state(), "AAPL", save_path=tmp_path)
|
||||
graph = SimpleNamespace(run_settings=lambda: SETTINGS)
|
||||
out = TradingAgentsGraph.save_reports(graph, _state(), "AAPL", save_path=tmp_path)
|
||||
assert (tmp_path / "complete_report.md").exists()
|
||||
assert out == tmp_path / "complete_report.md"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
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")
|
||||
assert out.exists()
|
||||
assert out.parent.parent.name == "reports" # results_dir/reports/AAPL_<stamp>/...
|
||||
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__
|
||||
|
||||
@@ -6,6 +6,7 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import tradingagents
|
||||
from tradingagents.agents.context import build_instrument_context, resolve_instrument_identity
|
||||
from tradingagents.agents.rating import run_rating
|
||||
from tradingagents.dataflows.config import run_config, set_config
|
||||
@@ -241,6 +242,26 @@ class TradingAgentsGraph:
|
||||
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:
|
||||
"""Write the markdown report tree for a completed run, like the CLI does.
|
||||
|
||||
@@ -254,7 +275,7 @@ class TradingAgentsGraph:
|
||||
/ "reports"
|
||||
/ 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):
|
||||
"""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"],
|
||||
"final_trade_decision": final_state["final_trade_decision"],
|
||||
"final_rating": run_rating(final_state),
|
||||
"run_settings": self.run_settings(),
|
||||
}
|
||||
|
||||
# A ticker that would escape the results directory is rejected.
|
||||
|
||||
@@ -10,8 +10,31 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def write_report_tree(final_state: dict, ticker: str, save_path) -> Path:
|
||||
"""Save a completed run's reports to ``save_path``; return the complete-report path."""
|
||||
def _header(ticker: str, final_state: dict, settings: dict | None) -> str:
|
||||
"""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.mkdir(parents=True, exist_ok=True)
|
||||
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']}")
|
||||
|
||||
# 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(header + "\n\n".join(sections), encoding="utf-8")
|
||||
(save_path / "complete_report.md").write_text(
|
||||
_header(ticker, final_state, settings) + "\n\n".join(sections), encoding="utf-8"
|
||||
)
|
||||
return save_path / "complete_report.md"
|
||||
|
||||
Reference in New Issue
Block a user