mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-27 15:02: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()
|
).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:
|
||||||
|
|||||||
@@ -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
@@ -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__
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user