From ae2c813d8ad161f924a244d614f58e889aadf6a0 Mon Sep 17 00:00:00 2001 From: Robin Lindbladh Date: Tue, 24 Mar 2026 20:10:21 +0100 Subject: [PATCH 01/20] feat: add portfolio analysis for multi-stock comparison Add PortfolioAnalyzer class that runs the full agent pipeline on multiple stocks and produces a comparative KEEP/REDUCE/EXIT recommendation using the deep thinking LLM. Includes per-ticker error handling, graceful degradation on LLM failure, and result logging. Addresses #60 and partially #406. Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/portfolio_analysis.py | 37 +++++ tradingagents/graph/__init__.py | 2 + tradingagents/graph/portfolio_analysis.py | 183 ++++++++++++++++++++++ tradingagents/graph/trading_graph.py | 17 ++ 4 files changed, 239 insertions(+) create mode 100644 examples/portfolio_analysis.py create mode 100644 tradingagents/graph/portfolio_analysis.py diff --git a/examples/portfolio_analysis.py b/examples/portfolio_analysis.py new file mode 100644 index 00000000..2ddeecc2 --- /dev/null +++ b/examples/portfolio_analysis.py @@ -0,0 +1,37 @@ +"""Portfolio analysis example. + +Analyzes multiple stocks in a portfolio and produces a comparative +recommendation for each position (KEEP / REDUCE / EXIT). + +Related GitHub issues: #60, #406 +""" + +from tradingagents.graph.trading_graph import TradingAgentsGraph +from tradingagents.default_config import DEFAULT_CONFIG +from dotenv import load_dotenv + +load_dotenv() + +config = DEFAULT_CONFIG.copy() +# Customize LLM provider and models as needed: +# config["llm_provider"] = "anthropic" # or "openai", "google" +# config["deep_think_llm"] = "claude-sonnet-4-20250514" +# config["quick_think_llm"] = "claude-haiku-4-5-20251001" +config["max_debate_rounds"] = 1 + +ta = TradingAgentsGraph(debug=False, config=config) + +# Define your portfolio +portfolio = ["NVDA", "AAPL", "MSFT", "GOOGL", "AMZN"] + +# Run the portfolio analysis +results = ta.propagate_portfolio(portfolio, "2025-03-23") + +# Print individual signals +print("\n=== INDIVIDUAL SIGNALS ===") +for ticker, result in results["individual_results"].items(): + print(f" {ticker}: {result['signal']}") + +# Print the comparative portfolio summary +print("\n=== PORTFOLIO SUMMARY ===") +print(results["portfolio_summary"]) diff --git a/tradingagents/graph/__init__.py b/tradingagents/graph/__init__.py index 80982c19..9bc3e725 100644 --- a/tradingagents/graph/__init__.py +++ b/tradingagents/graph/__init__.py @@ -6,6 +6,7 @@ from .setup import GraphSetup from .propagation import Propagator from .reflection import Reflector from .signal_processing import SignalProcessor +from .portfolio_analysis import PortfolioAnalyzer __all__ = [ "TradingAgentsGraph", @@ -14,4 +15,5 @@ __all__ = [ "Propagator", "Reflector", "SignalProcessor", + "PortfolioAnalyzer", ] diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py new file mode 100644 index 00000000..355f6e69 --- /dev/null +++ b/tradingagents/graph/portfolio_analysis.py @@ -0,0 +1,183 @@ +# TradingAgents/graph/portfolio_analysis.py + +import json +from pathlib import Path +from typing import Any, Callable, Dict, List, Tuple + +from langchain_openai import ChatOpenAI + + +class PortfolioAnalyzer: + """Analyzes multiple stocks and produces a comparative portfolio recommendation. + + Follows the same delegation pattern as SignalProcessor and Reflector — + the orchestrator (TradingAgentsGraph) owns the graph and LLMs, this class + owns the portfolio-level prompt, comparison logic, and logging. + """ + + def __init__(self, deep_thinking_llm: ChatOpenAI): + """Initialize with the deep thinking LLM for comparative analysis. + + Args: + deep_thinking_llm: The LLM instance used for the portfolio summary. + """ + self.deep_thinking_llm = deep_thinking_llm + + def analyze( + self, + tickers: List[str], + trade_date: str, + propagate_fn: Callable[[str, str], Tuple[Dict[str, Any], str]], + debug: bool = False, + ) -> Dict[str, Any]: + """Run analysis on multiple stocks and produce a comparative summary. + + Args: + tickers: List of ticker symbols to analyze. + trade_date: The trade date string (e.g., "2026-03-23"). + propagate_fn: The single-stock propagation function (typically + TradingAgentsGraph.propagate). + debug: Whether to print progress output. + + Returns: + Dictionary with: + - "individual_results": dict mapping ticker to its decision and signal + - "portfolio_summary": the comparative LLM analysis + + Raises: + ValueError: If tickers is empty. + """ + if not tickers: + raise ValueError("tickers must be a non-empty list") + + individual_results = self._analyze_individual( + tickers, trade_date, propagate_fn, debug + ) + + portfolio_summary = self._generate_summary( + individual_results, trade_date + ) + + self._log_portfolio(trade_date, tickers, individual_results, portfolio_summary) + + return { + "individual_results": individual_results, + "portfolio_summary": portfolio_summary, + } + + def _analyze_individual( + self, + tickers: List[str], + trade_date: str, + propagate_fn: Callable, + debug: bool, + ) -> Dict[str, Dict[str, str]]: + """Run the agent pipeline on each ticker, collecting results.""" + individual_results = {} + + for ticker in tickers: + if debug: + print(f"\n{'='*60}") + print(f"Analyzing {ticker}...") + print(f"{'='*60}\n") + + try: + final_state, signal = propagate_fn(ticker, trade_date) + individual_results[ticker] = { + "signal": signal, + "final_trade_decision": final_state["final_trade_decision"], + } + except Exception as e: + if debug: + print(f"Error analyzing {ticker}: {e}") + individual_results[ticker] = { + "signal": "ERROR", + "final_trade_decision": f"Analysis failed: {e}", + } + + return individual_results + + def _generate_summary( + self, + individual_results: Dict[str, Dict[str, str]], + trade_date: str, + ) -> str: + """Use the deep thinking LLM to compare all positions.""" + # Skip summary if all tickers failed + successful = { + t: r for t, r in individual_results.items() if r["signal"] != "ERROR" + } + if not successful: + return "Portfolio summary unavailable — all individual analyses failed." + + analyses_text = self._build_analyses_text(successful) + messages = [ + ("system", self._get_system_prompt()), + ( + "human", + f"Here are the individual analyses for my portfolio positions " + f"as of {trade_date}:\n{analyses_text}\n\n" + f"Please provide a comparative portfolio recommendation.", + ), + ] + + try: + return self.deep_thinking_llm.invoke(messages).content + except Exception as e: + return ( + f"Portfolio summary generation failed: {e}\n" + f"Individual signals were: " + + ", ".join(f"{t}: {r['signal']}" for t, r in individual_results.items()) + ) + + def _build_analyses_text(self, results: Dict[str, Dict[str, str]]) -> str: + """Format individual results into a text block for the LLM prompt.""" + parts = [] + for ticker, result in results.items(): + parts.append( + f"--- {ticker} ---\n" + f"Rating: {result['signal']}\n" + f"Full Analysis:\n{result['final_trade_decision']}" + ) + return "\n".join(parts) + + def _get_system_prompt(self) -> str: + """Return the system prompt for the portfolio comparison LLM call.""" + return ( + "You are a senior portfolio strategist. You have received individual " + "stock analyses for all positions in a portfolio. Your job is to compare " + "them relative to each other and provide a clear, actionable portfolio " + "recommendation.\n\n" + "For each stock, assign one of: KEEP, REDUCE, or EXIT.\n\n" + "Structure your response as:\n" + "1. A ranked summary table (best to worst) with ticker, action, and " + "one-line rationale.\n" + "2. A brief portfolio-level commentary covering overall risk exposure, " + "sector concentration, and any suggested rebalancing.\n\n" + "Be direct and concise. This is for an experienced investor." + ) + + def _log_portfolio( + self, + trade_date: str, + tickers: List[str], + individual_results: Dict[str, Dict[str, str]], + portfolio_summary: str, + ) -> None: + """Log the portfolio analysis results to a JSON file.""" + directory = Path("eval_results/portfolio/") + directory.mkdir(parents=True, exist_ok=True) + + log_data = { + "trade_date": str(trade_date), + "tickers": tickers, + "individual_results": individual_results, + "portfolio_summary": portfolio_summary, + } + + with open( + f"eval_results/portfolio/portfolio_analysis_{trade_date}.json", + "w", + encoding="utf-8", + ) as f: + json.dump(log_data, f, indent=4) diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index c8cd7492..b8c9c825 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -38,6 +38,7 @@ from .setup import GraphSetup from .propagation import Propagator from .reflection import Reflector from .signal_processing import SignalProcessor +from .portfolio_analysis import PortfolioAnalyzer class TradingAgentsGraph: @@ -124,6 +125,7 @@ class TradingAgentsGraph: self.propagator = Propagator() self.reflector = Reflector(self.quick_thinking_llm) self.signal_processor = SignalProcessor(self.quick_thinking_llm) + self.portfolio_analyzer = PortfolioAnalyzer(self.deep_thinking_llm) # State tracking self.curr_state = None @@ -269,6 +271,21 @@ class TradingAgentsGraph: ) as f: json.dump(self.log_states_dict, f, indent=4) + def propagate_portfolio( + self, tickers: List[str], trade_date: str + ) -> Dict[str, Any]: + """Run analysis on multiple stocks and produce a comparative portfolio summary. + + Delegates to PortfolioAnalyzer.analyze — see that class for full details. + + Note: Each call to propagate() overwrites self.ticker and self.curr_state, + so after this method returns, both reflect only the last ticker analyzed. + Calling reflect_and_remember() afterward will only apply to that last ticker. + """ + return self.portfolio_analyzer.analyze( + tickers, trade_date, self.propagate, debug=self.debug + ) + def reflect_and_remember(self, returns_losses): """Reflect on decisions and update memory based on returns.""" self.reflector.reflect_bull_researcher( From 03d7752d467ab7aec4dea057f43a80d67eb04ba0 Mon Sep 17 00:00:00 2001 From: robinsxe Date: Tue, 24 Mar 2026 20:18:14 +0100 Subject: [PATCH 02/20] Update tradingagents/graph/portfolio_analysis.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tradingagents/graph/portfolio_analysis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 355f6e69..15fe03cb 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -88,11 +88,12 @@ class PortfolioAnalyzer: "final_trade_decision": final_state["final_trade_decision"], } except Exception as e: + import traceback if debug: print(f"Error analyzing {ticker}: {e}") individual_results[ticker] = { "signal": "ERROR", - "final_trade_decision": f"Analysis failed: {e}", + "final_trade_decision": f"Analysis failed: {e}\n{traceback.format_exc()}", } return individual_results From dbd2c658e500768d28e488930679f26bceeb3bff Mon Sep 17 00:00:00 2001 From: robinsxe Date: Tue, 24 Mar 2026 20:18:30 +0100 Subject: [PATCH 03/20] Update tradingagents/graph/portfolio_analysis.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tradingagents/graph/portfolio_analysis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 15fe03cb..f3f2d2d2 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -125,8 +125,9 @@ class PortfolioAnalyzer: try: return self.deep_thinking_llm.invoke(messages).content except Exception as e: + import traceback return ( - f"Portfolio summary generation failed: {e}\n" + f"Portfolio summary generation failed: {e}\n{traceback.format_exc()}\n" f"Individual signals were: " + ", ".join(f"{t}: {r['signal']}" for t, r in individual_results.items()) ) From 85fbc48edecd42827193b9a481f9cfb85b4b9316 Mon Sep 17 00:00:00 2001 From: Robin Lindbladh Date: Tue, 24 Mar 2026 20:20:46 +0100 Subject: [PATCH 04/20] fix: address code review feedback - Preserve and restore self.ticker and self.curr_state in propagate_portfolio() using try/finally to prevent side effects - Use pathlib.Path for log file construction in _log_portfolio() - Move traceback import to module level Co-Authored-By: Claude Opus 4.6 (1M context) --- tradingagents/graph/portfolio_analysis.py | 9 +++------ tradingagents/graph/trading_graph.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index f3f2d2d2..a0b96a46 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -1,6 +1,7 @@ # TradingAgents/graph/portfolio_analysis.py import json +import traceback from pathlib import Path from typing import Any, Callable, Dict, List, Tuple @@ -88,7 +89,6 @@ class PortfolioAnalyzer: "final_trade_decision": final_state["final_trade_decision"], } except Exception as e: - import traceback if debug: print(f"Error analyzing {ticker}: {e}") individual_results[ticker] = { @@ -177,9 +177,6 @@ class PortfolioAnalyzer: "portfolio_summary": portfolio_summary, } - with open( - f"eval_results/portfolio/portfolio_analysis_{trade_date}.json", - "w", - encoding="utf-8", - ) as f: + log_file = directory / f"portfolio_analysis_{trade_date}.json" + with log_file.open("w", encoding="utf-8") as f: json.dump(log_data, f, indent=4) diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index b8c9c825..2ac1af7f 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -278,13 +278,18 @@ class TradingAgentsGraph: Delegates to PortfolioAnalyzer.analyze — see that class for full details. - Note: Each call to propagate() overwrites self.ticker and self.curr_state, - so after this method returns, both reflect only the last ticker analyzed. - Calling reflect_and_remember() afterward will only apply to that last ticker. + This method preserves the instance's ticker and curr_state attributes, + restoring them after the portfolio analysis is complete. """ - return self.portfolio_analyzer.analyze( - tickers, trade_date, self.propagate, debug=self.debug - ) + original_ticker = self.ticker + original_curr_state = self.curr_state + try: + return self.portfolio_analyzer.analyze( + tickers, trade_date, self.propagate, debug=self.debug + ) + finally: + self.ticker = original_ticker + self.curr_state = original_curr_state def reflect_and_remember(self, returns_losses): """Reflect on decisions and update memory based on returns.""" From 2466ec3c90788ff6ea13eb092a159b7559d0d4e2 Mon Sep 17 00:00:00 2001 From: robinsxe Date: Tue, 24 Mar 2026 20:32:09 +0100 Subject: [PATCH 05/20] Update tradingagents/graph/portfolio_analysis.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tradingagents/graph/portfolio_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index a0b96a46..52d1012c 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -5,7 +5,7 @@ import traceback from pathlib import Path from typing import Any, Callable, Dict, List, Tuple -from langchain_openai import ChatOpenAI +from langchain_core.language_models.chat_models import BaseChatModel class PortfolioAnalyzer: From 95e10bd1fd06e355d6d0f9234d6a67156c739b27 Mon Sep 17 00:00:00 2001 From: robinsxe Date: Tue, 24 Mar 2026 20:32:32 +0100 Subject: [PATCH 06/20] Update tradingagents/graph/portfolio_analysis.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tradingagents/graph/portfolio_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 52d1012c..96c51854 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -16,7 +16,7 @@ class PortfolioAnalyzer: owns the portfolio-level prompt, comparison logic, and logging. """ - def __init__(self, deep_thinking_llm: ChatOpenAI): + def __init__(self, deep_thinking_llm: BaseChatModel): """Initialize with the deep thinking LLM for comparative analysis. Args: From b3a087286b069ee43519a4b843feba6df1fb57a3 Mon Sep 17 00:00:00 2001 From: Robin Lindbladh Date: Tue, 24 Mar 2026 20:58:18 +0100 Subject: [PATCH 07/20] fix: remove redundant inline traceback import Co-Authored-By: Claude Opus 4.6 (1M context) --- tradingagents/graph/portfolio_analysis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 96c51854..52da027f 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -125,7 +125,6 @@ class PortfolioAnalyzer: try: return self.deep_thinking_llm.invoke(messages).content except Exception as e: - import traceback return ( f"Portfolio summary generation failed: {e}\n{traceback.format_exc()}\n" f"Individual signals were: " From 0c4a912b0a60e1f53cf6607eff35f59aae105e86 Mon Sep 17 00:00:00 2001 From: robinsxe Date: Tue, 24 Mar 2026 21:01:25 +0100 Subject: [PATCH 08/20] Update tradingagents/graph/portfolio_analysis.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tradingagents/graph/portfolio_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 52da027f..356c961d 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -176,6 +176,6 @@ class PortfolioAnalyzer: "portfolio_summary": portfolio_summary, } - log_file = directory / f"portfolio_analysis_{trade_date}.json" + log_file = directory / f"portfolio_analysis_{Path(str(trade_date)).name}.json" with log_file.open("w", encoding="utf-8") as f: json.dump(log_data, f, indent=4) From 92b527b60a8298989dbf93c961d81e773246ce7b Mon Sep 17 00:00:00 2001 From: robinsxe Date: Tue, 24 Mar 2026 21:01:35 +0100 Subject: [PATCH 09/20] Update tradingagents/graph/portfolio_analysis.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tradingagents/graph/portfolio_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 356c961d..78c24044 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -70,7 +70,7 @@ class PortfolioAnalyzer: self, tickers: List[str], trade_date: str, - propagate_fn: Callable, + propagate_fn: Callable[[str, str], Tuple[Dict[str, Any], str]], debug: bool, ) -> Dict[str, Dict[str, str]]: """Run the agent pipeline on each ticker, collecting results.""" From 698b4ede4a1f43c8c473f081b9a22474b9d29333 Mon Sep 17 00:00:00 2001 From: robinsxe Date: Tue, 24 Mar 2026 21:06:45 +0100 Subject: [PATCH 10/20] Update tradingagents/graph/portfolio_analysis.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tradingagents/graph/portfolio_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 78c24044..e03e8f5b 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -166,7 +166,7 @@ class PortfolioAnalyzer: portfolio_summary: str, ) -> None: """Log the portfolio analysis results to a JSON file.""" - directory = Path("eval_results/portfolio/") + directory = Path(self.config.get("portfolio_log_dir", "eval_results/portfolio/")) directory.mkdir(parents=True, exist_ok=True) log_data = { From f3d49335d11de5917692c7761cae7bc70551766c Mon Sep 17 00:00:00 2001 From: robinsxe Date: Tue, 24 Mar 2026 21:07:02 +0100 Subject: [PATCH 11/20] Update tradingagents/graph/trading_graph.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tradingagents/graph/trading_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index 2ac1af7f..7db0624e 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -125,7 +125,7 @@ class TradingAgentsGraph: self.propagator = Propagator() self.reflector = Reflector(self.quick_thinking_llm) self.signal_processor = SignalProcessor(self.quick_thinking_llm) - self.portfolio_analyzer = PortfolioAnalyzer(self.deep_thinking_llm) + self.portfolio_analyzer = PortfolioAnalyzer(self.deep_thinking_llm, self.config) # State tracking self.curr_state = None From 5ac72567be903b056dfa531fe9b975d1e8ce4cfc Mon Sep 17 00:00:00 2001 From: Robin Lindbladh Date: Tue, 24 Mar 2026 21:08:14 +0100 Subject: [PATCH 12/20] fix: add config parameter to PortfolioAnalyzer constructor Required by the configurable log directory and config passthrough from TradingAgentsGraph that were applied via GitHub suggestions. Co-Authored-By: Claude Opus 4.6 (1M context) --- tradingagents/graph/portfolio_analysis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index e03e8f5b..2af2a2bf 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -16,13 +16,15 @@ class PortfolioAnalyzer: owns the portfolio-level prompt, comparison logic, and logging. """ - def __init__(self, deep_thinking_llm: BaseChatModel): + def __init__(self, deep_thinking_llm: BaseChatModel, config: Dict[str, Any]): """Initialize with the deep thinking LLM for comparative analysis. Args: deep_thinking_llm: The LLM instance used for the portfolio summary. + config: The configuration dictionary for the application. """ self.deep_thinking_llm = deep_thinking_llm + self.config = config def analyze( self, From 3abff48c7de6c75c64d0efb36e483a36a79621c1 Mon Sep 17 00:00:00 2001 From: Robin Lindbladh Date: Tue, 24 Mar 2026 21:10:33 +0100 Subject: [PATCH 13/20] fix: protect log file write and preserve log_states_dict - Wrap _log_portfolio file I/O in try/except so a write failure doesn't discard the analysis results - Preserve and restore self.log_states_dict in propagate_portfolio() alongside ticker and curr_state Co-Authored-By: Claude Opus 4.6 (1M context) --- tradingagents/graph/portfolio_analysis.py | 6 +++++- tradingagents/graph/trading_graph.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 2af2a2bf..6cf3f709 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -61,7 +61,11 @@ class PortfolioAnalyzer: individual_results, trade_date ) - self._log_portfolio(trade_date, tickers, individual_results, portfolio_summary) + try: + self._log_portfolio(trade_date, tickers, individual_results, portfolio_summary) + except OSError as e: + if debug: + print(f"Warning: failed to save portfolio log: {e}") return { "individual_results": individual_results, diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index 7db0624e..6362dc49 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -283,6 +283,7 @@ class TradingAgentsGraph: """ original_ticker = self.ticker original_curr_state = self.curr_state + original_log_states = self.log_states_dict.copy() try: return self.portfolio_analyzer.analyze( tickers, trade_date, self.propagate, debug=self.debug @@ -290,6 +291,7 @@ class TradingAgentsGraph: finally: self.ticker = original_ticker self.curr_state = original_curr_state + self.log_states_dict = original_log_states def reflect_and_remember(self, returns_losses): """Reflect on decisions and update memory based on returns.""" From 2ce7e2b6d022e0acd90946daacb9ffde45935f8f Mon Sep 17 00:00:00 2001 From: Robin Lindbladh Date: Tue, 24 Mar 2026 21:11:49 +0100 Subject: [PATCH 14/20] fix: broaden _log_portfolio exception catch to handle all failures OSError only covers file I/O errors; json.dump can also raise TypeError on non-serializable data. Use Exception to ensure logging failures never discard analysis results. Co-Authored-By: Claude Opus 4.6 (1M context) --- tradingagents/graph/portfolio_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 6cf3f709..0c4bc296 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -63,7 +63,7 @@ class PortfolioAnalyzer: try: self._log_portfolio(trade_date, tickers, individual_results, portfolio_summary) - except OSError as e: + except Exception as e: if debug: print(f"Warning: failed to save portfolio log: {e}") From 6f5610d82b07d53c74d50246281ea78e5dc16c58 Mon Sep 17 00:00:00 2001 From: robinsxe Date: Tue, 24 Mar 2026 21:14:42 +0100 Subject: [PATCH 15/20] Update tradingagents/graph/portfolio_analysis.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tradingagents/graph/portfolio_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 0c4bc296..8e3e3451 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -182,6 +182,6 @@ class PortfolioAnalyzer: "portfolio_summary": portfolio_summary, } - log_file = directory / f"portfolio_analysis_{Path(str(trade_date)).name}.json" + log_file = directory / f"portfolio_analysis_{str(trade_date).replace('/', '-')}.json" with log_file.open("w", encoding="utf-8") as f: json.dump(log_data, f, indent=4) From 5d09c4c9841c25485250fbe7609700de34aae0b1 Mon Sep 17 00:00:00 2001 From: Robin Lindbladh Date: Tue, 24 Mar 2026 21:16:03 +0100 Subject: [PATCH 16/20] fix: gate tracebacks behind debug flag to prevent info leakage Only include full tracebacks in error messages when debug=True. In non-debug mode, return clean error messages without internal implementation details. Co-Authored-By: Claude Opus 4.6 (1M context) --- tradingagents/graph/portfolio_analysis.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 8e3e3451..cc7208ea 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -58,7 +58,7 @@ class PortfolioAnalyzer: ) portfolio_summary = self._generate_summary( - individual_results, trade_date + individual_results, trade_date, debug ) try: @@ -97,9 +97,12 @@ class PortfolioAnalyzer: except Exception as e: if debug: print(f"Error analyzing {ticker}: {e}") + error_msg = f"Analysis failed: {e}" + if debug: + error_msg += f"\n{traceback.format_exc()}" individual_results[ticker] = { "signal": "ERROR", - "final_trade_decision": f"Analysis failed: {e}\n{traceback.format_exc()}", + "final_trade_decision": error_msg, } return individual_results @@ -108,6 +111,7 @@ class PortfolioAnalyzer: self, individual_results: Dict[str, Dict[str, str]], trade_date: str, + debug: bool = False, ) -> str: """Use the deep thinking LLM to compare all positions.""" # Skip summary if all tickers failed @@ -131,11 +135,11 @@ class PortfolioAnalyzer: try: return self.deep_thinking_llm.invoke(messages).content except Exception as e: - return ( - f"Portfolio summary generation failed: {e}\n{traceback.format_exc()}\n" - f"Individual signals were: " - + ", ".join(f"{t}: {r['signal']}" for t, r in individual_results.items()) - ) + error_msg = f"Portfolio summary generation failed: {e}" + if debug: + error_msg += f"\n{traceback.format_exc()}" + signals = ", ".join(f"{t}: {r['signal']}" for t, r in individual_results.items()) + return f"{error_msg}\nIndividual signals were: {signals}" def _build_analyses_text(self, results: Dict[str, Dict[str, str]]) -> str: """Format individual results into a text block for the LLM prompt.""" From 59a2212ff71121fd4b77738b1e30f2e470404fb9 Mon Sep 17 00:00:00 2001 From: robinsxe Date: Tue, 24 Mar 2026 22:44:24 +0100 Subject: [PATCH 17/20] Update tradingagents/graph/portfolio_analysis.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tradingagents/graph/portfolio_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index cc7208ea..728985d8 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -186,6 +186,6 @@ class PortfolioAnalyzer: "portfolio_summary": portfolio_summary, } - log_file = directory / f"portfolio_analysis_{str(trade_date).replace('/', '-')}.json" + log_file = directory / f"portfolio_analysis_{re.sub(r'[^\w.-]', '_', str(trade_date))}.json" with log_file.open("w", encoding="utf-8") as f: json.dump(log_data, f, indent=4) From 2648f91e099678f566778b0f495dec7f0ebe4fba Mon Sep 17 00:00:00 2001 From: robinsxe Date: Tue, 24 Mar 2026 22:52:57 +0100 Subject: [PATCH 18/20] Update tradingagents/graph/portfolio_analysis.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tradingagents/graph/portfolio_analysis.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 728985d8..6409258b 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -1,6 +1,7 @@ # TradingAgents/graph/portfolio_analysis.py import json +import re import traceback from pathlib import Path from typing import Any, Callable, Dict, List, Tuple From 138c077cc66f77cfb20951aa70a6765d2cb3a3c9 Mon Sep 17 00:00:00 2001 From: Robin Lindbladh Date: Thu, 26 Mar 2026 18:15:27 +0100 Subject: [PATCH 19/20] fix: stop restoring log_states_dict after portfolio analysis The log_states_dict is meant to accumulate per-ticker state logs. Restoring it after propagate_portfolio() was discarding all the detailed logs generated during the portfolio run. Co-Authored-By: Claude Opus 4.6 (1M context) --- tradingagents/graph/trading_graph.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index 6362dc49..7db0624e 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -283,7 +283,6 @@ class TradingAgentsGraph: """ original_ticker = self.ticker original_curr_state = self.curr_state - original_log_states = self.log_states_dict.copy() try: return self.portfolio_analyzer.analyze( tickers, trade_date, self.propagate, debug=self.debug @@ -291,7 +290,6 @@ class TradingAgentsGraph: finally: self.ticker = original_ticker self.curr_state = original_curr_state - self.log_states_dict = original_log_states def reflect_and_remember(self, returns_losses): """Reflect on decisions and update memory based on returns.""" From ccf375eafd6d7bff39b1f89eb1d316a1137fd2cb Mon Sep 17 00:00:00 2001 From: robinsxe Date: Thu, 26 Mar 2026 18:20:23 +0100 Subject: [PATCH 20/20] Update tradingagents/graph/portfolio_analysis.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tradingagents/graph/portfolio_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tradingagents/graph/portfolio_analysis.py b/tradingagents/graph/portfolio_analysis.py index 6409258b..12325e3c 100644 --- a/tradingagents/graph/portfolio_analysis.py +++ b/tradingagents/graph/portfolio_analysis.py @@ -181,12 +181,12 @@ class PortfolioAnalyzer: directory.mkdir(parents=True, exist_ok=True) log_data = { - "trade_date": str(trade_date), + "trade_date": trade_date, "tickers": tickers, "individual_results": individual_results, "portfolio_summary": portfolio_summary, } - log_file = directory / f"portfolio_analysis_{re.sub(r'[^\w.-]', '_', str(trade_date))}.json" + log_file = directory / f"portfolio_analysis_{re.sub(r'[^\w.-]', '_', trade_date)}.json" with log_file.open("w", encoding="utf-8") as f: json.dump(log_data, f, indent=4)