From 08bfe70a6990a2756e7fefdd48a45c41e0f2f42f Mon Sep 17 00:00:00 2001 From: CadeYu Date: Sat, 21 Mar 2026 13:10:09 +0800 Subject: [PATCH 01/30] fix: preserve exchange-qualified tickers across agent prompts --- cli/main.py | 4 +++- cli/utils.py | 9 ++++++++- tests/test_ticker_symbol_handling.py | 18 ++++++++++++++++++ .../agents/analysts/fundamentals_analyst.py | 17 ++++++++++++----- .../agents/analysts/market_analyst.py | 14 +++++++++----- tradingagents/agents/analysts/news_analyst.py | 13 +++++++++---- .../agents/analysts/social_media_analyst.py | 12 ++++++------ .../agents/managers/research_manager.py | 6 ++++++ tradingagents/agents/managers/risk_manager.py | 5 +++++ tradingagents/agents/trader/trader.py | 5 ++++- tradingagents/agents/utils/agent_utils.py | 12 +++++++++++- 11 files changed, 91 insertions(+), 24 deletions(-) create mode 100644 tests/test_ticker_symbol_handling.py diff --git a/cli/main.py b/cli/main.py index adda48fc..e51a0e7b 100644 --- a/cli/main.py +++ b/cli/main.py @@ -501,7 +501,9 @@ def get_user_selections(): # Step 1: Ticker symbol console.print( create_question_box( - "Step 1: Ticker Symbol", "Enter the ticker symbol to analyze", "SPY" + "Step 1: Ticker Symbol", + "Enter the exact ticker symbol to analyze, including exchange suffix when needed (examples: SPY, CNC.TO, 7203.T, 0700.HK)", + "SPY", ) ) selected_ticker = get_ticker() diff --git a/cli/utils.py b/cli/utils.py index 5a8ec16c..b276cc0d 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -7,6 +7,8 @@ from cli.models import AnalystType console = Console() +TICKER_INPUT_EXAMPLES = "Examples: SPY, CNC.TO, 7203.T, 0700.HK" + ANALYST_ORDER = [ ("Market Analyst", AnalystType.MARKET), ("Social Media Analyst", AnalystType.SOCIAL), @@ -18,7 +20,7 @@ ANALYST_ORDER = [ def get_ticker() -> str: """Prompt the user to enter a ticker symbol.""" ticker = questionary.text( - "Enter the ticker symbol to analyze:", + f"Enter the exact ticker symbol to analyze ({TICKER_INPUT_EXAMPLES}):", validate=lambda x: len(x.strip()) > 0 or "Please enter a valid ticker symbol.", style=questionary.Style( [ @@ -32,6 +34,11 @@ def get_ticker() -> str: console.print("\n[red]No ticker symbol provided. Exiting...[/red]") exit(1) + return normalize_ticker_symbol(ticker) + + +def normalize_ticker_symbol(ticker: str) -> str: + """Normalize ticker input while preserving exchange suffixes.""" return ticker.strip().upper() diff --git a/tests/test_ticker_symbol_handling.py b/tests/test_ticker_symbol_handling.py new file mode 100644 index 00000000..858d26cd --- /dev/null +++ b/tests/test_ticker_symbol_handling.py @@ -0,0 +1,18 @@ +import unittest + +from cli.utils import normalize_ticker_symbol +from tradingagents.agents.utils.agent_utils import build_instrument_context + + +class TickerSymbolHandlingTests(unittest.TestCase): + def test_normalize_ticker_symbol_preserves_exchange_suffix(self): + self.assertEqual(normalize_ticker_symbol(" cnc.to "), "CNC.TO") + + def test_build_instrument_context_mentions_exact_symbol(self): + context = build_instrument_context("7203.T") + self.assertIn("7203.T", context) + self.assertIn("exchange suffix", context) + + +if __name__ == "__main__": + unittest.main() diff --git a/tradingagents/agents/analysts/fundamentals_analyst.py b/tradingagents/agents/analysts/fundamentals_analyst.py index 22d91848..abbe70eb 100644 --- a/tradingagents/agents/analysts/fundamentals_analyst.py +++ b/tradingagents/agents/analysts/fundamentals_analyst.py @@ -1,7 +1,14 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder import time import json -from tradingagents.agents.utils.agent_utils import get_fundamentals, get_balance_sheet, get_cashflow, get_income_statement, get_insider_transactions +from tradingagents.agents.utils.agent_utils import ( + build_instrument_context, + get_balance_sheet, + get_cashflow, + get_fundamentals, + get_income_statement, + get_insider_transactions, +) from tradingagents.dataflows.config import get_config @@ -9,7 +16,7 @@ def create_fundamentals_analyst(llm): def fundamentals_analyst_node(state): current_date = state["trade_date"] ticker = state["company_of_interest"] - company_name = state["company_of_interest"] + instrument_context = build_instrument_context(ticker) tools = [ get_fundamentals, @@ -19,7 +26,7 @@ def create_fundamentals_analyst(llm): ] system_message = ( - "You are a researcher tasked with analyzing fundamental information over the past week about a company. Please write a comprehensive report of the company's fundamental information such as financial documents, company profile, basic company financials, and company financial history to gain a full view of the company's fundamental information to inform traders. Make sure to include as much detail as possible. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions." + "You are a researcher tasked with analyzing fundamental information over the past week about a company. Please write a comprehensive report of the company's fundamental information such as financial documents, company profile, basic company financials, and company financial history to gain a full view of the company's fundamental information to inform traders. Make sure to include as much detail as possible. Always preserve the exact ticker symbol provided by the user, including any exchange suffix, and never merge fundamentals for similarly named companies from other exchanges. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions." + " Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read." + " Use the available tools: `get_fundamentals` for comprehensive company analysis, `get_balance_sheet`, `get_cashflow`, and `get_income_statement` for specific financial statements.", ) @@ -35,7 +42,7 @@ def create_fundamentals_analyst(llm): " If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable," " prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop." " You have access to the following tools: {tool_names}.\n{system_message}" - "For your reference, the current date is {current_date}. The company we want to look at is {ticker}", + "For your reference, the current date is {current_date}. {instrument_context}", ), MessagesPlaceholder(variable_name="messages"), ] @@ -44,7 +51,7 @@ def create_fundamentals_analyst(llm): prompt = prompt.partial(system_message=system_message) prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools])) prompt = prompt.partial(current_date=current_date) - prompt = prompt.partial(ticker=ticker) + prompt = prompt.partial(instrument_context=instrument_context) chain = prompt | llm.bind_tools(tools) diff --git a/tradingagents/agents/analysts/market_analyst.py b/tradingagents/agents/analysts/market_analyst.py index e175b94e..e3f09ab4 100644 --- a/tradingagents/agents/analysts/market_analyst.py +++ b/tradingagents/agents/analysts/market_analyst.py @@ -1,7 +1,11 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder import time import json -from tradingagents.agents.utils.agent_utils import get_stock_data, get_indicators +from tradingagents.agents.utils.agent_utils import ( + build_instrument_context, + get_indicators, + get_stock_data, +) from tradingagents.dataflows.config import get_config @@ -10,7 +14,7 @@ def create_market_analyst(llm): def market_analyst_node(state): current_date = state["trade_date"] ticker = state["company_of_interest"] - company_name = state["company_of_interest"] + instrument_context = build_instrument_context(ticker) tools = [ get_stock_data, @@ -42,7 +46,7 @@ Volatility Indicators: Volume-Based Indicators: - vwma: VWMA: A moving average weighted by volume. Usage: Confirm trends by integrating price action with volume data. Tips: Watch for skewed results from volume spikes; use in combination with other volume analyses. -- Select indicators that provide diverse and complementary information. Avoid redundancy (e.g., do not select both rsi and stochrsi). Also briefly explain why they are suitable for the given market context. When you tool call, please use the exact name of the indicators provided above as they are defined parameters, otherwise your call will fail. Please make sure to call get_stock_data first to retrieve the CSV that is needed to generate indicators. Then use get_indicators with the specific indicator names. Write a very detailed and nuanced report of the trends you observe. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions.""" +- Select indicators that provide diverse and complementary information. Avoid redundancy (e.g., do not select both rsi and stochrsi). Also briefly explain why they are suitable for the given market context. When you tool call, please use the exact name of the indicators provided above as they are defined parameters, otherwise your call will fail. Please make sure to call get_stock_data first to retrieve the CSV that is needed to generate indicators. Then use get_indicators with the specific indicator names. Always preserve the exact ticker symbol provided by the user, including any exchange suffix, and never mix in similarly named companies from other exchanges. Write a very detailed and nuanced report of the trends you observe. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions.""" + """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read.""" ) @@ -57,7 +61,7 @@ Volume-Based Indicators: " If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable," " prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop." " You have access to the following tools: {tool_names}.\n{system_message}" - "For your reference, the current date is {current_date}. The company we want to look at is {ticker}", + "For your reference, the current date is {current_date}. {instrument_context}", ), MessagesPlaceholder(variable_name="messages"), ] @@ -66,7 +70,7 @@ Volume-Based Indicators: prompt = prompt.partial(system_message=system_message) prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools])) prompt = prompt.partial(current_date=current_date) - prompt = prompt.partial(ticker=ticker) + prompt = prompt.partial(instrument_context=instrument_context) chain = prompt | llm.bind_tools(tools) diff --git a/tradingagents/agents/analysts/news_analyst.py b/tradingagents/agents/analysts/news_analyst.py index 03b4fae4..c65de0b8 100644 --- a/tradingagents/agents/analysts/news_analyst.py +++ b/tradingagents/agents/analysts/news_analyst.py @@ -1,7 +1,11 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder import time import json -from tradingagents.agents.utils.agent_utils import get_news, get_global_news +from tradingagents.agents.utils.agent_utils import ( + build_instrument_context, + get_global_news, + get_news, +) from tradingagents.dataflows.config import get_config @@ -9,6 +13,7 @@ def create_news_analyst(llm): def news_analyst_node(state): current_date = state["trade_date"] ticker = state["company_of_interest"] + instrument_context = build_instrument_context(ticker) tools = [ get_news, @@ -16,7 +21,7 @@ def create_news_analyst(llm): ] system_message = ( - "You are a news researcher tasked with analyzing recent news and trends over the past week. Please write a comprehensive report of the current state of the world that is relevant for trading and macroeconomics. Use the available tools: get_news(query, start_date, end_date) for company-specific or targeted news searches, and get_global_news(curr_date, look_back_days, limit) for broader macroeconomic news. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions." + "You are a news researcher tasked with analyzing recent news and trends over the past week. Please write a comprehensive report of the current state of the world that is relevant for trading and macroeconomics. Use the available tools: get_news(query, start_date, end_date) for company-specific or targeted news searches, and get_global_news(curr_date, look_back_days, limit) for broader macroeconomic news. Always preserve the exact ticker symbol provided by the user, including any exchange suffix, and never merge news for similarly named companies from other exchanges. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions." + """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read.""" ) @@ -31,7 +36,7 @@ def create_news_analyst(llm): " If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable," " prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop." " You have access to the following tools: {tool_names}.\n{system_message}" - "For your reference, the current date is {current_date}. We are looking at the company {ticker}", + "For your reference, the current date is {current_date}. {instrument_context}", ), MessagesPlaceholder(variable_name="messages"), ] @@ -40,7 +45,7 @@ def create_news_analyst(llm): prompt = prompt.partial(system_message=system_message) prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools])) prompt = prompt.partial(current_date=current_date) - prompt = prompt.partial(ticker=ticker) + prompt = prompt.partial(instrument_context=instrument_context) chain = prompt | llm.bind_tools(tools) result = chain.invoke(state["messages"]) diff --git a/tradingagents/agents/analysts/social_media_analyst.py b/tradingagents/agents/analysts/social_media_analyst.py index b25712d7..de57dfe3 100644 --- a/tradingagents/agents/analysts/social_media_analyst.py +++ b/tradingagents/agents/analysts/social_media_analyst.py @@ -1,7 +1,7 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder import time import json -from tradingagents.agents.utils.agent_utils import get_news +from tradingagents.agents.utils.agent_utils import build_instrument_context, get_news from tradingagents.dataflows.config import get_config @@ -9,15 +9,15 @@ def create_social_media_analyst(llm): def social_media_analyst_node(state): current_date = state["trade_date"] ticker = state["company_of_interest"] - company_name = state["company_of_interest"] + instrument_context = build_instrument_context(ticker) tools = [ get_news, ] system_message = ( - "You are a social media and company specific news researcher/analyst tasked with analyzing social media posts, recent company news, and public sentiment for a specific company over the past week. You will be given a company's name your objective is to write a comprehensive long report detailing your analysis, insights, and implications for traders and investors on this company's current state after looking at social media and what people are saying about that company, analyzing sentiment data of what people feel each day about the company, and looking at recent company news. Use the get_news(query, start_date, end_date) tool to search for company-specific news and social media discussions. Try to look at all sources possible from social media to sentiment to news. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions." - + """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read.""", + "You are a social media and company specific news researcher/analyst tasked with analyzing social media posts, recent company news, and public sentiment for a specific company over the past week. You will be given a company's name your objective is to write a comprehensive long report detailing your analysis, insights, and implications for traders and investors on this company's current state after looking at social media and what people are saying about that company, analyzing sentiment data of what people feel each day about the company, and looking at recent company news. Use the get_news(query, start_date, end_date) tool to search for company-specific news and social media discussions. Try to look at all sources possible from social media to sentiment to news. Always preserve the exact ticker symbol provided by the user, including any exchange suffix, and never merge commentary for similarly named companies from other exchanges. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions." + + """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read.""" ) prompt = ChatPromptTemplate.from_messages( @@ -31,7 +31,7 @@ def create_social_media_analyst(llm): " If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable," " prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop." " You have access to the following tools: {tool_names}.\n{system_message}" - "For your reference, the current date is {current_date}. The current company we want to analyze is {ticker}", + "For your reference, the current date is {current_date}. {instrument_context}", ), MessagesPlaceholder(variable_name="messages"), ] @@ -40,7 +40,7 @@ def create_social_media_analyst(llm): prompt = prompt.partial(system_message=system_message) prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools])) prompt = prompt.partial(current_date=current_date) - prompt = prompt.partial(ticker=ticker) + prompt = prompt.partial(instrument_context=instrument_context) chain = prompt | llm.bind_tools(tools) diff --git a/tradingagents/agents/managers/research_manager.py b/tradingagents/agents/managers/research_manager.py index c537fa2f..86abf195 100644 --- a/tradingagents/agents/managers/research_manager.py +++ b/tradingagents/agents/managers/research_manager.py @@ -1,9 +1,13 @@ import time import json +from tradingagents.agents.utils.agent_utils import build_instrument_context + def create_research_manager(llm, memory): def research_manager_node(state) -> dict: + ticker = state["company_of_interest"] + instrument_context = build_instrument_context(ticker) history = state["investment_debate_state"].get("history", "") market_research_report = state["market_report"] sentiment_report = state["sentiment_report"] @@ -33,6 +37,8 @@ Take into account your past mistakes on similar situations. Use these insights t Here are your past reflections on mistakes: \"{past_memory_str}\" +{instrument_context} + Here is the debate: Debate History: {history}""" diff --git a/tradingagents/agents/managers/risk_manager.py b/tradingagents/agents/managers/risk_manager.py index 1f2334cc..5c3e0543 100644 --- a/tradingagents/agents/managers/risk_manager.py +++ b/tradingagents/agents/managers/risk_manager.py @@ -1,11 +1,14 @@ import time import json +from tradingagents.agents.utils.agent_utils import build_instrument_context + def create_risk_manager(llm, memory): def risk_manager_node(state) -> dict: company_name = state["company_of_interest"] + instrument_context = build_instrument_context(company_name) history = state["risk_debate_state"]["history"] risk_debate_state = state["risk_debate_state"] @@ -34,6 +37,8 @@ Deliverables: - A clear and actionable recommendation: Buy, Sell, or Hold. - Detailed reasoning anchored in the debate and past reflections. +{instrument_context} + --- **Analysts Debate History:** diff --git a/tradingagents/agents/trader/trader.py b/tradingagents/agents/trader/trader.py index 1b05c35d..a40eb22a 100644 --- a/tradingagents/agents/trader/trader.py +++ b/tradingagents/agents/trader/trader.py @@ -2,10 +2,13 @@ import functools import time import json +from tradingagents.agents.utils.agent_utils import build_instrument_context + def create_trader(llm, memory): def trader_node(state, name): company_name = state["company_of_interest"] + instrument_context = build_instrument_context(company_name) investment_plan = state["investment_plan"] market_research_report = state["market_report"] sentiment_report = state["sentiment_report"] @@ -24,7 +27,7 @@ def create_trader(llm, memory): context = { "role": "user", - "content": f"Based on a comprehensive analysis by a team of analysts, here is an investment plan tailored for {company_name}. This plan incorporates insights from current technical market trends, macroeconomic indicators, and social media sentiment. Use this plan as a foundation for evaluating your next trading decision.\n\nProposed Investment Plan: {investment_plan}\n\nLeverage these insights to make an informed and strategic decision.", + "content": f"Based on a comprehensive analysis by a team of analysts, here is an investment plan tailored for {company_name}. {instrument_context} This plan incorporates insights from current technical market trends, macroeconomic indicators, and social media sentiment. Use this plan as a foundation for evaluating your next trading decision.\n\nProposed Investment Plan: {investment_plan}\n\nLeverage these insights to make an informed and strategic decision.", } messages = [ diff --git a/tradingagents/agents/utils/agent_utils.py b/tradingagents/agents/utils/agent_utils.py index b329a3e9..073b209f 100644 --- a/tradingagents/agents/utils/agent_utils.py +++ b/tradingagents/agents/utils/agent_utils.py @@ -19,6 +19,16 @@ from tradingagents.agents.utils.news_data_tools import ( get_global_news ) + +def build_instrument_context(ticker: str) -> str: + """Describe the exact instrument so agents avoid cross-exchange symbol mixups.""" + return ( + f"The exact listed instrument to analyze is `{ticker}`. " + "Use this exact ticker in every tool call, report, and recommendation. " + "If it includes an exchange suffix such as `.TO`, `.L`, `.HK`, or `.T`, preserve that suffix and do not mix in companies from other exchanges that share the same root symbol. " + "If it does not include a suffix, do not invent one." + ) + def create_msg_delete(): def delete_messages(state): """Clear messages and add placeholder for Anthropic compatibility""" @@ -35,4 +45,4 @@ def create_msg_delete(): return delete_messages - \ No newline at end of file + From 7d200d834ab8cbd109d518d01d9f2103edfa0a5f Mon Sep 17 00:00:00 2001 From: CadeYu Date: Sat, 21 Mar 2026 21:31:38 +0800 Subject: [PATCH 02/30] style: inline single-use instrument context vars --- tradingagents/agents/analysts/fundamentals_analyst.py | 3 +-- tradingagents/agents/analysts/market_analyst.py | 3 +-- tradingagents/agents/analysts/news_analyst.py | 3 +-- tradingagents/agents/analysts/social_media_analyst.py | 3 +-- tradingagents/agents/managers/research_manager.py | 3 +-- tradingagents/agents/managers/risk_manager.py | 3 +-- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/tradingagents/agents/analysts/fundamentals_analyst.py b/tradingagents/agents/analysts/fundamentals_analyst.py index abbe70eb..ddf57abd 100644 --- a/tradingagents/agents/analysts/fundamentals_analyst.py +++ b/tradingagents/agents/analysts/fundamentals_analyst.py @@ -15,8 +15,7 @@ from tradingagents.dataflows.config import get_config def create_fundamentals_analyst(llm): def fundamentals_analyst_node(state): current_date = state["trade_date"] - ticker = state["company_of_interest"] - instrument_context = build_instrument_context(ticker) + instrument_context = build_instrument_context(state["company_of_interest"]) tools = [ get_fundamentals, diff --git a/tradingagents/agents/analysts/market_analyst.py b/tradingagents/agents/analysts/market_analyst.py index e3f09ab4..8c1a9ab7 100644 --- a/tradingagents/agents/analysts/market_analyst.py +++ b/tradingagents/agents/analysts/market_analyst.py @@ -13,8 +13,7 @@ def create_market_analyst(llm): def market_analyst_node(state): current_date = state["trade_date"] - ticker = state["company_of_interest"] - instrument_context = build_instrument_context(ticker) + instrument_context = build_instrument_context(state["company_of_interest"]) tools = [ get_stock_data, diff --git a/tradingagents/agents/analysts/news_analyst.py b/tradingagents/agents/analysts/news_analyst.py index c65de0b8..2a3a3433 100644 --- a/tradingagents/agents/analysts/news_analyst.py +++ b/tradingagents/agents/analysts/news_analyst.py @@ -12,8 +12,7 @@ from tradingagents.dataflows.config import get_config def create_news_analyst(llm): def news_analyst_node(state): current_date = state["trade_date"] - ticker = state["company_of_interest"] - instrument_context = build_instrument_context(ticker) + instrument_context = build_instrument_context(state["company_of_interest"]) tools = [ get_news, diff --git a/tradingagents/agents/analysts/social_media_analyst.py b/tradingagents/agents/analysts/social_media_analyst.py index de57dfe3..4a6e0074 100644 --- a/tradingagents/agents/analysts/social_media_analyst.py +++ b/tradingagents/agents/analysts/social_media_analyst.py @@ -8,8 +8,7 @@ from tradingagents.dataflows.config import get_config def create_social_media_analyst(llm): def social_media_analyst_node(state): current_date = state["trade_date"] - ticker = state["company_of_interest"] - instrument_context = build_instrument_context(ticker) + instrument_context = build_instrument_context(state["company_of_interest"]) tools = [ get_news, diff --git a/tradingagents/agents/managers/research_manager.py b/tradingagents/agents/managers/research_manager.py index 86abf195..3ac4b150 100644 --- a/tradingagents/agents/managers/research_manager.py +++ b/tradingagents/agents/managers/research_manager.py @@ -6,8 +6,7 @@ from tradingagents.agents.utils.agent_utils import build_instrument_context def create_research_manager(llm, memory): def research_manager_node(state) -> dict: - ticker = state["company_of_interest"] - instrument_context = build_instrument_context(ticker) + instrument_context = build_instrument_context(state["company_of_interest"]) history = state["investment_debate_state"].get("history", "") market_research_report = state["market_report"] sentiment_report = state["sentiment_report"] diff --git a/tradingagents/agents/managers/risk_manager.py b/tradingagents/agents/managers/risk_manager.py index 5c3e0543..3dab49fe 100644 --- a/tradingagents/agents/managers/risk_manager.py +++ b/tradingagents/agents/managers/risk_manager.py @@ -7,8 +7,7 @@ from tradingagents.agents.utils.agent_utils import build_instrument_context def create_risk_manager(llm, memory): def risk_manager_node(state) -> dict: - company_name = state["company_of_interest"] - instrument_context = build_instrument_context(company_name) + instrument_context = build_instrument_context(state["company_of_interest"]) history = state["risk_debate_state"]["history"] risk_debate_state = state["risk_debate_state"] From 3ff28f3559c3598cd5efb6f3469735e14ce7f3e0 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 22 Mar 2026 20:34:03 +0000 Subject: [PATCH 03/30] fix: use OpenAI Responses API for native models Enable use_responses_api for native OpenAI provider, which supports reasoning_effort with function tools across all model families. Removes the UnifiedChatOpenAI subclass workaround. Closes #403 --- tradingagents/llm_clients/openai_client.py | 69 +++++++++++----------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/tradingagents/llm_clients/openai_client.py b/tradingagents/llm_clients/openai_client.py index 4605c1f9..c314d077 100644 --- a/tradingagents/llm_clients/openai_client.py +++ b/tradingagents/llm_clients/openai_client.py @@ -6,28 +6,28 @@ from langchain_openai import ChatOpenAI from .base_client import BaseLLMClient from .validators import validate_model +# Kwargs forwarded from user config to ChatOpenAI +_PASSTHROUGH_KWARGS = ( + "timeout", "max_retries", "reasoning_effort", + "api_key", "callbacks", "http_client", "http_async_client", +) -class UnifiedChatOpenAI(ChatOpenAI): - """ChatOpenAI subclass that strips temperature/top_p for GPT-5 family models. - - GPT-5 family models use reasoning natively. temperature/top_p are only - accepted when reasoning.effort is 'none'; with any other effort level - (or for older GPT-5/GPT-5-mini/GPT-5-nano which always reason) the API - rejects these params. Langchain defaults temperature=0.7, so we must - strip it to avoid errors. - - Non-GPT-5 models (GPT-4.1, xAI, Ollama, etc.) are unaffected. - """ - - def __init__(self, **kwargs): - if "gpt-5" in kwargs.get("model", "").lower(): - kwargs.pop("temperature", None) - kwargs.pop("top_p", None) - super().__init__(**kwargs) +# Provider base URLs and API key env vars +_PROVIDER_CONFIG = { + "xai": ("https://api.x.ai/v1", "XAI_API_KEY"), + "openrouter": ("https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"), + "ollama": ("http://localhost:11434/v1", None), +} class OpenAIClient(BaseLLMClient): - """Client for OpenAI, Ollama, OpenRouter, and xAI providers.""" + """Client for OpenAI, Ollama, OpenRouter, and xAI providers. + + For native OpenAI models, uses the Responses API (/v1/responses) which + supports reasoning_effort with function tools across all model families + (GPT-4.1, GPT-5). Third-party compatible providers (xAI, OpenRouter, + Ollama) use standard Chat Completions. + """ def __init__( self, @@ -43,27 +43,30 @@ class OpenAIClient(BaseLLMClient): """Return configured ChatOpenAI instance.""" llm_kwargs = {"model": self.model} - if self.provider == "xai": - llm_kwargs["base_url"] = "https://api.x.ai/v1" - api_key = os.environ.get("XAI_API_KEY") - if api_key: - llm_kwargs["api_key"] = api_key - elif self.provider == "openrouter": - llm_kwargs["base_url"] = "https://openrouter.ai/api/v1" - api_key = os.environ.get("OPENROUTER_API_KEY") - if api_key: - llm_kwargs["api_key"] = api_key - elif self.provider == "ollama": - llm_kwargs["base_url"] = "http://localhost:11434/v1" - llm_kwargs["api_key"] = "ollama" # Ollama doesn't require auth + # Provider-specific base URL and auth + if self.provider in _PROVIDER_CONFIG: + base_url, api_key_env = _PROVIDER_CONFIG[self.provider] + llm_kwargs["base_url"] = base_url + if api_key_env: + api_key = os.environ.get(api_key_env) + if api_key: + llm_kwargs["api_key"] = api_key + else: + llm_kwargs["api_key"] = "ollama" elif self.base_url: llm_kwargs["base_url"] = self.base_url - for key in ("timeout", "max_retries", "reasoning_effort", "api_key", "callbacks", "http_client", "http_async_client"): + # Forward user-provided kwargs + for key in _PASSTHROUGH_KWARGS: if key in self.kwargs: llm_kwargs[key] = self.kwargs[key] - return UnifiedChatOpenAI(**llm_kwargs) + # Native OpenAI: use Responses API for consistent behavior across + # all model families. Third-party providers use Chat Completions. + if self.provider == "openai": + llm_kwargs["use_responses_api"] = True + + return ChatOpenAI(**llm_kwargs) def validate_model(self) -> bool: """Validate model for the provider.""" From 0b13145dc0e145fad9e09298ae7c66ba729f38b0 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 22 Mar 2026 20:40:18 +0000 Subject: [PATCH 04/30] fix: handle list content when writing report sections Closes #400 --- cli/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/main.py b/cli/main.py index adda48fc..df6dc891 100644 --- a/cli/main.py +++ b/cli/main.py @@ -972,8 +972,9 @@ def run_analysis(): content = obj.report_sections[section_name] if content: file_name = f"{section_name}.md" + text = "\n".join(str(item) for item in content) if isinstance(content, list) else content with open(report_dir / file_name, "w", encoding="utf-8") as f: - f.write(content) + f.write(text) return wrapper message_buffer.add_message = save_message_decorator(message_buffer, "add_message") From 77755f0431b284690ddc835ee66684dab25fbb33 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 22 Mar 2026 21:38:01 +0000 Subject: [PATCH 05/30] chore: consolidate install, fix CLI portability, normalize LLM responses - Point requirements.txt to pyproject.toml as single source of truth - Resolve welcome.txt path relative to module for CLI portability - Include cli/static files in package build - Extract shared normalize_content for OpenAI Responses API and Gemini 3 list-format responses into base_client.py - Update README install and CLI usage instructions --- README.md | 11 ++++++----- cli/main.py | 2 +- pyproject.toml | 3 +++ requirements.txt | 22 +--------------------- tradingagents/llm_clients/base_client.py | 19 +++++++++++++++++++ tradingagents/llm_clients/google_client.py | 17 +++-------------- tradingagents/llm_clients/openai_client.py | 16 ++++++++++++++-- 7 files changed, 47 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 8cf085e8..e31c43ad 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,9 @@ conda create -n tradingagents python=3.13 conda activate tradingagents ``` -Install dependencies: +Install the package and its dependencies: ```bash -pip install -r requirements.txt +pip install . ``` ### Required APIs @@ -139,11 +139,12 @@ cp .env.example .env ### CLI Usage -You can also try out the CLI directly by running: +Launch the interactive CLI: ```bash -python -m cli.main +tradingagents # installed command +python -m cli.main # alternative: run directly from source ``` -You will see a screen where you can select your desired tickers, date, LLMs, research depth, etc. +You will see a screen where you can select your desired tickers, analysis date, LLM provider, research depth, and more.

diff --git a/cli/main.py b/cli/main.py index df6dc891..a706f11d 100644 --- a/cli/main.py +++ b/cli/main.py @@ -462,7 +462,7 @@ def update_display(layout, spinner_text=None, stats_handler=None, start_time=Non def get_user_selections(): """Get all user selections before starting the analysis display.""" # Display ASCII art welcome message - with open("./cli/static/welcome.txt", "r", encoding="utf-8") as f: + with open(Path(__file__).parent / "static" / "welcome.txt", "r", encoding="utf-8") as f: welcome_ascii = f.read() # Create welcome box content diff --git a/pyproject.toml b/pyproject.toml index 4c91a733..256d21d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,3 +37,6 @@ tradingagents = "cli.main:app" [tool.setuptools.packages.find] include = ["tradingagents*", "cli*"] + +[tool.setuptools.package-data] +cli = ["static/*"] diff --git a/requirements.txt b/requirements.txt index 184468b8..9c558e35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1 @@ -typing-extensions -langchain-core -langchain-openai -langchain-experimental -pandas -yfinance -stockstats -langgraph -rank-bm25 -setuptools -backtrader -parsel -requests -tqdm -pytz -redis -rich -typer -questionary -langchain_anthropic -langchain-google-genai +. diff --git a/tradingagents/llm_clients/base_client.py b/tradingagents/llm_clients/base_client.py index 43845575..9c3dd17c 100644 --- a/tradingagents/llm_clients/base_client.py +++ b/tradingagents/llm_clients/base_client.py @@ -2,6 +2,25 @@ from abc import ABC, abstractmethod from typing import Any, Optional +def normalize_content(response): + """Normalize LLM response content to a plain string. + + Multiple providers (OpenAI Responses API, Google Gemini 3) return content + as a list of typed blocks, e.g. [{'type': 'reasoning', ...}, {'type': 'text', 'text': '...'}]. + Downstream agents expect response.content to be a string. This extracts + and joins the text blocks, discarding reasoning/metadata blocks. + """ + content = response.content + if isinstance(content, list): + texts = [ + item.get("text", "") if isinstance(item, dict) and item.get("type") == "text" + else item if isinstance(item, str) else "" + for item in content + ] + response.content = "\n".join(t for t in texts if t) + return response + + class BaseLLMClient(ABC): """Abstract base class for LLM clients.""" diff --git a/tradingagents/llm_clients/google_client.py b/tradingagents/llm_clients/google_client.py index 3dd85e3f..7401df0e 100644 --- a/tradingagents/llm_clients/google_client.py +++ b/tradingagents/llm_clients/google_client.py @@ -2,30 +2,19 @@ from typing import Any, Optional from langchain_google_genai import ChatGoogleGenerativeAI -from .base_client import BaseLLMClient +from .base_client import BaseLLMClient, normalize_content from .validators import validate_model class NormalizedChatGoogleGenerativeAI(ChatGoogleGenerativeAI): """ChatGoogleGenerativeAI with normalized content output. - Gemini 3 models return content as list: [{'type': 'text', 'text': '...'}] + Gemini 3 models return content as list of typed blocks. This normalizes to string for consistent downstream handling. """ - def _normalize_content(self, response): - content = response.content - if isinstance(content, list): - texts = [ - item.get("text", "") if isinstance(item, dict) and item.get("type") == "text" - else item if isinstance(item, str) else "" - for item in content - ] - response.content = "\n".join(t for t in texts if t) - return response - def invoke(self, input, config=None, **kwargs): - return self._normalize_content(super().invoke(input, config, **kwargs)) + return normalize_content(super().invoke(input, config, **kwargs)) class GoogleClient(BaseLLMClient): diff --git a/tradingagents/llm_clients/openai_client.py b/tradingagents/llm_clients/openai_client.py index c314d077..fd9b4e33 100644 --- a/tradingagents/llm_clients/openai_client.py +++ b/tradingagents/llm_clients/openai_client.py @@ -3,9 +3,21 @@ from typing import Any, Optional from langchain_openai import ChatOpenAI -from .base_client import BaseLLMClient +from .base_client import BaseLLMClient, normalize_content from .validators import validate_model + +class NormalizedChatOpenAI(ChatOpenAI): + """ChatOpenAI with normalized content output. + + The Responses API returns content as a list of typed blocks + (reasoning, text, etc.). This normalizes to string for consistent + downstream handling. + """ + + def invoke(self, input, config=None, **kwargs): + return normalize_content(super().invoke(input, config, **kwargs)) + # Kwargs forwarded from user config to ChatOpenAI _PASSTHROUGH_KWARGS = ( "timeout", "max_retries", "reasoning_effort", @@ -66,7 +78,7 @@ class OpenAIClient(BaseLLMClient): if self.provider == "openai": llm_kwargs["use_responses_api"] = True - return ChatOpenAI(**llm_kwargs) + return NormalizedChatOpenAI(**llm_kwargs) def validate_model(self) -> bool: """Validate model for the provider.""" From bd9b1e5efa1fabcfd34d49a07bdc9f0b018f2847 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 22 Mar 2026 21:57:05 +0000 Subject: [PATCH 06/30] feat: add Anthropic effort level support for Claude models Add effort parameter (high/medium/low) for Claude 4.5+ and 4.6 models, consistent with OpenAI reasoning_effort and Google thinking_level. Also add content normalization for Anthropic responses. --- cli/main.py | 11 +++++++++ cli/utils.py | 20 ++++++++++++++++ tradingagents/default_config.py | 1 + tradingagents/graph/trading_graph.py | 5 ++++ tradingagents/llm_clients/anthropic_client.py | 23 ++++++++++++++++--- 5 files changed, 57 insertions(+), 3 deletions(-) diff --git a/cli/main.py b/cli/main.py index a706f11d..f6e2c44a 100644 --- a/cli/main.py +++ b/cli/main.py @@ -556,6 +556,7 @@ def get_user_selections(): # Step 7: Provider-specific thinking configuration thinking_level = None reasoning_effort = None + anthropic_effort = None provider_lower = selected_llm_provider.lower() if provider_lower == "google": @@ -574,6 +575,14 @@ def get_user_selections(): ) ) reasoning_effort = ask_openai_reasoning_effort() + elif provider_lower == "anthropic": + console.print( + create_question_box( + "Step 7: Effort Level", + "Configure Claude effort level" + ) + ) + anthropic_effort = ask_anthropic_effort() return { "ticker": selected_ticker, @@ -586,6 +595,7 @@ def get_user_selections(): "deep_thinker": selected_deep_thinker, "google_thinking_level": thinking_level, "openai_reasoning_effort": reasoning_effort, + "anthropic_effort": anthropic_effort, } @@ -911,6 +921,7 @@ def run_analysis(): # Provider-specific thinking configuration config["google_thinking_level"] = selections.get("google_thinking_level") config["openai_reasoning_effort"] = selections.get("openai_reasoning_effort") + config["anthropic_effort"] = selections.get("anthropic_effort") # Create stats callback handler for tracking LLM/tool calls stats_handler = StatsCallbackHandler() diff --git a/cli/utils.py b/cli/utils.py index 5a8ec16c..18efe1d6 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -311,6 +311,26 @@ def ask_openai_reasoning_effort() -> str: ).ask() +def ask_anthropic_effort() -> str | None: + """Ask for Anthropic effort level. + + Controls token usage and response thoroughness on Claude 4.5+ and 4.6 models. + """ + return questionary.select( + "Select Effort Level:", + choices=[ + questionary.Choice("High (recommended)", "high"), + questionary.Choice("Medium (balanced)", "medium"), + questionary.Choice("Low (faster, cheaper)", "low"), + ], + style=questionary.Style([ + ("selected", "fg:cyan noinherit"), + ("highlighted", "fg:cyan noinherit"), + ("pointer", "fg:cyan noinherit"), + ]), + ).ask() + + def ask_gemini_thinking_config() -> str | None: """Ask for Gemini thinking configuration. diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py index ecf0dc29..898e1e1e 100644 --- a/tradingagents/default_config.py +++ b/tradingagents/default_config.py @@ -15,6 +15,7 @@ DEFAULT_CONFIG = { # Provider-specific thinking configuration "google_thinking_level": None, # "high", "minimal", etc. "openai_reasoning_effort": None, # "medium", "high", "low" + "anthropic_effort": None, # "high", "medium", "low" # Debate and discussion settings "max_debate_rounds": 1, "max_risk_discuss_rounds": 1, diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index c7ef0f98..306f7f38 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -148,6 +148,11 @@ class TradingAgentsGraph: if reasoning_effort: kwargs["reasoning_effort"] = reasoning_effort + elif provider == "anthropic": + effort = self.config.get("anthropic_effort") + if effort: + kwargs["effort"] = effort + return kwargs def _create_tool_nodes(self) -> Dict[str, ToolNode]: diff --git a/tradingagents/llm_clients/anthropic_client.py b/tradingagents/llm_clients/anthropic_client.py index 8539c752..2c1e5a67 100644 --- a/tradingagents/llm_clients/anthropic_client.py +++ b/tradingagents/llm_clients/anthropic_client.py @@ -2,9 +2,26 @@ from typing import Any, Optional from langchain_anthropic import ChatAnthropic -from .base_client import BaseLLMClient +from .base_client import BaseLLMClient, normalize_content from .validators import validate_model +_PASSTHROUGH_KWARGS = ( + "timeout", "max_retries", "api_key", "max_tokens", + "callbacks", "http_client", "http_async_client", "effort", +) + + +class NormalizedChatAnthropic(ChatAnthropic): + """ChatAnthropic with normalized content output. + + Claude models with extended thinking or tool use return content as a + list of typed blocks. This normalizes to string for consistent + downstream handling. + """ + + def invoke(self, input, config=None, **kwargs): + return normalize_content(super().invoke(input, config, **kwargs)) + class AnthropicClient(BaseLLMClient): """Client for Anthropic Claude models.""" @@ -16,11 +33,11 @@ class AnthropicClient(BaseLLMClient): """Return configured ChatAnthropic instance.""" llm_kwargs = {"model": self.model} - for key in ("timeout", "max_retries", "api_key", "max_tokens", "callbacks", "http_client", "http_async_client"): + for key in _PASSTHROUGH_KWARGS: if key in self.kwargs: llm_kwargs[key] = self.kwargs[key] - return ChatAnthropic(**llm_kwargs) + return NormalizedChatAnthropic(**llm_kwargs) def validate_model(self) -> bool: """Validate model for Anthropic.""" From 7cca9c924e786a7f8340179a9710164161988ef8 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 22 Mar 2026 22:11:08 +0000 Subject: [PATCH 07/30] fix: add exponential backoff retry for yfinance rate limits (#426) --- tradingagents/dataflows/stockstats_utils.py | 29 ++++++++++++++++++-- tradingagents/dataflows/y_finance.py | 30 ++++++++++----------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/tradingagents/dataflows/stockstats_utils.py b/tradingagents/dataflows/stockstats_utils.py index 467156a2..47d5460a 100644 --- a/tradingagents/dataflows/stockstats_utils.py +++ b/tradingagents/dataflows/stockstats_utils.py @@ -1,10 +1,35 @@ +import time +import logging + import pandas as pd import yfinance as yf +from yfinance.exceptions import YFRateLimitError from stockstats import wrap from typing import Annotated import os from .config import get_config +logger = logging.getLogger(__name__) + + +def yf_retry(func, max_retries=3, base_delay=2.0): + """Execute a yfinance call with exponential backoff on rate limits. + + yfinance raises YFRateLimitError on HTTP 429 responses but does not + retry them internally. This wrapper adds retry logic specifically + for rate limits. Other exceptions propagate immediately. + """ + for attempt in range(max_retries + 1): + try: + return func() + except YFRateLimitError: + if attempt < max_retries: + delay = base_delay * (2 ** attempt) + logger.warning(f"Yahoo Finance rate limited, retrying in {delay:.0f}s (attempt {attempt + 1}/{max_retries})") + time.sleep(delay) + else: + raise + def _clean_dataframe(data: pd.DataFrame) -> pd.DataFrame: """Normalize a stock DataFrame for stockstats: parse dates, drop invalid rows, fill price gaps.""" @@ -51,14 +76,14 @@ class StockstatsUtils: if os.path.exists(data_file): data = pd.read_csv(data_file, on_bad_lines="skip") else: - data = yf.download( + data = yf_retry(lambda: yf.download( symbol, start=start_date_str, end=end_date_str, multi_level_index=False, progress=False, auto_adjust=True, - ) + )) data = data.reset_index() data.to_csv(data_file, index=False) diff --git a/tradingagents/dataflows/y_finance.py b/tradingagents/dataflows/y_finance.py index b915490d..3682a01d 100644 --- a/tradingagents/dataflows/y_finance.py +++ b/tradingagents/dataflows/y_finance.py @@ -3,7 +3,7 @@ from datetime import datetime from dateutil.relativedelta import relativedelta import yfinance as yf import os -from .stockstats_utils import StockstatsUtils, _clean_dataframe +from .stockstats_utils import StockstatsUtils, _clean_dataframe, yf_retry def get_YFin_data_online( symbol: Annotated[str, "ticker symbol of the company"], @@ -18,7 +18,7 @@ def get_YFin_data_online( ticker = yf.Ticker(symbol.upper()) # Fetch historical data for the specified date range - data = ticker.history(start=start_date, end=end_date) + data = yf_retry(lambda: ticker.history(start=start_date, end=end_date)) # Check if data is empty if data.empty: @@ -234,14 +234,14 @@ def _get_stock_stats_bulk( if os.path.exists(data_file): data = pd.read_csv(data_file, on_bad_lines="skip") else: - data = yf.download( + data = yf_retry(lambda: yf.download( symbol, start=start_date_str, end=end_date_str, multi_level_index=False, progress=False, auto_adjust=True, - ) + )) data = data.reset_index() data.to_csv(data_file, index=False) @@ -300,7 +300,7 @@ def get_fundamentals( """Get company fundamentals overview from yfinance.""" try: ticker_obj = yf.Ticker(ticker.upper()) - info = ticker_obj.info + info = yf_retry(lambda: ticker_obj.info) if not info: return f"No fundamentals data found for symbol '{ticker}'" @@ -358,11 +358,11 @@ def get_balance_sheet( """Get balance sheet data from yfinance.""" try: ticker_obj = yf.Ticker(ticker.upper()) - + if freq.lower() == "quarterly": - data = ticker_obj.quarterly_balance_sheet + data = yf_retry(lambda: ticker_obj.quarterly_balance_sheet) else: - data = ticker_obj.balance_sheet + data = yf_retry(lambda: ticker_obj.balance_sheet) if data.empty: return f"No balance sheet data found for symbol '{ticker}'" @@ -388,11 +388,11 @@ def get_cashflow( """Get cash flow data from yfinance.""" try: ticker_obj = yf.Ticker(ticker.upper()) - + if freq.lower() == "quarterly": - data = ticker_obj.quarterly_cashflow + data = yf_retry(lambda: ticker_obj.quarterly_cashflow) else: - data = ticker_obj.cashflow + data = yf_retry(lambda: ticker_obj.cashflow) if data.empty: return f"No cash flow data found for symbol '{ticker}'" @@ -418,11 +418,11 @@ def get_income_statement( """Get income statement data from yfinance.""" try: ticker_obj = yf.Ticker(ticker.upper()) - + if freq.lower() == "quarterly": - data = ticker_obj.quarterly_income_stmt + data = yf_retry(lambda: ticker_obj.quarterly_income_stmt) else: - data = ticker_obj.income_stmt + data = yf_retry(lambda: ticker_obj.income_stmt) if data.empty: return f"No income statement data found for symbol '{ticker}'" @@ -446,7 +446,7 @@ def get_insider_transactions( """Get insider transactions data from yfinance.""" try: ticker_obj = yf.Ticker(ticker.upper()) - data = ticker_obj.insider_transactions + data = yf_retry(lambda: ticker_obj.insider_transactions) if data is None or data.empty: return f"No insider transactions data found for symbol '{ticker}'" From 318adda0c64ec04985f36a81834b2fe8c75dd717 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 22 Mar 2026 23:07:20 +0000 Subject: [PATCH 08/30] refactor: five-tier rating scale and streamlined agent prompts --- .../agents/analysts/fundamentals_analyst.py | 2 +- .../agents/analysts/market_analyst.py | 2 +- tradingagents/agents/analysts/news_analyst.py | 2 +- .../agents/analysts/social_media_analyst.py | 2 +- tradingagents/agents/managers/risk_manager.py | 35 ++++++++++++------- .../agents/risk_mgmt/aggressive_debator.py | 2 +- .../agents/risk_mgmt/conservative_debator.py | 2 +- .../agents/risk_mgmt/neutral_debator.py | 2 +- tradingagents/agents/trader/trader.py | 2 +- tradingagents/agents/utils/agent_utils.py | 9 +++-- tradingagents/graph/signal_processing.py | 6 ++-- 11 files changed, 38 insertions(+), 28 deletions(-) diff --git a/tradingagents/agents/analysts/fundamentals_analyst.py b/tradingagents/agents/analysts/fundamentals_analyst.py index ddf57abd..990398a6 100644 --- a/tradingagents/agents/analysts/fundamentals_analyst.py +++ b/tradingagents/agents/analysts/fundamentals_analyst.py @@ -25,7 +25,7 @@ def create_fundamentals_analyst(llm): ] system_message = ( - "You are a researcher tasked with analyzing fundamental information over the past week about a company. Please write a comprehensive report of the company's fundamental information such as financial documents, company profile, basic company financials, and company financial history to gain a full view of the company's fundamental information to inform traders. Make sure to include as much detail as possible. Always preserve the exact ticker symbol provided by the user, including any exchange suffix, and never merge fundamentals for similarly named companies from other exchanges. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions." + "You are a researcher tasked with analyzing fundamental information over the past week about a company. Please write a comprehensive report of the company's fundamental information such as financial documents, company profile, basic company financials, and company financial history to gain a full view of the company's fundamental information to inform traders. Make sure to include as much detail as possible. Provide specific, actionable insights with supporting evidence to help traders make informed decisions." + " Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read." + " Use the available tools: `get_fundamentals` for comprehensive company analysis, `get_balance_sheet`, `get_cashflow`, and `get_income_statement` for specific financial statements.", ) diff --git a/tradingagents/agents/analysts/market_analyst.py b/tradingagents/agents/analysts/market_analyst.py index 8c1a9ab7..f5d17acd 100644 --- a/tradingagents/agents/analysts/market_analyst.py +++ b/tradingagents/agents/analysts/market_analyst.py @@ -45,7 +45,7 @@ Volatility Indicators: Volume-Based Indicators: - vwma: VWMA: A moving average weighted by volume. Usage: Confirm trends by integrating price action with volume data. Tips: Watch for skewed results from volume spikes; use in combination with other volume analyses. -- Select indicators that provide diverse and complementary information. Avoid redundancy (e.g., do not select both rsi and stochrsi). Also briefly explain why they are suitable for the given market context. When you tool call, please use the exact name of the indicators provided above as they are defined parameters, otherwise your call will fail. Please make sure to call get_stock_data first to retrieve the CSV that is needed to generate indicators. Then use get_indicators with the specific indicator names. Always preserve the exact ticker symbol provided by the user, including any exchange suffix, and never mix in similarly named companies from other exchanges. Write a very detailed and nuanced report of the trends you observe. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions.""" +- Select indicators that provide diverse and complementary information. Avoid redundancy (e.g., do not select both rsi and stochrsi). Also briefly explain why they are suitable for the given market context. When you tool call, please use the exact name of the indicators provided above as they are defined parameters, otherwise your call will fail. Please make sure to call get_stock_data first to retrieve the CSV that is needed to generate indicators. Then use get_indicators with the specific indicator names. Write a very detailed and nuanced report of the trends you observe. Provide specific, actionable insights with supporting evidence to help traders make informed decisions.""" + """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read.""" ) diff --git a/tradingagents/agents/analysts/news_analyst.py b/tradingagents/agents/analysts/news_analyst.py index 2a3a3433..3697c6f6 100644 --- a/tradingagents/agents/analysts/news_analyst.py +++ b/tradingagents/agents/analysts/news_analyst.py @@ -20,7 +20,7 @@ def create_news_analyst(llm): ] system_message = ( - "You are a news researcher tasked with analyzing recent news and trends over the past week. Please write a comprehensive report of the current state of the world that is relevant for trading and macroeconomics. Use the available tools: get_news(query, start_date, end_date) for company-specific or targeted news searches, and get_global_news(curr_date, look_back_days, limit) for broader macroeconomic news. Always preserve the exact ticker symbol provided by the user, including any exchange suffix, and never merge news for similarly named companies from other exchanges. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions." + "You are a news researcher tasked with analyzing recent news and trends over the past week. Please write a comprehensive report of the current state of the world that is relevant for trading and macroeconomics. Use the available tools: get_news(query, start_date, end_date) for company-specific or targeted news searches, and get_global_news(curr_date, look_back_days, limit) for broader macroeconomic news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions." + """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read.""" ) diff --git a/tradingagents/agents/analysts/social_media_analyst.py b/tradingagents/agents/analysts/social_media_analyst.py index 4a6e0074..43df2258 100644 --- a/tradingagents/agents/analysts/social_media_analyst.py +++ b/tradingagents/agents/analysts/social_media_analyst.py @@ -15,7 +15,7 @@ def create_social_media_analyst(llm): ] system_message = ( - "You are a social media and company specific news researcher/analyst tasked with analyzing social media posts, recent company news, and public sentiment for a specific company over the past week. You will be given a company's name your objective is to write a comprehensive long report detailing your analysis, insights, and implications for traders and investors on this company's current state after looking at social media and what people are saying about that company, analyzing sentiment data of what people feel each day about the company, and looking at recent company news. Use the get_news(query, start_date, end_date) tool to search for company-specific news and social media discussions. Try to look at all sources possible from social media to sentiment to news. Always preserve the exact ticker symbol provided by the user, including any exchange suffix, and never merge commentary for similarly named companies from other exchanges. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions." + "You are a social media and company specific news researcher/analyst tasked with analyzing social media posts, recent company news, and public sentiment for a specific company over the past week. You will be given a company's name your objective is to write a comprehensive long report detailing your analysis, insights, and implications for traders and investors on this company's current state after looking at social media and what people are saying about that company, analyzing sentiment data of what people feel each day about the company, and looking at recent company news. Use the get_news(query, start_date, end_date) tool to search for company-specific news and social media discussions. Try to look at all sources possible from social media to sentiment to news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions." + """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read.""" ) diff --git a/tradingagents/agents/managers/risk_manager.py b/tradingagents/agents/managers/risk_manager.py index 3dab49fe..d827c2e8 100644 --- a/tradingagents/agents/managers/risk_manager.py +++ b/tradingagents/agents/managers/risk_manager.py @@ -24,28 +24,37 @@ def create_risk_manager(llm, memory): for i, rec in enumerate(past_memories, 1): past_memory_str += rec["recommendation"] + "\n\n" - prompt = f"""As the Risk Management Judge and Debate Facilitator, your goal is to evaluate the debate between three risk analysts—Aggressive, Neutral, and Conservative—and determine the best course of action for the trader. Your decision must result in a clear recommendation: Buy, Sell, or Hold. Choose Hold only if strongly justified by specific arguments, not as a fallback when all sides seem valid. Strive for clarity and decisiveness. - -Guidelines for Decision-Making: -1. **Summarize Key Arguments**: Extract the strongest points from each analyst, focusing on relevance to the context. -2. **Provide Rationale**: Support your recommendation with direct quotes and counterarguments from the debate. -3. **Refine the Trader's Plan**: Start with the trader's original plan, **{trader_plan}**, and adjust it based on the analysts' insights. -4. **Learn from Past Mistakes**: Use lessons from **{past_memory_str}** to address prior misjudgments and improve the decision you are making now to make sure you don't make a wrong BUY/SELL/HOLD call that loses money. - -Deliverables: -- A clear and actionable recommendation: Buy, Sell, or Hold. -- Detailed reasoning anchored in the debate and past reflections. + prompt = f"""As the Risk Management Judge, evaluate the debate between the Aggressive, Neutral, and Conservative analysts and deliver a final trading decision. {instrument_context} --- -**Analysts Debate History:** +**Rating Scale** (use exactly one): +- **Buy**: Strong conviction to enter or add to position +- **Overweight**: Favorable outlook, gradually increase exposure +- **Hold**: Maintain current position, no action needed +- **Underweight**: Reduce exposure, take partial profits +- **Sell**: Exit position or avoid entry + +**Guidelines:** +1. Extract the strongest points from each analyst, focusing on relevance to the current context. +2. Start with the trader's original plan: **{trader_plan}**, and refine it based on the analysts' insights. +3. Apply lessons from past decisions to strengthen this analysis: **{past_memory_str}** + +**Required Output Structure:** +1. **Rating**: State one of Buy / Overweight / Hold / Underweight / Sell. +2. **Executive Summary**: A concise action plan covering entry strategy, position sizing, key risk levels, and time horizon. Keep this brief and actionable. +3. **Investment Thesis**: Detailed reasoning anchored in the debate and past reflections. + +--- + +**Analysts Debate History:** {history} --- -Focus on actionable insights and continuous improvement. Build on past lessons, critically evaluate all perspectives, and ensure each decision advances better outcomes.""" +Be decisive and ground every conclusion in specific evidence from the analysts.""" response = llm.invoke(prompt) diff --git a/tradingagents/agents/risk_mgmt/aggressive_debator.py b/tradingagents/agents/risk_mgmt/aggressive_debator.py index 3905d3d1..651114a7 100644 --- a/tradingagents/agents/risk_mgmt/aggressive_debator.py +++ b/tradingagents/agents/risk_mgmt/aggressive_debator.py @@ -28,7 +28,7 @@ Market Research Report: {market_research_report} Social Media Sentiment Report: {sentiment_report} Latest World Affairs Report: {news_report} Company Fundamentals Report: {fundamentals_report} -Here is the current conversation history: {history} Here are the last arguments from the conservative analyst: {current_conservative_response} Here are the last arguments from the neutral analyst: {current_neutral_response}. If there are no responses from the other viewpoints, do not hallucinate and just present your point. +Here is the current conversation history: {history} Here are the last arguments from the conservative analyst: {current_conservative_response} Here are the last arguments from the neutral analyst: {current_neutral_response}. If there are no responses from the other viewpoints yet, present your own argument based on the available data. Engage actively by addressing any specific concerns raised, refuting the weaknesses in their logic, and asserting the benefits of risk-taking to outpace market norms. Maintain a focus on debating and persuading, not just presenting data. Challenge each counterpoint to underscore why a high-risk approach is optimal. Output conversationally as if you are speaking without any special formatting.""" diff --git a/tradingagents/agents/risk_mgmt/conservative_debator.py b/tradingagents/agents/risk_mgmt/conservative_debator.py index 6b106b1b..7c8c0fd1 100644 --- a/tradingagents/agents/risk_mgmt/conservative_debator.py +++ b/tradingagents/agents/risk_mgmt/conservative_debator.py @@ -29,7 +29,7 @@ Market Research Report: {market_research_report} Social Media Sentiment Report: {sentiment_report} Latest World Affairs Report: {news_report} Company Fundamentals Report: {fundamentals_report} -Here is the current conversation history: {history} Here is the last response from the aggressive analyst: {current_aggressive_response} Here is the last response from the neutral analyst: {current_neutral_response}. If there are no responses from the other viewpoints, do not hallucinate and just present your point. +Here is the current conversation history: {history} Here is the last response from the aggressive analyst: {current_aggressive_response} Here is the last response from the neutral analyst: {current_neutral_response}. If there are no responses from the other viewpoints yet, present your own argument based on the available data. Engage by questioning their optimism and emphasizing the potential downsides they may have overlooked. Address each of their counterpoints to showcase why a conservative stance is ultimately the safest path for the firm's assets. Focus on debating and critiquing their arguments to demonstrate the strength of a low-risk strategy over their approaches. Output conversationally as if you are speaking without any special formatting.""" diff --git a/tradingagents/agents/risk_mgmt/neutral_debator.py b/tradingagents/agents/risk_mgmt/neutral_debator.py index f6aa888d..9ed490da 100644 --- a/tradingagents/agents/risk_mgmt/neutral_debator.py +++ b/tradingagents/agents/risk_mgmt/neutral_debator.py @@ -28,7 +28,7 @@ Market Research Report: {market_research_report} Social Media Sentiment Report: {sentiment_report} Latest World Affairs Report: {news_report} Company Fundamentals Report: {fundamentals_report} -Here is the current conversation history: {history} Here is the last response from the aggressive analyst: {current_aggressive_response} Here is the last response from the conservative analyst: {current_conservative_response}. If there are no responses from the other viewpoints, do not hallucinate and just present your point. +Here is the current conversation history: {history} Here is the last response from the aggressive analyst: {current_aggressive_response} Here is the last response from the conservative analyst: {current_conservative_response}. If there are no responses from the other viewpoints yet, present your own argument based on the available data. Engage actively by analyzing both sides critically, addressing weaknesses in the aggressive and conservative arguments to advocate for a more balanced approach. Challenge each of their points to illustrate why a moderate risk strategy might offer the best of both worlds, providing growth potential while safeguarding against extreme volatility. Focus on debating rather than simply presenting data, aiming to show that a balanced view can lead to the most reliable outcomes. Output conversationally as if you are speaking without any special formatting.""" diff --git a/tradingagents/agents/trader/trader.py b/tradingagents/agents/trader/trader.py index a40eb22a..6298f239 100644 --- a/tradingagents/agents/trader/trader.py +++ b/tradingagents/agents/trader/trader.py @@ -33,7 +33,7 @@ def create_trader(llm, memory): messages = [ { "role": "system", - "content": f"""You are a trading agent analyzing market data to make investment decisions. Based on your analysis, provide a specific recommendation to buy, sell, or hold. End with a firm decision and always conclude your response with 'FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL**' to confirm your recommendation. Do not forget to utilize lessons from past decisions to learn from your mistakes. Here is some reflections from similar situatiosn you traded in and the lessons learned: {past_memory_str}""", + "content": f"""You are a trading agent analyzing market data to make investment decisions. Based on your analysis, provide a specific recommendation to buy, sell, or hold. End with a firm decision and always conclude your response with 'FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL**' to confirm your recommendation. Apply lessons from past decisions to strengthen your analysis. Here are reflections from similar situations you traded in and the lessons learned: {past_memory_str}""", }, context, ] diff --git a/tradingagents/agents/utils/agent_utils.py b/tradingagents/agents/utils/agent_utils.py index 073b209f..e4abc4cd 100644 --- a/tradingagents/agents/utils/agent_utils.py +++ b/tradingagents/agents/utils/agent_utils.py @@ -21,12 +21,11 @@ from tradingagents.agents.utils.news_data_tools import ( def build_instrument_context(ticker: str) -> str: - """Describe the exact instrument so agents avoid cross-exchange symbol mixups.""" + """Describe the exact instrument so agents preserve exchange-qualified tickers.""" return ( - f"The exact listed instrument to analyze is `{ticker}`. " - "Use this exact ticker in every tool call, report, and recommendation. " - "If it includes an exchange suffix such as `.TO`, `.L`, `.HK`, or `.T`, preserve that suffix and do not mix in companies from other exchanges that share the same root symbol. " - "If it does not include a suffix, do not invent one." + f"The instrument to analyze is `{ticker}`. " + "Use this exact ticker in every tool call, report, and recommendation, " + "preserving any exchange suffix (e.g. `.TO`, `.L`, `.HK`, `.T`)." ) def create_msg_delete(): diff --git a/tradingagents/graph/signal_processing.py b/tradingagents/graph/signal_processing.py index 903e8529..f96c1efa 100644 --- a/tradingagents/graph/signal_processing.py +++ b/tradingagents/graph/signal_processing.py @@ -18,12 +18,14 @@ class SignalProcessor: full_signal: Complete trading signal text Returns: - Extracted decision (BUY, SELL, or HOLD) + Extracted rating (BUY, OVERWEIGHT, HOLD, UNDERWEIGHT, or SELL) """ messages = [ ( "system", - "You are an efficient assistant designed to analyze paragraphs or financial reports provided by a group of analysts. Your task is to extract the investment decision: SELL, BUY, or HOLD. Provide only the extracted decision (SELL, BUY, or HOLD) as your output, without adding any additional text or information.", + "You are an efficient assistant that extracts the trading decision from analyst reports. " + "Extract the rating as exactly one of: BUY, OVERWEIGHT, HOLD, UNDERWEIGHT, SELL. " + "Output only the single rating word, nothing else.", ), ("human", full_signal), ] From b8b2825783f490faeebfef5ddaf0178f70b2b0ba Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 22 Mar 2026 23:30:29 +0000 Subject: [PATCH 09/30] refactor: standardize portfolio manager, five-tier rating scale, fix analyst status tracking --- cli/main.py | 13 +++++++--- tradingagents/agents/__init__.py | 4 ++-- .../{risk_manager.py => portfolio_manager.py} | 24 ++++++++----------- tradingagents/graph/conditional_logic.py | 2 +- tradingagents/graph/reflection.py | 8 +++---- tradingagents/graph/setup.py | 18 +++++++------- tradingagents/graph/trading_graph.py | 8 +++---- 7 files changed, 40 insertions(+), 37 deletions(-) rename tradingagents/agents/managers/{risk_manager.py => portfolio_manager.py} (74%) diff --git a/cli/main.py b/cli/main.py index f26ae4c5..f975bac1 100644 --- a/cli/main.py +++ b/cli/main.py @@ -800,9 +800,11 @@ ANALYST_REPORT_MAP = { def update_analyst_statuses(message_buffer, chunk): - """Update all analyst statuses based on current report state. + """Update analyst statuses based on accumulated report state. Logic: + - Store new report content from the current chunk if present + - Check accumulated report_sections (not just current chunk) for status - Analysts with reports = completed - First analyst without report = in_progress - Remaining analysts without reports = pending @@ -817,11 +819,16 @@ def update_analyst_statuses(message_buffer, chunk): agent_name = ANALYST_AGENT_NAMES[analyst_key] report_key = ANALYST_REPORT_MAP[analyst_key] - has_report = bool(chunk.get(report_key)) + + # Capture new report content from current chunk + if chunk.get(report_key): + message_buffer.update_report_section(report_key, chunk[report_key]) + + # Determine status from accumulated sections, not just current chunk + has_report = bool(message_buffer.report_sections.get(report_key)) if has_report: message_buffer.update_agent_status(agent_name, "completed") - message_buffer.update_report_section(report_key, chunk[report_key]) elif not found_active: message_buffer.update_agent_status(agent_name, "in_progress") found_active = True diff --git a/tradingagents/agents/__init__.py b/tradingagents/agents/__init__.py index 8a169f22..1f03642c 100644 --- a/tradingagents/agents/__init__.py +++ b/tradingagents/agents/__init__.py @@ -15,7 +15,7 @@ from .risk_mgmt.conservative_debator import create_conservative_debator from .risk_mgmt.neutral_debator import create_neutral_debator from .managers.research_manager import create_research_manager -from .managers.risk_manager import create_risk_manager +from .managers.portfolio_manager import create_portfolio_manager from .trader.trader import create_trader @@ -33,7 +33,7 @@ __all__ = [ "create_neutral_debator", "create_news_analyst", "create_aggressive_debator", - "create_risk_manager", + "create_portfolio_manager", "create_conservative_debator", "create_social_media_analyst", "create_trader", diff --git a/tradingagents/agents/managers/risk_manager.py b/tradingagents/agents/managers/portfolio_manager.py similarity index 74% rename from tradingagents/agents/managers/risk_manager.py rename to tradingagents/agents/managers/portfolio_manager.py index d827c2e8..acdf940b 100644 --- a/tradingagents/agents/managers/risk_manager.py +++ b/tradingagents/agents/managers/portfolio_manager.py @@ -1,11 +1,8 @@ -import time -import json - from tradingagents.agents.utils.agent_utils import build_instrument_context -def create_risk_manager(llm, memory): - def risk_manager_node(state) -> dict: +def create_portfolio_manager(llm, memory): + def portfolio_manager_node(state) -> dict: instrument_context = build_instrument_context(state["company_of_interest"]) @@ -24,7 +21,7 @@ def create_risk_manager(llm, memory): for i, rec in enumerate(past_memories, 1): past_memory_str += rec["recommendation"] + "\n\n" - prompt = f"""As the Risk Management Judge, evaluate the debate between the Aggressive, Neutral, and Conservative analysts and deliver a final trading decision. + prompt = f"""As the Portfolio Manager, synthesize the risk analysts' debate and deliver the final trading decision. {instrument_context} @@ -37,19 +34,18 @@ def create_risk_manager(llm, memory): - **Underweight**: Reduce exposure, take partial profits - **Sell**: Exit position or avoid entry -**Guidelines:** -1. Extract the strongest points from each analyst, focusing on relevance to the current context. -2. Start with the trader's original plan: **{trader_plan}**, and refine it based on the analysts' insights. -3. Apply lessons from past decisions to strengthen this analysis: **{past_memory_str}** +**Context:** +- Trader's proposed plan: **{trader_plan}** +- Lessons from past decisions: **{past_memory_str}** **Required Output Structure:** 1. **Rating**: State one of Buy / Overweight / Hold / Underweight / Sell. -2. **Executive Summary**: A concise action plan covering entry strategy, position sizing, key risk levels, and time horizon. Keep this brief and actionable. -3. **Investment Thesis**: Detailed reasoning anchored in the debate and past reflections. +2. **Executive Summary**: A concise action plan covering entry strategy, position sizing, key risk levels, and time horizon. +3. **Investment Thesis**: Detailed reasoning anchored in the analysts' debate and past reflections. --- -**Analysts Debate History:** +**Risk Analysts Debate History:** {history} --- @@ -76,4 +72,4 @@ Be decisive and ground every conclusion in specific evidence from the analysts." "final_trade_decision": response.content, } - return risk_manager_node + return portfolio_manager_node diff --git a/tradingagents/graph/conditional_logic.py b/tradingagents/graph/conditional_logic.py index 7b1b1f90..48371793 100644 --- a/tradingagents/graph/conditional_logic.py +++ b/tradingagents/graph/conditional_logic.py @@ -59,7 +59,7 @@ class ConditionalLogic: if ( state["risk_debate_state"]["count"] >= 3 * self.max_risk_discuss_rounds ): # 3 rounds of back-and-forth between 3 agents - return "Risk Judge" + return "Portfolio Manager" if state["risk_debate_state"]["latest_speaker"].startswith("Aggressive"): return "Conservative Analyst" if state["risk_debate_state"]["latest_speaker"].startswith("Conservative"): diff --git a/tradingagents/graph/reflection.py b/tradingagents/graph/reflection.py index 33303231..85438595 100644 --- a/tradingagents/graph/reflection.py +++ b/tradingagents/graph/reflection.py @@ -110,12 +110,12 @@ Adhere strictly to these instructions, and ensure your output is detailed, accur ) invest_judge_memory.add_situations([(situation, result)]) - def reflect_risk_manager(self, current_state, returns_losses, risk_manager_memory): - """Reflect on risk manager's decision and update memory.""" + def reflect_portfolio_manager(self, current_state, returns_losses, portfolio_manager_memory): + """Reflect on portfolio manager's decision and update memory.""" situation = self._extract_current_situation(current_state) judge_decision = current_state["risk_debate_state"]["judge_decision"] result = self._reflect_on_component( - "RISK JUDGE", judge_decision, situation, returns_losses + "PORTFOLIO MANAGER", judge_decision, situation, returns_losses ) - risk_manager_memory.add_situations([(situation, result)]) + portfolio_manager_memory.add_situations([(situation, result)]) diff --git a/tradingagents/graph/setup.py b/tradingagents/graph/setup.py index 772efe7f..e0771c65 100644 --- a/tradingagents/graph/setup.py +++ b/tradingagents/graph/setup.py @@ -23,7 +23,7 @@ class GraphSetup: bear_memory, trader_memory, invest_judge_memory, - risk_manager_memory, + portfolio_manager_memory, conditional_logic: ConditionalLogic, ): """Initialize with required components.""" @@ -34,7 +34,7 @@ class GraphSetup: self.bear_memory = bear_memory self.trader_memory = trader_memory self.invest_judge_memory = invest_judge_memory - self.risk_manager_memory = risk_manager_memory + self.portfolio_manager_memory = portfolio_manager_memory self.conditional_logic = conditional_logic def setup_graph( @@ -101,8 +101,8 @@ class GraphSetup: aggressive_analyst = create_aggressive_debator(self.quick_thinking_llm) neutral_analyst = create_neutral_debator(self.quick_thinking_llm) conservative_analyst = create_conservative_debator(self.quick_thinking_llm) - risk_manager_node = create_risk_manager( - self.deep_thinking_llm, self.risk_manager_memory + portfolio_manager_node = create_portfolio_manager( + self.deep_thinking_llm, self.portfolio_manager_memory ) # Create workflow @@ -124,7 +124,7 @@ class GraphSetup: workflow.add_node("Aggressive Analyst", aggressive_analyst) workflow.add_node("Neutral Analyst", neutral_analyst) workflow.add_node("Conservative Analyst", conservative_analyst) - workflow.add_node("Risk Judge", risk_manager_node) + workflow.add_node("Portfolio Manager", portfolio_manager_node) # Define edges # Start with the first analyst @@ -176,7 +176,7 @@ class GraphSetup: self.conditional_logic.should_continue_risk_analysis, { "Conservative Analyst": "Conservative Analyst", - "Risk Judge": "Risk Judge", + "Portfolio Manager": "Portfolio Manager", }, ) workflow.add_conditional_edges( @@ -184,7 +184,7 @@ class GraphSetup: self.conditional_logic.should_continue_risk_analysis, { "Neutral Analyst": "Neutral Analyst", - "Risk Judge": "Risk Judge", + "Portfolio Manager": "Portfolio Manager", }, ) workflow.add_conditional_edges( @@ -192,11 +192,11 @@ class GraphSetup: self.conditional_logic.should_continue_risk_analysis, { "Aggressive Analyst": "Aggressive Analyst", - "Risk Judge": "Risk Judge", + "Portfolio Manager": "Portfolio Manager", }, ) - workflow.add_edge("Risk Judge", END) + workflow.add_edge("Portfolio Manager", END) # Compile and return return workflow.compile() diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index 306f7f38..c8cd7492 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -99,7 +99,7 @@ class TradingAgentsGraph: self.bear_memory = FinancialSituationMemory("bear_memory", self.config) self.trader_memory = FinancialSituationMemory("trader_memory", self.config) self.invest_judge_memory = FinancialSituationMemory("invest_judge_memory", self.config) - self.risk_manager_memory = FinancialSituationMemory("risk_manager_memory", self.config) + self.portfolio_manager_memory = FinancialSituationMemory("portfolio_manager_memory", self.config) # Create tool nodes self.tool_nodes = self._create_tool_nodes() @@ -117,7 +117,7 @@ class TradingAgentsGraph: self.bear_memory, self.trader_memory, self.invest_judge_memory, - self.risk_manager_memory, + self.portfolio_manager_memory, self.conditional_logic, ) @@ -283,8 +283,8 @@ class TradingAgentsGraph: self.reflector.reflect_invest_judge( self.curr_state, returns_losses, self.invest_judge_memory ) - self.reflector.reflect_risk_manager( - self.curr_state, returns_losses, self.risk_manager_memory + self.reflector.reflect_portfolio_manager( + self.curr_state, returns_losses, self.portfolio_manager_memory ) def process_signal(self, full_signal): From 6c9c9ce1fdc3053381bf2b7f3bca41f62d12c098 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 22 Mar 2026 23:42:37 +0000 Subject: [PATCH 10/30] fix: set process-level UTF-8 default for cross-platform consistency --- cli/main.py | 8 ++++---- tradingagents/__init__.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cli/main.py b/cli/main.py index f975bac1..53837db2 100644 --- a/cli/main.py +++ b/cli/main.py @@ -462,7 +462,7 @@ def update_display(layout, spinner_text=None, stats_handler=None, start_time=Non def get_user_selections(): """Get all user selections before starting the analysis display.""" # Display ASCII art welcome message - with open(Path(__file__).parent / "static" / "welcome.txt", "r", encoding="utf-8") as f: + with open(Path(__file__).parent / "static" / "welcome.txt", "r") as f: welcome_ascii = f.read() # Create welcome box content @@ -968,7 +968,7 @@ def run_analysis(): func(*args, **kwargs) timestamp, message_type, content = obj.messages[-1] content = content.replace("\n", " ") # Replace newlines with spaces - with open(log_file, "a", encoding="utf-8") as f: + with open(log_file, "a") as f: f.write(f"{timestamp} [{message_type}] {content}\n") return wrapper @@ -979,7 +979,7 @@ def run_analysis(): func(*args, **kwargs) timestamp, tool_name, args = obj.tool_calls[-1] args_str = ", ".join(f"{k}={v}" for k, v in args.items()) - with open(log_file, "a", encoding="utf-8") as f: + with open(log_file, "a") as f: f.write(f"{timestamp} [Tool Call] {tool_name}({args_str})\n") return wrapper @@ -993,7 +993,7 @@ def run_analysis(): if content: file_name = f"{section_name}.md" text = "\n".join(str(item) for item in content) if isinstance(content, list) else content - with open(report_dir / file_name, "w", encoding="utf-8") as f: + with open(report_dir / file_name, "w") as f: f.write(text) return wrapper diff --git a/tradingagents/__init__.py b/tradingagents/__init__.py index e69de29b..43a2b439 100644 --- a/tradingagents/__init__.py +++ b/tradingagents/__init__.py @@ -0,0 +1,2 @@ +import os +os.environ.setdefault("PYTHONUTF8", "1") From 589b351f2ab55a8a37d846848479cebc810a5a36 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 22 Mar 2026 23:47:56 +0000 Subject: [PATCH 11/30] TradingAgents v0.2.2 --- README.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e31c43ad..4c4856d1 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ # TradingAgents: Multi-Agents LLM Financial Trading Framework ## News -- [2026-03] **TradingAgents v0.2.1** released with GPT-5.4, Gemini 3.1, Claude 4.6 model coverage and improved system stability. +- [2026-03] **TradingAgents v0.2.2** released with GPT-5.4/Gemini 3.1/Claude 4.6 model coverage, five-tier rating scale, OpenAI Responses API, Anthropic effort control, and cross-platform stability. - [2026-02] **TradingAgents v0.2.0** released with multi-provider LLM support (GPT-5.x, Gemini 3.x, Claude 4.x, Grok 4.x) and improved system architecture. - [2026-01] **Trading-R1** [Technical Report](https://arxiv.org/abs/2509.11420) released, with [Terminal](https://github.com/TauricResearch/Trading-R1) expected to land soon. diff --git a/pyproject.toml b/pyproject.toml index 256d21d9..de27a2b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tradingagents" -version = "0.2.1" +version = "0.2.2" description = "TradingAgents: Multi-Agents LLM Financial Trading Framework" readme = "README.md" requires-python = ">=3.10" From f5026009f97b797541899bbd4e55e1f520e71df7 Mon Sep 17 00:00:00 2001 From: javierdejesusda Date: Tue, 24 Mar 2026 14:35:02 +0100 Subject: [PATCH 12/30] fix(llm_clients): standardize Google API key to unified api_key param GoogleClient now accepts the unified `api_key` parameter used by OpenAI and Anthropic clients, mapping it to the provider-specific `google_api_key` that ChatGoogleGenerativeAI expects. Legacy `google_api_key` still works for backward compatibility. Resolves TODO.md item #2 (inconsistent parameter handling). --- tests/test_google_api_key.py | 39 ++++++++++++++++++++++ tradingagents/llm_clients/TODO.md | 11 ++---- tradingagents/llm_clients/google_client.py | 8 ++++- 3 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 tests/test_google_api_key.py diff --git a/tests/test_google_api_key.py b/tests/test_google_api_key.py new file mode 100644 index 00000000..1ec2301f --- /dev/null +++ b/tests/test_google_api_key.py @@ -0,0 +1,39 @@ +import unittest +from unittest.mock import patch + + +class TestGoogleApiKeyStandardization(unittest.TestCase): + """Verify GoogleClient accepts unified api_key parameter.""" + + @patch("tradingagents.llm_clients.google_client.NormalizedChatGoogleGenerativeAI") + def test_api_key_mapped_to_google_api_key(self, mock_chat): + from tradingagents.llm_clients.google_client import GoogleClient + + client = GoogleClient("gemini-2.5-flash", api_key="test-key-123") + client.get_llm() + call_kwargs = mock_chat.call_args[1] + self.assertEqual(call_kwargs["google_api_key"], "test-key-123") + + @patch("tradingagents.llm_clients.google_client.NormalizedChatGoogleGenerativeAI") + def test_legacy_google_api_key_still_works(self, mock_chat): + from tradingagents.llm_clients.google_client import GoogleClient + + client = GoogleClient("gemini-2.5-flash", google_api_key="legacy-key-456") + client.get_llm() + call_kwargs = mock_chat.call_args[1] + self.assertEqual(call_kwargs["google_api_key"], "legacy-key-456") + + @patch("tradingagents.llm_clients.google_client.NormalizedChatGoogleGenerativeAI") + def test_api_key_takes_precedence_over_google_api_key(self, mock_chat): + from tradingagents.llm_clients.google_client import GoogleClient + + client = GoogleClient( + "gemini-2.5-flash", api_key="unified", google_api_key="legacy" + ) + client.get_llm() + call_kwargs = mock_chat.call_args[1] + self.assertEqual(call_kwargs["google_api_key"], "unified") + + +if __name__ == "__main__": + unittest.main() diff --git a/tradingagents/llm_clients/TODO.md b/tradingagents/llm_clients/TODO.md index d5b5ac9c..f666665d 100644 --- a/tradingagents/llm_clients/TODO.md +++ b/tradingagents/llm_clients/TODO.md @@ -5,14 +5,9 @@ ### 1. `validate_model()` is never called - Add validation call in `get_llm()` with warning (not error) for unknown models -### 2. Inconsistent parameter handling -| Client | API Key Param | Special Params | -|--------|---------------|----------------| -| OpenAI | `api_key` | `reasoning_effort` | -| Anthropic | `api_key` | `thinking_config` → `thinking` | -| Google | `google_api_key` | `thinking_budget` | - -**Fix:** Standardize with unified `api_key` that maps to provider-specific keys +### 2. ~~Inconsistent parameter handling~~ (Fixed) +- GoogleClient now accepts unified `api_key` and maps it to `google_api_key` +- Legacy `google_api_key` still works for backward compatibility ### 3. `base_url` accepted but ignored - `AnthropicClient`: accepts `base_url` but never uses it diff --git a/tradingagents/llm_clients/google_client.py b/tradingagents/llm_clients/google_client.py index 7401df0e..af8c6e48 100644 --- a/tradingagents/llm_clients/google_client.py +++ b/tradingagents/llm_clients/google_client.py @@ -27,10 +27,16 @@ class GoogleClient(BaseLLMClient): """Return configured ChatGoogleGenerativeAI instance.""" llm_kwargs = {"model": self.model} - for key in ("timeout", "max_retries", "google_api_key", "callbacks", "http_client", "http_async_client"): + for key in ("timeout", "max_retries", "callbacks", "http_client", "http_async_client"): if key in self.kwargs: llm_kwargs[key] = self.kwargs[key] + # Unified api_key maps to provider-specific google_api_key + if "api_key" in self.kwargs: + llm_kwargs["google_api_key"] = self.kwargs["api_key"] + elif "google_api_key" in self.kwargs: + llm_kwargs["google_api_key"] = self.kwargs["google_api_key"] + # Map thinking_level to appropriate API param based on model # Gemini 3 Pro: low, high # Gemini 3 Flash: minimal, low, medium, high From 047b38971cba6b390d04bb73e7191f2c05ee135e Mon Sep 17 00:00:00 2001 From: javierdejesusda Date: Tue, 24 Mar 2026 14:52:51 +0100 Subject: [PATCH 13/30] refactor: simplify api_key mapping and consolidate tests Apply review suggestions: use concise `or` pattern for API key resolution, consolidate tests into parameterized subTest, move import to module level per PEP 8. --- tests/test_google_api_key.py | 41 ++++++++-------------- tradingagents/llm_clients/google_client.py | 7 ++-- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/tests/test_google_api_key.py b/tests/test_google_api_key.py index 1ec2301f..e1607c49 100644 --- a/tests/test_google_api_key.py +++ b/tests/test_google_api_key.py @@ -1,38 +1,27 @@ import unittest from unittest.mock import patch +from tradingagents.llm_clients.google_client import GoogleClient + class TestGoogleApiKeyStandardization(unittest.TestCase): """Verify GoogleClient accepts unified api_key parameter.""" @patch("tradingagents.llm_clients.google_client.NormalizedChatGoogleGenerativeAI") - def test_api_key_mapped_to_google_api_key(self, mock_chat): - from tradingagents.llm_clients.google_client import GoogleClient + def test_api_key_handling(self, mock_chat): + test_cases = [ + ("unified api_key is mapped", {"api_key": "test-key-123"}, "test-key-123"), + ("legacy google_api_key still works", {"google_api_key": "legacy-key-456"}, "legacy-key-456"), + ("unified api_key takes precedence", {"api_key": "unified", "google_api_key": "legacy"}, "unified"), + ] - client = GoogleClient("gemini-2.5-flash", api_key="test-key-123") - client.get_llm() - call_kwargs = mock_chat.call_args[1] - self.assertEqual(call_kwargs["google_api_key"], "test-key-123") - - @patch("tradingagents.llm_clients.google_client.NormalizedChatGoogleGenerativeAI") - def test_legacy_google_api_key_still_works(self, mock_chat): - from tradingagents.llm_clients.google_client import GoogleClient - - client = GoogleClient("gemini-2.5-flash", google_api_key="legacy-key-456") - client.get_llm() - call_kwargs = mock_chat.call_args[1] - self.assertEqual(call_kwargs["google_api_key"], "legacy-key-456") - - @patch("tradingagents.llm_clients.google_client.NormalizedChatGoogleGenerativeAI") - def test_api_key_takes_precedence_over_google_api_key(self, mock_chat): - from tradingagents.llm_clients.google_client import GoogleClient - - client = GoogleClient( - "gemini-2.5-flash", api_key="unified", google_api_key="legacy" - ) - client.get_llm() - call_kwargs = mock_chat.call_args[1] - self.assertEqual(call_kwargs["google_api_key"], "unified") + for msg, kwargs, expected_key in test_cases: + with self.subTest(msg=msg): + mock_chat.reset_mock() + client = GoogleClient("gemini-2.5-flash", **kwargs) + client.get_llm() + call_kwargs = mock_chat.call_args[1] + self.assertEqual(call_kwargs.get("google_api_key"), expected_key) if __name__ == "__main__": diff --git a/tradingagents/llm_clients/google_client.py b/tradingagents/llm_clients/google_client.py index af8c6e48..f9971aa6 100644 --- a/tradingagents/llm_clients/google_client.py +++ b/tradingagents/llm_clients/google_client.py @@ -32,10 +32,9 @@ class GoogleClient(BaseLLMClient): llm_kwargs[key] = self.kwargs[key] # Unified api_key maps to provider-specific google_api_key - if "api_key" in self.kwargs: - llm_kwargs["google_api_key"] = self.kwargs["api_key"] - elif "google_api_key" in self.kwargs: - llm_kwargs["google_api_key"] = self.kwargs["google_api_key"] + google_api_key = self.kwargs.get("api_key") or self.kwargs.get("google_api_key") + if google_api_key: + llm_kwargs["google_api_key"] = google_api_key # Map thinking_level to appropriate API param based on model # Gemini 3 Pro: low, high From 8793336dade0709b95233969147feafc00dc9ff4 Mon Sep 17 00:00:00 2001 From: CadeYu Date: Wed, 25 Mar 2026 21:23:02 +0800 Subject: [PATCH 14/30] sync model validation with cli catalog --- cli/utils.py | 81 +------------ tests/test_model_validation.py | 52 +++++++++ tradingagents/llm_clients/anthropic_client.py | 1 + tradingagents/llm_clients/base_client.py | 22 ++++ tradingagents/llm_clients/google_client.py | 1 + tradingagents/llm_clients/model_catalog.py | 106 ++++++++++++++++++ tradingagents/llm_clients/openai_client.py | 1 + tradingagents/llm_clients/validators.py | 53 +-------- 8 files changed, 192 insertions(+), 125 deletions(-) create mode 100644 tests/test_model_validation.py create mode 100644 tradingagents/llm_clients/model_catalog.py diff --git a/cli/utils.py b/cli/utils.py index 5a8ec16c..9869fb4d 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -4,6 +4,7 @@ from typing import List, Optional, Tuple, Dict from rich.console import Console from cli.models import AnalystType +from tradingagents.llm_clients.model_catalog import get_model_options console = Console() @@ -129,48 +130,11 @@ def select_research_depth() -> int: def select_shallow_thinking_agent(provider) -> str: """Select shallow thinking llm engine using an interactive selection.""" - # Define shallow thinking llm engine options with their corresponding model names - # Ordering: medium → light → heavy (balanced first for quick tasks) - # Within same tier, newer models first - SHALLOW_AGENT_OPTIONS = { - "openai": [ - ("GPT-5 Mini - Balanced speed, cost, and capability", "gpt-5-mini"), - ("GPT-5 Nano - High-throughput, simple tasks", "gpt-5-nano"), - ("GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"), - ("GPT-4.1 - Smartest non-reasoning model", "gpt-4.1"), - ], - "anthropic": [ - ("Claude Sonnet 4.6 - Best speed and intelligence balance", "claude-sonnet-4-6"), - ("Claude Haiku 4.5 - Fast, near-instant responses", "claude-haiku-4-5"), - ("Claude Sonnet 4.5 - Agents and coding", "claude-sonnet-4-5"), - ], - "google": [ - ("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"), - ("Gemini 2.5 Flash - Balanced, stable", "gemini-2.5-flash"), - ("Gemini 3.1 Flash Lite - Most cost-efficient", "gemini-3.1-flash-lite-preview"), - ("Gemini 2.5 Flash Lite - Fast, low-cost", "gemini-2.5-flash-lite"), - ], - "xai": [ - ("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"), - ("Grok 4 Fast (Non-Reasoning) - Speed optimized", "grok-4-fast-non-reasoning"), - ("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"), - ], - "openrouter": [ - ("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"), - ("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"), - ], - "ollama": [ - ("Qwen3:latest (8B, local)", "qwen3:latest"), - ("GPT-OSS:latest (20B, local)", "gpt-oss:latest"), - ("GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"), - ], - } - choice = questionary.select( "Select Your [Quick-Thinking LLM Engine]:", choices=[ questionary.Choice(display, value=value) - for display, value in SHALLOW_AGENT_OPTIONS[provider.lower()] + for display, value in get_model_options(provider, "quick") ], instruction="\n- Use arrow keys to navigate\n- Press Enter to select", style=questionary.Style( @@ -194,50 +158,11 @@ def select_shallow_thinking_agent(provider) -> str: def select_deep_thinking_agent(provider) -> str: """Select deep thinking llm engine using an interactive selection.""" - # Define deep thinking llm engine options with their corresponding model names - # Ordering: heavy → medium → light (most capable first for deep tasks) - # Within same tier, newer models first - DEEP_AGENT_OPTIONS = { - "openai": [ - ("GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"), - ("GPT-5.2 - Strong reasoning, cost-effective", "gpt-5.2"), - ("GPT-5 Mini - Balanced speed, cost, and capability", "gpt-5-mini"), - ("GPT-5.4 Pro - Most capable, expensive ($30/$180 per 1M tokens)", "gpt-5.4-pro"), - ], - "anthropic": [ - ("Claude Opus 4.6 - Most intelligent, agents and coding", "claude-opus-4-6"), - ("Claude Opus 4.5 - Premium, max intelligence", "claude-opus-4-5"), - ("Claude Sonnet 4.6 - Best speed and intelligence balance", "claude-sonnet-4-6"), - ("Claude Sonnet 4.5 - Agents and coding", "claude-sonnet-4-5"), - ], - "google": [ - ("Gemini 3.1 Pro - Reasoning-first, complex workflows", "gemini-3.1-pro-preview"), - ("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"), - ("Gemini 2.5 Pro - Stable pro model", "gemini-2.5-pro"), - ("Gemini 2.5 Flash - Balanced, stable", "gemini-2.5-flash"), - ], - "xai": [ - ("Grok 4 - Flagship model", "grok-4-0709"), - ("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"), - ("Grok 4 Fast (Reasoning) - High-performance", "grok-4-fast-reasoning"), - ("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"), - ], - "openrouter": [ - ("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"), - ("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"), - ], - "ollama": [ - ("GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"), - ("GPT-OSS:latest (20B, local)", "gpt-oss:latest"), - ("Qwen3:latest (8B, local)", "qwen3:latest"), - ], - } - choice = questionary.select( "Select Your [Deep-Thinking LLM Engine]:", choices=[ questionary.Choice(display, value=value) - for display, value in DEEP_AGENT_OPTIONS[provider.lower()] + for display, value in get_model_options(provider, "deep") ], instruction="\n- Use arrow keys to navigate\n- Press Enter to select", style=questionary.Style( diff --git a/tests/test_model_validation.py b/tests/test_model_validation.py new file mode 100644 index 00000000..50f26318 --- /dev/null +++ b/tests/test_model_validation.py @@ -0,0 +1,52 @@ +import unittest +import warnings + +from tradingagents.llm_clients.base_client import BaseLLMClient +from tradingagents.llm_clients.model_catalog import get_known_models +from tradingagents.llm_clients.validators import validate_model + + +class DummyLLMClient(BaseLLMClient): + def __init__(self, provider: str, model: str): + self.provider = provider + super().__init__(model) + + def get_llm(self): + self.warn_if_unknown_model() + return object() + + def validate_model(self) -> bool: + return validate_model(self.provider, self.model) + + +class ModelValidationTests(unittest.TestCase): + def test_cli_catalog_models_are_all_validator_approved(self): + for provider, models in get_known_models().items(): + if provider in ("ollama", "openrouter"): + continue + + for model in models: + with self.subTest(provider=provider, model=model): + self.assertTrue(validate_model(provider, model)) + + def test_unknown_model_emits_warning_for_strict_provider(self): + client = DummyLLMClient("openai", "not-a-real-openai-model") + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + client.get_llm() + + self.assertEqual(len(caught), 1) + self.assertIn("not-a-real-openai-model", str(caught[0].message)) + self.assertIn("openai", str(caught[0].message)) + + def test_openrouter_and_ollama_accept_custom_models_without_warning(self): + for provider in ("openrouter", "ollama"): + client = DummyLLMClient(provider, "custom-model-name") + + with self.subTest(provider=provider): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + client.get_llm() + + self.assertEqual(caught, []) diff --git a/tradingagents/llm_clients/anthropic_client.py b/tradingagents/llm_clients/anthropic_client.py index 8539c752..939c7488 100644 --- a/tradingagents/llm_clients/anthropic_client.py +++ b/tradingagents/llm_clients/anthropic_client.py @@ -14,6 +14,7 @@ class AnthropicClient(BaseLLMClient): def get_llm(self) -> Any: """Return configured ChatAnthropic instance.""" + self.warn_if_unknown_model() llm_kwargs = {"model": self.model} for key in ("timeout", "max_retries", "api_key", "max_tokens", "callbacks", "http_client", "http_async_client"): diff --git a/tradingagents/llm_clients/base_client.py b/tradingagents/llm_clients/base_client.py index 43845575..81880856 100644 --- a/tradingagents/llm_clients/base_client.py +++ b/tradingagents/llm_clients/base_client.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from typing import Any, Optional +import warnings class BaseLLMClient(ABC): @@ -10,6 +11,27 @@ class BaseLLMClient(ABC): self.base_url = base_url self.kwargs = kwargs + def get_provider_name(self) -> str: + """Return the provider name used in warning messages.""" + provider = getattr(self, "provider", None) + if provider: + return str(provider) + return self.__class__.__name__.removesuffix("Client").lower() + + def warn_if_unknown_model(self) -> None: + """Warn when the model is outside the known list for the provider.""" + if self.validate_model(): + return + + warnings.warn( + ( + f"Model '{self.model}' is not in the known model list for " + f"provider '{self.get_provider_name()}'. Continuing anyway." + ), + RuntimeWarning, + stacklevel=2, + ) + @abstractmethod def get_llm(self) -> Any: """Return the configured LLM instance.""" diff --git a/tradingagents/llm_clients/google_client.py b/tradingagents/llm_clients/google_client.py index 3dd85e3f..557e2640 100644 --- a/tradingagents/llm_clients/google_client.py +++ b/tradingagents/llm_clients/google_client.py @@ -36,6 +36,7 @@ class GoogleClient(BaseLLMClient): def get_llm(self) -> Any: """Return configured ChatGoogleGenerativeAI instance.""" + self.warn_if_unknown_model() llm_kwargs = {"model": self.model} for key in ("timeout", "max_retries", "google_api_key", "callbacks", "http_client", "http_async_client"): diff --git a/tradingagents/llm_clients/model_catalog.py b/tradingagents/llm_clients/model_catalog.py new file mode 100644 index 00000000..58447a89 --- /dev/null +++ b/tradingagents/llm_clients/model_catalog.py @@ -0,0 +1,106 @@ +"""Shared model catalog for CLI selections and validation.""" + +from __future__ import annotations + +from typing import Dict, List, Tuple + +ModelOption = Tuple[str, str] +ProviderModeOptions = Dict[str, List[ModelOption]] + + +MODEL_OPTIONS: ProviderModeOptions = { + "openai": { + "quick": [ + ("GPT-5 Mini - Balanced speed, cost, and capability", "gpt-5-mini"), + ("GPT-5 Nano - High-throughput, simple tasks", "gpt-5-nano"), + ("GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"), + ("GPT-4.1 - Smartest non-reasoning model", "gpt-4.1"), + ], + "deep": [ + ("GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"), + ("GPT-5.2 - Strong reasoning, cost-effective", "gpt-5.2"), + ("GPT-5 Mini - Balanced speed, cost, and capability", "gpt-5-mini"), + ("GPT-5.4 Pro - Most capable, expensive ($30/$180 per 1M tokens)", "gpt-5.4-pro"), + ], + }, + "anthropic": { + "quick": [ + ("Claude Sonnet 4.6 - Best speed and intelligence balance", "claude-sonnet-4-6"), + ("Claude Haiku 4.5 - Fast, near-instant responses", "claude-haiku-4-5"), + ("Claude Sonnet 4.5 - Agents and coding", "claude-sonnet-4-5"), + ], + "deep": [ + ("Claude Opus 4.6 - Most intelligent, agents and coding", "claude-opus-4-6"), + ("Claude Opus 4.5 - Premium, max intelligence", "claude-opus-4-5"), + ("Claude Sonnet 4.6 - Best speed and intelligence balance", "claude-sonnet-4-6"), + ("Claude Sonnet 4.5 - Agents and coding", "claude-sonnet-4-5"), + ], + }, + "google": { + "quick": [ + ("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"), + ("Gemini 2.5 Flash - Balanced, stable", "gemini-2.5-flash"), + ("Gemini 3.1 Flash Lite - Most cost-efficient", "gemini-3.1-flash-lite-preview"), + ("Gemini 2.5 Flash Lite - Fast, low-cost", "gemini-2.5-flash-lite"), + ], + "deep": [ + ("Gemini 3.1 Pro - Reasoning-first, complex workflows", "gemini-3.1-pro-preview"), + ("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"), + ("Gemini 2.5 Pro - Stable pro model", "gemini-2.5-pro"), + ("Gemini 2.5 Flash - Balanced, stable", "gemini-2.5-flash"), + ], + }, + "xai": { + "quick": [ + ("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"), + ("Grok 4 Fast (Non-Reasoning) - Speed optimized", "grok-4-fast-non-reasoning"), + ("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"), + ], + "deep": [ + ("Grok 4 - Flagship model", "grok-4-0709"), + ("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"), + ("Grok 4 Fast (Reasoning) - High-performance", "grok-4-fast-reasoning"), + ("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"), + ], + }, + "openrouter": { + "quick": [ + ("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"), + ("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"), + ], + "deep": [ + ("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"), + ("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"), + ], + }, + "ollama": { + "quick": [ + ("Qwen3:latest (8B, local)", "qwen3:latest"), + ("GPT-OSS:latest (20B, local)", "gpt-oss:latest"), + ("GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"), + ], + "deep": [ + ("GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"), + ("GPT-OSS:latest (20B, local)", "gpt-oss:latest"), + ("Qwen3:latest (8B, local)", "qwen3:latest"), + ], + }, +} + + +def get_model_options(provider: str, mode: str) -> List[ModelOption]: + """Return shared model options for a provider and selection mode.""" + return MODEL_OPTIONS[provider.lower()][mode] + + +def get_known_models() -> Dict[str, List[str]]: + """Build known model names from the shared CLI catalog.""" + known_models: Dict[str, List[str]] = {} + for provider, mode_options in MODEL_OPTIONS.items(): + model_names = { + value + for options in mode_options.values() + for _, value in options + } + known_models[provider] = sorted(model_names) + return known_models diff --git a/tradingagents/llm_clients/openai_client.py b/tradingagents/llm_clients/openai_client.py index 4605c1f9..0629d894 100644 --- a/tradingagents/llm_clients/openai_client.py +++ b/tradingagents/llm_clients/openai_client.py @@ -41,6 +41,7 @@ class OpenAIClient(BaseLLMClient): def get_llm(self) -> Any: """Return configured ChatOpenAI instance.""" + self.warn_if_unknown_model() llm_kwargs = {"model": self.model} if self.provider == "xai": diff --git a/tradingagents/llm_clients/validators.py b/tradingagents/llm_clients/validators.py index 1e2388b3..4e6d457b 100644 --- a/tradingagents/llm_clients/validators.py +++ b/tradingagents/llm_clients/validators.py @@ -1,53 +1,12 @@ -"""Model name validators for each provider. +"""Model name validators for each provider.""" + +from .model_catalog import get_known_models -Only validates model names - does NOT enforce limits. -Let LLM providers use their own defaults for unspecified params. -""" VALID_MODELS = { - "openai": [ - # GPT-5 series - "gpt-5.4-pro", - "gpt-5.4", - "gpt-5.2", - "gpt-5.1", - "gpt-5", - "gpt-5-mini", - "gpt-5-nano", - # GPT-4.1 series - "gpt-4.1", - "gpt-4.1-mini", - "gpt-4.1-nano", - ], - "anthropic": [ - # Claude 4.6 series (latest) - "claude-opus-4-6", - "claude-sonnet-4-6", - # Claude 4.5 series - "claude-opus-4-5", - "claude-sonnet-4-5", - "claude-haiku-4-5", - ], - "google": [ - # Gemini 3.1 series (preview) - "gemini-3.1-pro-preview", - "gemini-3.1-flash-lite-preview", - # Gemini 3 series (preview) - "gemini-3-flash-preview", - # Gemini 2.5 series - "gemini-2.5-pro", - "gemini-2.5-flash", - "gemini-2.5-flash-lite", - ], - "xai": [ - # Grok 4.1 series - "grok-4-1-fast-reasoning", - "grok-4-1-fast-non-reasoning", - # Grok 4 series - "grok-4-0709", - "grok-4-fast-reasoning", - "grok-4-fast-non-reasoning", - ], + provider: models + for provider, models in get_known_models().items() + if provider not in ("ollama", "openrouter") } From bd6a5b75b5361654acd8ed0d935b301573b8f992 Mon Sep 17 00:00:00 2001 From: CadeYu Date: Wed, 25 Mar 2026 21:46:56 +0800 Subject: [PATCH 15/30] fix model catalog typing and known-model helper --- tradingagents/llm_clients/model_catalog.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tradingagents/llm_clients/model_catalog.py b/tradingagents/llm_clients/model_catalog.py index 58447a89..f147c5e1 100644 --- a/tradingagents/llm_clients/model_catalog.py +++ b/tradingagents/llm_clients/model_catalog.py @@ -5,7 +5,7 @@ from __future__ import annotations from typing import Dict, List, Tuple ModelOption = Tuple[str, str] -ProviderModeOptions = Dict[str, List[ModelOption]] +ProviderModeOptions = Dict[str, Dict[str, List[ModelOption]]] MODEL_OPTIONS: ProviderModeOptions = { @@ -95,12 +95,13 @@ def get_model_options(provider: str, mode: str) -> List[ModelOption]: def get_known_models() -> Dict[str, List[str]]: """Build known model names from the shared CLI catalog.""" - known_models: Dict[str, List[str]] = {} - for provider, mode_options in MODEL_OPTIONS.items(): - model_names = { - value - for options in mode_options.values() - for _, value in options - } - known_models[provider] = sorted(model_names) - return known_models + return { + provider: sorted( + { + value + for options in mode_options.values() + for _, value in options + } + ) + for provider, mode_options in MODEL_OPTIONS.items() + } From e1113880a1da00c80258612657fd4f8e68a79ef2 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 29 Mar 2026 17:34:35 +0000 Subject: [PATCH 16/30] fix: prevent look-ahead bias in backtesting data fetchers (#475) --- .../dataflows/alpha_vantage_fundamentals.py | 80 ++++++---------- tradingagents/dataflows/stockstats_utils.py | 94 ++++++++++++------- tradingagents/dataflows/y_finance.py | 71 +++----------- tradingagents/dataflows/yfinance_news.py | 5 + 4 files changed, 108 insertions(+), 142 deletions(-) diff --git a/tradingagents/dataflows/alpha_vantage_fundamentals.py b/tradingagents/dataflows/alpha_vantage_fundamentals.py index 8b92faa6..a4ef24c0 100644 --- a/tradingagents/dataflows/alpha_vantage_fundamentals.py +++ b/tradingagents/dataflows/alpha_vantage_fundamentals.py @@ -1,6 +1,23 @@ from .alpha_vantage_common import _make_api_request +def _filter_reports_by_date(result, curr_date: str): + """Filter annualReports/quarterlyReports to exclude entries after curr_date. + + Prevents look-ahead bias by removing fiscal periods that end after + the simulation's current date. + """ + if not curr_date or not isinstance(result, dict): + return result + for key in ("annualReports", "quarterlyReports"): + if key in result: + result[key] = [ + r for r in result[key] + if r.get("fiscalDateEnding", "") <= curr_date + ] + return result + + def get_fundamentals(ticker: str, curr_date: str = None) -> str: """ Retrieve comprehensive fundamental data for a given ticker symbol using Alpha Vantage. @@ -19,59 +36,20 @@ def get_fundamentals(ticker: str, curr_date: str = None) -> str: return _make_api_request("OVERVIEW", params) -def get_balance_sheet(ticker: str, freq: str = "quarterly", curr_date: str = None) -> str: - """ - Retrieve balance sheet data for a given ticker symbol using Alpha Vantage. - - Args: - ticker (str): Ticker symbol of the company - freq (str): Reporting frequency: annual/quarterly (default quarterly) - not used for Alpha Vantage - curr_date (str): Current date you are trading at, yyyy-mm-dd (not used for Alpha Vantage) - - Returns: - str: Balance sheet data with normalized fields - """ - params = { - "symbol": ticker, - } - - return _make_api_request("BALANCE_SHEET", params) +def get_balance_sheet(ticker: str, freq: str = "quarterly", curr_date: str = None): + """Retrieve balance sheet data for a given ticker symbol using Alpha Vantage.""" + result = _make_api_request("BALANCE_SHEET", {"symbol": ticker}) + return _filter_reports_by_date(result, curr_date) -def get_cashflow(ticker: str, freq: str = "quarterly", curr_date: str = None) -> str: - """ - Retrieve cash flow statement data for a given ticker symbol using Alpha Vantage. - - Args: - ticker (str): Ticker symbol of the company - freq (str): Reporting frequency: annual/quarterly (default quarterly) - not used for Alpha Vantage - curr_date (str): Current date you are trading at, yyyy-mm-dd (not used for Alpha Vantage) - - Returns: - str: Cash flow statement data with normalized fields - """ - params = { - "symbol": ticker, - } - - return _make_api_request("CASH_FLOW", params) +def get_cashflow(ticker: str, freq: str = "quarterly", curr_date: str = None): + """Retrieve cash flow statement data for a given ticker symbol using Alpha Vantage.""" + result = _make_api_request("CASH_FLOW", {"symbol": ticker}) + return _filter_reports_by_date(result, curr_date) -def get_income_statement(ticker: str, freq: str = "quarterly", curr_date: str = None) -> str: - """ - Retrieve income statement data for a given ticker symbol using Alpha Vantage. - - Args: - ticker (str): Ticker symbol of the company - freq (str): Reporting frequency: annual/quarterly (default quarterly) - not used for Alpha Vantage - curr_date (str): Current date you are trading at, yyyy-mm-dd (not used for Alpha Vantage) - - Returns: - str: Income statement data with normalized fields - """ - params = { - "symbol": ticker, - } - - return _make_api_request("INCOME_STATEMENT", params) +def get_income_statement(ticker: str, freq: str = "quarterly", curr_date: str = None): + """Retrieve income statement data for a given ticker symbol using Alpha Vantage.""" + result = _make_api_request("INCOME_STATEMENT", {"symbol": ticker}) + return _filter_reports_by_date(result, curr_date) diff --git a/tradingagents/dataflows/stockstats_utils.py b/tradingagents/dataflows/stockstats_utils.py index 47d5460a..50747883 100644 --- a/tradingagents/dataflows/stockstats_utils.py +++ b/tradingagents/dataflows/stockstats_utils.py @@ -44,6 +44,64 @@ def _clean_dataframe(data: pd.DataFrame) -> pd.DataFrame: return data +def load_ohlcv(symbol: str, curr_date: str) -> pd.DataFrame: + """Fetch OHLCV data with caching, filtered to prevent look-ahead bias. + + Downloads 15 years of data up to today and caches per symbol. On + subsequent calls the cache is reused. Rows after curr_date are + filtered out so backtests never see future prices. + """ + config = get_config() + curr_date_dt = pd.to_datetime(curr_date) + + # Cache uses a fixed window (15y to today) so one file per symbol + today_date = pd.Timestamp.today() + start_date = today_date - pd.DateOffset(years=5) + start_str = start_date.strftime("%Y-%m-%d") + end_str = today_date.strftime("%Y-%m-%d") + + os.makedirs(config["data_cache_dir"], exist_ok=True) + data_file = os.path.join( + config["data_cache_dir"], + f"{symbol}-YFin-data-{start_str}-{end_str}.csv", + ) + + if os.path.exists(data_file): + data = pd.read_csv(data_file, on_bad_lines="skip") + else: + data = yf_retry(lambda: yf.download( + symbol, + start=start_str, + end=end_str, + multi_level_index=False, + progress=False, + auto_adjust=True, + )) + data = data.reset_index() + data.to_csv(data_file, index=False) + + data = _clean_dataframe(data) + + # Filter to curr_date to prevent look-ahead bias in backtesting + data = data[data["Date"] <= curr_date_dt] + + return data + + +def filter_financials_by_date(data: pd.DataFrame, curr_date: str) -> pd.DataFrame: + """Drop financial statement columns (fiscal period timestamps) after curr_date. + + yfinance financial statements use fiscal period end dates as columns. + Columns after curr_date represent future data and are removed to + prevent look-ahead bias. + """ + if not curr_date or data.empty: + return data + cutoff = pd.Timestamp(curr_date) + mask = pd.to_datetime(data.columns, errors="coerce") <= cutoff + return data.loc[:, mask] + + class StockstatsUtils: @staticmethod def get_stock_stats( @@ -55,42 +113,10 @@ class StockstatsUtils: str, "curr date for retrieving stock price data, YYYY-mm-dd" ], ): - config = get_config() - - today_date = pd.Timestamp.today() - curr_date_dt = pd.to_datetime(curr_date) - - end_date = today_date - start_date = today_date - pd.DateOffset(years=15) - start_date_str = start_date.strftime("%Y-%m-%d") - end_date_str = end_date.strftime("%Y-%m-%d") - - # Ensure cache directory exists - os.makedirs(config["data_cache_dir"], exist_ok=True) - - data_file = os.path.join( - config["data_cache_dir"], - f"{symbol}-YFin-data-{start_date_str}-{end_date_str}.csv", - ) - - if os.path.exists(data_file): - data = pd.read_csv(data_file, on_bad_lines="skip") - else: - data = yf_retry(lambda: yf.download( - symbol, - start=start_date_str, - end=end_date_str, - multi_level_index=False, - progress=False, - auto_adjust=True, - )) - data = data.reset_index() - data.to_csv(data_file, index=False) - - data = _clean_dataframe(data) + data = load_ohlcv(symbol, curr_date) df = wrap(data) df["Date"] = df["Date"].dt.strftime("%Y-%m-%d") - curr_date_str = curr_date_dt.strftime("%Y-%m-%d") + curr_date_str = pd.to_datetime(curr_date).strftime("%Y-%m-%d") df[indicator] # trigger stockstats to calculate the indicator matching_rows = df[df["Date"].str.startswith(curr_date_str)] diff --git a/tradingagents/dataflows/y_finance.py b/tradingagents/dataflows/y_finance.py index 3682a01d..8b4b93f5 100644 --- a/tradingagents/dataflows/y_finance.py +++ b/tradingagents/dataflows/y_finance.py @@ -3,7 +3,7 @@ from datetime import datetime from dateutil.relativedelta import relativedelta import yfinance as yf import os -from .stockstats_utils import StockstatsUtils, _clean_dataframe, yf_retry +from .stockstats_utils import StockstatsUtils, _clean_dataframe, yf_retry, load_ohlcv, filter_financials_by_date def get_YFin_data_online( symbol: Annotated[str, "ticker symbol of the company"], @@ -194,58 +194,9 @@ def _get_stock_stats_bulk( Fetches data once and calculates indicator for all available dates. Returns dict mapping date strings to indicator values. """ - from .config import get_config - import pandas as pd from stockstats import wrap - import os - - config = get_config() - online = config["data_vendors"]["technical_indicators"] != "local" - - if not online: - # Local data path - try: - data = pd.read_csv( - os.path.join( - config.get("data_cache_dir", "data"), - f"{symbol}-YFin-data-2015-01-01-2025-03-25.csv", - ), - on_bad_lines="skip", - ) - except FileNotFoundError: - raise Exception("Stockstats fail: Yahoo Finance data not fetched yet!") - else: - # Online data fetching with caching - today_date = pd.Timestamp.today() - curr_date_dt = pd.to_datetime(curr_date) - end_date = today_date - start_date = today_date - pd.DateOffset(years=15) - start_date_str = start_date.strftime("%Y-%m-%d") - end_date_str = end_date.strftime("%Y-%m-%d") - - os.makedirs(config["data_cache_dir"], exist_ok=True) - - data_file = os.path.join( - config["data_cache_dir"], - f"{symbol}-YFin-data-{start_date_str}-{end_date_str}.csv", - ) - - if os.path.exists(data_file): - data = pd.read_csv(data_file, on_bad_lines="skip") - else: - data = yf_retry(lambda: yf.download( - symbol, - start=start_date_str, - end=end_date_str, - multi_level_index=False, - progress=False, - auto_adjust=True, - )) - data = data.reset_index() - data.to_csv(data_file, index=False) - - data = _clean_dataframe(data) + data = load_ohlcv(symbol, curr_date) df = wrap(data) df["Date"] = df["Date"].dt.strftime("%Y-%m-%d") @@ -353,7 +304,7 @@ def get_fundamentals( def get_balance_sheet( ticker: Annotated[str, "ticker symbol of the company"], freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly", - curr_date: Annotated[str, "current date (not used for yfinance)"] = None + curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None ): """Get balance sheet data from yfinance.""" try: @@ -363,7 +314,9 @@ def get_balance_sheet( data = yf_retry(lambda: ticker_obj.quarterly_balance_sheet) else: data = yf_retry(lambda: ticker_obj.balance_sheet) - + + data = filter_financials_by_date(data, curr_date) + if data.empty: return f"No balance sheet data found for symbol '{ticker}'" @@ -383,7 +336,7 @@ def get_balance_sheet( def get_cashflow( ticker: Annotated[str, "ticker symbol of the company"], freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly", - curr_date: Annotated[str, "current date (not used for yfinance)"] = None + curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None ): """Get cash flow data from yfinance.""" try: @@ -393,7 +346,9 @@ def get_cashflow( data = yf_retry(lambda: ticker_obj.quarterly_cashflow) else: data = yf_retry(lambda: ticker_obj.cashflow) - + + data = filter_financials_by_date(data, curr_date) + if data.empty: return f"No cash flow data found for symbol '{ticker}'" @@ -413,7 +368,7 @@ def get_cashflow( def get_income_statement( ticker: Annotated[str, "ticker symbol of the company"], freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly", - curr_date: Annotated[str, "current date (not used for yfinance)"] = None + curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None ): """Get income statement data from yfinance.""" try: @@ -423,7 +378,9 @@ def get_income_statement( data = yf_retry(lambda: ticker_obj.quarterly_income_stmt) else: data = yf_retry(lambda: ticker_obj.income_stmt) - + + data = filter_financials_by_date(data, curr_date) + if data.empty: return f"No income statement data found for symbol '{ticker}'" diff --git a/tradingagents/dataflows/yfinance_news.py b/tradingagents/dataflows/yfinance_news.py index 20e9120d..7254ebc3 100644 --- a/tradingagents/dataflows/yfinance_news.py +++ b/tradingagents/dataflows/yfinance_news.py @@ -167,6 +167,11 @@ def get_global_news_yfinance( # Handle both flat and nested structures if "content" in article: data = _extract_article_data(article) + # Skip articles published after curr_date (look-ahead guard) + if data.get("pub_date"): + pub_naive = data["pub_date"].replace(tzinfo=None) if hasattr(data["pub_date"], "replace") else data["pub_date"] + if pub_naive > curr_dt + relativedelta(days=1): + continue title = data["title"] publisher = data["publisher"] link = data["link"] From f3f58bdbdcb6200e70dc1689254af19333f7c8f3 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 29 Mar 2026 17:42:24 +0000 Subject: [PATCH 17/30] fix: add yf_retry to yfinance news fetchers (#445) --- tradingagents/dataflows/yfinance_news.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tradingagents/dataflows/yfinance_news.py b/tradingagents/dataflows/yfinance_news.py index 7254ebc3..dd1046f5 100644 --- a/tradingagents/dataflows/yfinance_news.py +++ b/tradingagents/dataflows/yfinance_news.py @@ -4,6 +4,8 @@ import yfinance as yf from datetime import datetime from dateutil.relativedelta import relativedelta +from .stockstats_utils import yf_retry + def _extract_article_data(article: dict) -> dict: """Extract article data from yfinance news format (handles nested 'content' structure).""" @@ -64,7 +66,7 @@ def get_news_yfinance( """ try: stock = yf.Ticker(ticker) - news = stock.get_news(count=20) + news = yf_retry(lambda: stock.get_news(count=20)) if not news: return f"No news found for {ticker}" @@ -131,11 +133,11 @@ def get_global_news_yfinance( try: for query in search_queries: - search = yf.Search( - query=query, + search = yf_retry(lambda q=query: yf.Search( + query=q, news_count=limit, enable_fuzzy_query=True, - ) + )) if search.news: for article in search.news: From ae8c8aebe85179590a2af5ce4622de6b9067f9d1 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 29 Mar 2026 17:50:30 +0000 Subject: [PATCH 18/30] fix: gracefully handle invalid indicator names in tool calls (#429) --- .../agents/utils/technical_indicators_tools.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tradingagents/agents/utils/technical_indicators_tools.py b/tradingagents/agents/utils/technical_indicators_tools.py index 77acf09c..dc982580 100644 --- a/tradingagents/agents/utils/technical_indicators_tools.py +++ b/tradingagents/agents/utils/technical_indicators_tools.py @@ -23,9 +23,10 @@ def get_indicators( # LLMs sometimes pass multiple indicators as a comma-separated string; # split and process each individually. indicators = [i.strip() for i in indicator.split(",") if i.strip()] - if len(indicators) > 1: - results = [] - for ind in indicators: + results = [] + for ind in indicators: + try: results.append(route_to_vendor("get_indicators", symbol, ind, curr_date, look_back_days)) - return "\n\n".join(results) - return route_to_vendor("get_indicators", symbol, indicator.strip(), curr_date, look_back_days) \ No newline at end of file + except ValueError as e: + results.append(str(e)) + return "\n\n".join(results) \ No newline at end of file From 58e99421bd8ae3cab7820f2ca0e8398892d71425 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 29 Mar 2026 17:59:52 +0000 Subject: [PATCH 19/30] fix: pass base_url to Google and Anthropic clients for proxy support (#427) --- tradingagents/llm_clients/TODO.md | 12 ++++-------- tradingagents/llm_clients/anthropic_client.py | 3 +++ tradingagents/llm_clients/google_client.py | 3 +++ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tradingagents/llm_clients/TODO.md b/tradingagents/llm_clients/TODO.md index f666665d..2d3fe915 100644 --- a/tradingagents/llm_clients/TODO.md +++ b/tradingagents/llm_clients/TODO.md @@ -7,13 +7,9 @@ ### 2. ~~Inconsistent parameter handling~~ (Fixed) - GoogleClient now accepts unified `api_key` and maps it to `google_api_key` -- Legacy `google_api_key` still works for backward compatibility -### 3. `base_url` accepted but ignored -- `AnthropicClient`: accepts `base_url` but never uses it -- `GoogleClient`: accepts `base_url` but never uses it (correct - Google doesn't support it) +### 3. ~~`base_url` accepted but ignored~~ (Fixed) +- All clients now pass `base_url` to their respective LLM constructors -**Fix:** Remove unused `base_url` from clients that don't support it - -### 4. Update validators.py with models from CLI -- Sync `VALID_MODELS` dict with CLI model options after Feature 2 is complete +### 4. ~~Update validators.py with models from CLI~~ (Fixed) +- Synced in v0.2.2 diff --git a/tradingagents/llm_clients/anthropic_client.py b/tradingagents/llm_clients/anthropic_client.py index 2c1e5a67..27b01234 100644 --- a/tradingagents/llm_clients/anthropic_client.py +++ b/tradingagents/llm_clients/anthropic_client.py @@ -33,6 +33,9 @@ class AnthropicClient(BaseLLMClient): """Return configured ChatAnthropic instance.""" llm_kwargs = {"model": self.model} + if self.base_url: + llm_kwargs["base_url"] = self.base_url + for key in _PASSTHROUGH_KWARGS: if key in self.kwargs: llm_kwargs[key] = self.kwargs[key] diff --git a/tradingagents/llm_clients/google_client.py b/tradingagents/llm_clients/google_client.py index f9971aa6..755ff4ed 100644 --- a/tradingagents/llm_clients/google_client.py +++ b/tradingagents/llm_clients/google_client.py @@ -27,6 +27,9 @@ class GoogleClient(BaseLLMClient): """Return configured ChatGoogleGenerativeAI instance.""" llm_kwargs = {"model": self.model} + if self.base_url: + llm_kwargs["base_url"] = self.base_url + for key in ("timeout", "max_retries", "callbacks", "http_client", "http_async_client"): if key in self.kwargs: llm_kwargs[key] = self.kwargs[key] From 6cddd26d6eab51e12ca8ab73b02bf9372980ca19 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 29 Mar 2026 19:19:01 +0000 Subject: [PATCH 20/30] feat: multi-language output support for analyst reports and final decision (#472) --- cli/main.py | 37 ++++++++++++------- cli/utils.py | 34 +++++++++++++++++ .../agents/analysts/fundamentals_analyst.py | 4 +- .../agents/analysts/market_analyst.py | 2 + tradingagents/agents/analysts/news_analyst.py | 2 + .../agents/analysts/social_media_analyst.py | 3 +- .../agents/managers/portfolio_manager.py | 4 +- tradingagents/agents/utils/agent_utils.py | 14 +++++++ tradingagents/default_config.py | 3 ++ 9 files changed, 86 insertions(+), 17 deletions(-) diff --git a/cli/main.py b/cli/main.py index 53837db2..29294d8d 100644 --- a/cli/main.py +++ b/cli/main.py @@ -519,10 +519,19 @@ def get_user_selections(): ) analysis_date = get_analysis_date() - # Step 3: Select analysts + # Step 3: Output language console.print( create_question_box( - "Step 3: Analysts Team", "Select your LLM analyst agents for the analysis" + "Step 3: Output Language", + "Select the language for analyst reports and final decision" + ) + ) + output_language = ask_output_language() + + # Step 4: Select analysts + console.print( + create_question_box( + "Step 4: Analysts Team", "Select your LLM analyst agents for the analysis" ) ) selected_analysts = select_analysts() @@ -530,32 +539,32 @@ def get_user_selections(): f"[green]Selected analysts:[/green] {', '.join(analyst.value for analyst in selected_analysts)}" ) - # Step 4: Research depth + # Step 5: Research depth console.print( create_question_box( - "Step 4: Research Depth", "Select your research depth level" + "Step 5: Research Depth", "Select your research depth level" ) ) selected_research_depth = select_research_depth() - # Step 5: OpenAI backend + # Step 6: LLM Provider console.print( create_question_box( - "Step 5: OpenAI backend", "Select which service to talk to" + "Step 6: LLM Provider", "Select your LLM provider" ) ) selected_llm_provider, backend_url = select_llm_provider() - - # Step 6: Thinking agents + + # Step 7: Thinking agents console.print( create_question_box( - "Step 6: Thinking Agents", "Select your thinking agents for analysis" + "Step 7: Thinking Agents", "Select your thinking agents for analysis" ) ) selected_shallow_thinker = select_shallow_thinking_agent(selected_llm_provider) selected_deep_thinker = select_deep_thinking_agent(selected_llm_provider) - # Step 7: Provider-specific thinking configuration + # Step 8: Provider-specific thinking configuration thinking_level = None reasoning_effort = None anthropic_effort = None @@ -564,7 +573,7 @@ def get_user_selections(): if provider_lower == "google": console.print( create_question_box( - "Step 7: Thinking Mode", + "Step 8: Thinking Mode", "Configure Gemini thinking mode" ) ) @@ -572,7 +581,7 @@ def get_user_selections(): elif provider_lower == "openai": console.print( create_question_box( - "Step 7: Reasoning Effort", + "Step 8: Reasoning Effort", "Configure OpenAI reasoning effort level" ) ) @@ -580,7 +589,7 @@ def get_user_selections(): elif provider_lower == "anthropic": console.print( create_question_box( - "Step 7: Effort Level", + "Step 8: Effort Level", "Configure Claude effort level" ) ) @@ -598,6 +607,7 @@ def get_user_selections(): "google_thinking_level": thinking_level, "openai_reasoning_effort": reasoning_effort, "anthropic_effort": anthropic_effort, + "output_language": output_language, } @@ -931,6 +941,7 @@ def run_analysis(): config["google_thinking_level"] = selections.get("google_thinking_level") config["openai_reasoning_effort"] = selections.get("openai_reasoning_effort") config["anthropic_effort"] = selections.get("anthropic_effort") + config["output_language"] = selections.get("output_language", "English") # Create stats callback handler for tracking LLM/tool calls stats_handler = StatsCallbackHandler() diff --git a/cli/utils.py b/cli/utils.py index 0166cd95..62b50c9c 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -281,3 +281,37 @@ def ask_gemini_thinking_config() -> str | None: ("pointer", "fg:green noinherit"), ]), ).ask() + + +def ask_output_language() -> str: + """Ask for report output language.""" + choice = questionary.select( + "Select Output Language:", + choices=[ + questionary.Choice("English (default)", "English"), + questionary.Choice("Chinese (中文)", "Chinese"), + questionary.Choice("Japanese (日本語)", "Japanese"), + questionary.Choice("Korean (한국어)", "Korean"), + questionary.Choice("Hindi (हिन्दी)", "Hindi"), + questionary.Choice("Spanish (Español)", "Spanish"), + questionary.Choice("Portuguese (Português)", "Portuguese"), + questionary.Choice("French (Français)", "French"), + questionary.Choice("German (Deutsch)", "German"), + questionary.Choice("Arabic (العربية)", "Arabic"), + questionary.Choice("Russian (Русский)", "Russian"), + questionary.Choice("Custom language", "custom"), + ], + style=questionary.Style([ + ("selected", "fg:yellow noinherit"), + ("highlighted", "fg:yellow noinherit"), + ("pointer", "fg:yellow noinherit"), + ]), + ).ask() + + if choice == "custom": + return questionary.text( + "Enter language name (e.g. Turkish, Vietnamese, Thai, Indonesian):", + validate=lambda x: len(x.strip()) > 0 or "Please enter a language name.", + ).ask().strip() + + return choice diff --git a/tradingagents/agents/analysts/fundamentals_analyst.py b/tradingagents/agents/analysts/fundamentals_analyst.py index 990398a6..3f70c734 100644 --- a/tradingagents/agents/analysts/fundamentals_analyst.py +++ b/tradingagents/agents/analysts/fundamentals_analyst.py @@ -8,6 +8,7 @@ from tradingagents.agents.utils.agent_utils import ( get_fundamentals, get_income_statement, get_insider_transactions, + get_language_instruction, ) from tradingagents.dataflows.config import get_config @@ -27,7 +28,8 @@ def create_fundamentals_analyst(llm): system_message = ( "You are a researcher tasked with analyzing fundamental information over the past week about a company. Please write a comprehensive report of the company's fundamental information such as financial documents, company profile, basic company financials, and company financial history to gain a full view of the company's fundamental information to inform traders. Make sure to include as much detail as possible. Provide specific, actionable insights with supporting evidence to help traders make informed decisions." + " Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read." - + " Use the available tools: `get_fundamentals` for comprehensive company analysis, `get_balance_sheet`, `get_cashflow`, and `get_income_statement` for specific financial statements.", + + " Use the available tools: `get_fundamentals` for comprehensive company analysis, `get_balance_sheet`, `get_cashflow`, and `get_income_statement` for specific financial statements." + + get_language_instruction(), ) prompt = ChatPromptTemplate.from_messages( diff --git a/tradingagents/agents/analysts/market_analyst.py b/tradingagents/agents/analysts/market_analyst.py index f5d17acd..680f9019 100644 --- a/tradingagents/agents/analysts/market_analyst.py +++ b/tradingagents/agents/analysts/market_analyst.py @@ -4,6 +4,7 @@ import json from tradingagents.agents.utils.agent_utils import ( build_instrument_context, get_indicators, + get_language_instruction, get_stock_data, ) from tradingagents.dataflows.config import get_config @@ -47,6 +48,7 @@ Volume-Based Indicators: - Select indicators that provide diverse and complementary information. Avoid redundancy (e.g., do not select both rsi and stochrsi). Also briefly explain why they are suitable for the given market context. When you tool call, please use the exact name of the indicators provided above as they are defined parameters, otherwise your call will fail. Please make sure to call get_stock_data first to retrieve the CSV that is needed to generate indicators. Then use get_indicators with the specific indicator names. Write a very detailed and nuanced report of the trends you observe. Provide specific, actionable insights with supporting evidence to help traders make informed decisions.""" + """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read.""" + + get_language_instruction() ) prompt = ChatPromptTemplate.from_messages( diff --git a/tradingagents/agents/analysts/news_analyst.py b/tradingagents/agents/analysts/news_analyst.py index 3697c6f6..42fc7a61 100644 --- a/tradingagents/agents/analysts/news_analyst.py +++ b/tradingagents/agents/analysts/news_analyst.py @@ -4,6 +4,7 @@ import json from tradingagents.agents.utils.agent_utils import ( build_instrument_context, get_global_news, + get_language_instruction, get_news, ) from tradingagents.dataflows.config import get_config @@ -22,6 +23,7 @@ def create_news_analyst(llm): system_message = ( "You are a news researcher tasked with analyzing recent news and trends over the past week. Please write a comprehensive report of the current state of the world that is relevant for trading and macroeconomics. Use the available tools: get_news(query, start_date, end_date) for company-specific or targeted news searches, and get_global_news(curr_date, look_back_days, limit) for broader macroeconomic news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions." + """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read.""" + + get_language_instruction() ) prompt = ChatPromptTemplate.from_messages( diff --git a/tradingagents/agents/analysts/social_media_analyst.py b/tradingagents/agents/analysts/social_media_analyst.py index 43df2258..67d78f4c 100644 --- a/tradingagents/agents/analysts/social_media_analyst.py +++ b/tradingagents/agents/analysts/social_media_analyst.py @@ -1,7 +1,7 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder import time import json -from tradingagents.agents.utils.agent_utils import build_instrument_context, get_news +from tradingagents.agents.utils.agent_utils import build_instrument_context, get_language_instruction, get_news from tradingagents.dataflows.config import get_config @@ -17,6 +17,7 @@ def create_social_media_analyst(llm): system_message = ( "You are a social media and company specific news researcher/analyst tasked with analyzing social media posts, recent company news, and public sentiment for a specific company over the past week. You will be given a company's name your objective is to write a comprehensive long report detailing your analysis, insights, and implications for traders and investors on this company's current state after looking at social media and what people are saying about that company, analyzing sentiment data of what people feel each day about the company, and looking at recent company news. Use the get_news(query, start_date, end_date) tool to search for company-specific news and social media discussions. Try to look at all sources possible from social media to sentiment to news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions." + """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read.""" + + get_language_instruction() ) prompt = ChatPromptTemplate.from_messages( diff --git a/tradingagents/agents/managers/portfolio_manager.py b/tradingagents/agents/managers/portfolio_manager.py index acdf940b..970efb46 100644 --- a/tradingagents/agents/managers/portfolio_manager.py +++ b/tradingagents/agents/managers/portfolio_manager.py @@ -1,4 +1,4 @@ -from tradingagents.agents.utils.agent_utils import build_instrument_context +from tradingagents.agents.utils.agent_utils import build_instrument_context, get_language_instruction def create_portfolio_manager(llm, memory): @@ -50,7 +50,7 @@ def create_portfolio_manager(llm, memory): --- -Be decisive and ground every conclusion in specific evidence from the analysts.""" +Be decisive and ground every conclusion in specific evidence from the analysts.{get_language_instruction()}""" response = llm.invoke(prompt) diff --git a/tradingagents/agents/utils/agent_utils.py b/tradingagents/agents/utils/agent_utils.py index e4abc4cd..4ba40a80 100644 --- a/tradingagents/agents/utils/agent_utils.py +++ b/tradingagents/agents/utils/agent_utils.py @@ -20,6 +20,20 @@ from tradingagents.agents.utils.news_data_tools import ( ) +def get_language_instruction() -> str: + """Return a prompt instruction for the configured output language. + + Returns empty string when English (default), so no extra tokens are used. + Only applied to user-facing agents (analysts, portfolio manager). + Internal debate agents stay in English for reasoning quality. + """ + from tradingagents.dataflows.config import get_config + lang = get_config().get("output_language", "English") + if lang.strip().lower() == "english": + return "" + return f" Write your entire response in {lang}." + + def build_instrument_context(ticker: str) -> str: """Describe the exact instrument so agents preserve exchange-qualified tickers.""" return ( diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py index 898e1e1e..31952c00 100644 --- a/tradingagents/default_config.py +++ b/tradingagents/default_config.py @@ -16,6 +16,9 @@ DEFAULT_CONFIG = { "google_thinking_level": None, # "high", "minimal", etc. "openai_reasoning_effort": None, # "medium", "high", "low" "anthropic_effort": None, # "high", "medium", "low" + # Output language for analyst reports and final decision + # Internal agent debate stays in English for reasoning quality + "output_language": "English", # Debate and discussion settings "max_debate_rounds": 1, "max_risk_discuss_rounds": 1, From e75d17bc51981b49ed47c6a2c2016100e0689e09 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 29 Mar 2026 19:45:36 +0000 Subject: [PATCH 21/30] chore: update model lists and defaults to GPT-5.4 family --- README.md | 4 ++-- main.py | 4 ++-- tradingagents/default_config.py | 4 ++-- tradingagents/llm_clients/model_catalog.py | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4c4856d1..41a124c7 100644 --- a/README.md +++ b/README.md @@ -189,8 +189,8 @@ from tradingagents.default_config import DEFAULT_CONFIG config = DEFAULT_CONFIG.copy() config["llm_provider"] = "openai" # openai, google, anthropic, xai, openrouter, ollama -config["deep_think_llm"] = "gpt-5.2" # Model for complex reasoning -config["quick_think_llm"] = "gpt-5-mini" # Model for quick tasks +config["deep_think_llm"] = "gpt-5.4" # Model for complex reasoning +config["quick_think_llm"] = "gpt-5.4-mini" # Model for quick tasks config["max_debate_rounds"] = 2 ta = TradingAgentsGraph(debug=True, config=config) diff --git a/main.py b/main.py index 26cab658..c94fde32 100644 --- a/main.py +++ b/main.py @@ -8,8 +8,8 @@ load_dotenv() # Create a custom config config = DEFAULT_CONFIG.copy() -config["deep_think_llm"] = "gpt-5-mini" # Use a different model -config["quick_think_llm"] = "gpt-5-mini" # Use a different model +config["deep_think_llm"] = "gpt-5.4-mini" # Use a different model +config["quick_think_llm"] = "gpt-5.4-mini" # Use a different model config["max_debate_rounds"] = 1 # Increase debate rounds # Configure data vendors (default uses yfinance, no extra API keys needed) diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py index 31952c00..26a4e4d2 100644 --- a/tradingagents/default_config.py +++ b/tradingagents/default_config.py @@ -9,8 +9,8 @@ DEFAULT_CONFIG = { ), # LLM settings "llm_provider": "openai", - "deep_think_llm": "gpt-5.2", - "quick_think_llm": "gpt-5-mini", + "deep_think_llm": "gpt-5.4", + "quick_think_llm": "gpt-5.4-mini", "backend_url": "https://api.openai.com/v1", # Provider-specific thinking configuration "google_thinking_level": None, # "high", "minimal", etc. diff --git a/tradingagents/llm_clients/model_catalog.py b/tradingagents/llm_clients/model_catalog.py index f147c5e1..91e1659c 100644 --- a/tradingagents/llm_clients/model_catalog.py +++ b/tradingagents/llm_clients/model_catalog.py @@ -11,15 +11,15 @@ ProviderModeOptions = Dict[str, Dict[str, List[ModelOption]]] MODEL_OPTIONS: ProviderModeOptions = { "openai": { "quick": [ - ("GPT-5 Mini - Balanced speed, cost, and capability", "gpt-5-mini"), - ("GPT-5 Nano - High-throughput, simple tasks", "gpt-5-nano"), + ("GPT-5.4 Mini - Fast, strong coding and tool use", "gpt-5.4-mini"), + ("GPT-5.4 Nano - Cheapest, high-volume tasks", "gpt-5.4-nano"), ("GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"), ("GPT-4.1 - Smartest non-reasoning model", "gpt-4.1"), ], "deep": [ ("GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"), ("GPT-5.2 - Strong reasoning, cost-effective", "gpt-5.2"), - ("GPT-5 Mini - Balanced speed, cost, and capability", "gpt-5-mini"), + ("GPT-5.4 Mini - Fast, strong coding and tool use", "gpt-5.4-mini"), ("GPT-5.4 Pro - Most capable, expensive ($30/$180 per 1M tokens)", "gpt-5.4-pro"), ], }, From 4641c03340c70e0e75e74234c998325164c72b36 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 29 Mar 2026 19:50:46 +0000 Subject: [PATCH 22/30] TradingAgents v0.2.3 --- README.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 41a124c7..4cfeb4e5 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ # TradingAgents: Multi-Agents LLM Financial Trading Framework ## News +- [2026-03] **TradingAgents v0.2.3** released with multi-language support, GPT-5.4 family models, unified model catalog, backtesting date fidelity, and proxy support. - [2026-03] **TradingAgents v0.2.2** released with GPT-5.4/Gemini 3.1/Claude 4.6 model coverage, five-tier rating scale, OpenAI Responses API, Anthropic effort control, and cross-platform stability. - [2026-02] **TradingAgents v0.2.0** released with multi-provider LLM support (GPT-5.x, Gemini 3.x, Claude 4.x, Grok 4.x) and improved system architecture. - [2026-01] **Trading-R1** [Technical Report](https://arxiv.org/abs/2509.11420) released, with [Terminal](https://github.com/TauricResearch/Trading-R1) expected to land soon. diff --git a/pyproject.toml b/pyproject.toml index de27a2b9..0decedb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tradingagents" -version = "0.2.2" +version = "0.2.3" description = "TradingAgents: Multi-Agents LLM Financial Trading Framework" readme = "README.md" requires-python = ">=3.10" From 7004dfe5540a5a82166927eaf11645ed9a6dc1e6 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sat, 4 Apr 2026 07:07:53 +0000 Subject: [PATCH 23/30] fix: remove hardcoded Google endpoint that caused 404 (#493, #496) --- cli/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cli/utils.py b/cli/utils.py index 62b50c9c..15c4a056 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -187,12 +187,11 @@ def select_deep_thinking_agent(provider) -> str: return choice -def select_llm_provider() -> tuple[str, str]: - """Select the OpenAI api url using interactive selection.""" - # Define OpenAI api options with their corresponding endpoints +def select_llm_provider() -> tuple[str, str | None]: + """Select the LLM provider and its API endpoint.""" BASE_URLS = [ ("OpenAI", "https://api.openai.com/v1"), - ("Google", "https://generativelanguage.googleapis.com/v1"), + ("Google", None), # google-genai SDK manages its own endpoint ("Anthropic", "https://api.anthropic.com/"), ("xAI", "https://api.x.ai/v1"), ("Openrouter", "https://openrouter.ai/api/v1"), From 28d5cc661fc706b4711d15d5257884bdc4600b01 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sat, 4 Apr 2026 07:14:10 +0000 Subject: [PATCH 24/30] fix: add missing pandas import in y_finance.py (#488) --- tradingagents/dataflows/y_finance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tradingagents/dataflows/y_finance.py b/tradingagents/dataflows/y_finance.py index 8b4b93f5..8f9bfe71 100644 --- a/tradingagents/dataflows/y_finance.py +++ b/tradingagents/dataflows/y_finance.py @@ -1,6 +1,7 @@ from typing import Annotated from datetime import datetime from dateutil.relativedelta import relativedelta +import pandas as pd import yfinance as yf import os from .stockstats_utils import StockstatsUtils, _clean_dataframe, yf_retry, load_ohlcv, filter_financials_by_date From 7269f877c1276f2e45c1e3455ed499f5e6746d6e Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sat, 4 Apr 2026 07:22:01 +0000 Subject: [PATCH 25/30] fix: portfolio manager reads trader's proposal and research plan (#503) --- tradingagents/agents/managers/portfolio_manager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tradingagents/agents/managers/portfolio_manager.py b/tradingagents/agents/managers/portfolio_manager.py index 970efb46..6c69ae9f 100644 --- a/tradingagents/agents/managers/portfolio_manager.py +++ b/tradingagents/agents/managers/portfolio_manager.py @@ -12,7 +12,8 @@ def create_portfolio_manager(llm, memory): news_report = state["news_report"] fundamentals_report = state["fundamentals_report"] sentiment_report = state["sentiment_report"] - trader_plan = state["investment_plan"] + research_plan = state["investment_plan"] + trader_plan = state["trader_investment_plan"] curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}" past_memories = memory.get_memories(curr_situation, n_matches=2) @@ -35,7 +36,8 @@ def create_portfolio_manager(llm, memory): - **Sell**: Exit position or avoid entry **Context:** -- Trader's proposed plan: **{trader_plan}** +- Research Manager's investment plan: **{research_plan}** +- Trader's transaction proposal: **{trader_plan}** - Lessons from past decisions: **{past_memory_str}** **Required Output Structure:** From 78fb66aed1a5664d163489e756f52750ecfc0ac2 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sat, 4 Apr 2026 07:23:31 +0000 Subject: [PATCH 26/30] fix: normalize indicator names to lowercase (#490) --- tradingagents/agents/utils/technical_indicators_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tradingagents/agents/utils/technical_indicators_tools.py b/tradingagents/agents/utils/technical_indicators_tools.py index dc982580..a3dda5a5 100644 --- a/tradingagents/agents/utils/technical_indicators_tools.py +++ b/tradingagents/agents/utils/technical_indicators_tools.py @@ -22,7 +22,7 @@ def get_indicators( """ # LLMs sometimes pass multiple indicators as a comma-separated string; # split and process each individually. - indicators = [i.strip() for i in indicator.split(",") if i.strip()] + indicators = [i.strip().lower() for i in indicator.split(",") if i.strip()] results = [] for ind in indicators: try: From bdc5fc62d32d048dc79bd124c5099fba84bb2971 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sat, 4 Apr 2026 07:28:03 +0000 Subject: [PATCH 27/30] chore: bump langchain-google-genai minimum to 4.0.0 for thought signature support --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0decedb0..98385e32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "backtrader>=1.9.78.123", "langchain-anthropic>=0.3.15", "langchain-experimental>=0.3.4", - "langchain-google-genai>=2.1.5", + "langchain-google-genai>=4.0.0", "langchain-openai>=0.3.23", "langgraph>=0.4.8", "pandas>=2.3.0", From bdb9c29d44a2f97eede350567cef654ff93031ce Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sat, 4 Apr 2026 07:35:35 +0000 Subject: [PATCH 28/30] refactor: remove stale imports, use configurable results path (#499) --- tradingagents/agents/analysts/fundamentals_analyst.py | 2 -- tradingagents/agents/analysts/market_analyst.py | 2 -- tradingagents/agents/analysts/news_analyst.py | 2 -- tradingagents/agents/analysts/social_media_analyst.py | 2 -- tradingagents/agents/managers/research_manager.py | 2 -- tradingagents/agents/researchers/bear_researcher.py | 3 --- tradingagents/agents/researchers/bull_researcher.py | 3 --- tradingagents/agents/risk_mgmt/aggressive_debator.py | 2 -- .../agents/risk_mgmt/conservative_debator.py | 3 --- tradingagents/agents/risk_mgmt/neutral_debator.py | 2 -- tradingagents/agents/trader/trader.py | 2 -- tradingagents/agents/utils/agent_states.py | 10 +++------- tradingagents/graph/reflection.py | 5 ++--- tradingagents/graph/setup.py | 9 ++++----- tradingagents/graph/signal_processing.py | 4 ++-- tradingagents/graph/trading_graph.py | 11 ++++------- 16 files changed, 15 insertions(+), 49 deletions(-) diff --git a/tradingagents/agents/analysts/fundamentals_analyst.py b/tradingagents/agents/analysts/fundamentals_analyst.py index 3f70c734..6aa49cf3 100644 --- a/tradingagents/agents/analysts/fundamentals_analyst.py +++ b/tradingagents/agents/analysts/fundamentals_analyst.py @@ -1,6 +1,4 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder -import time -import json from tradingagents.agents.utils.agent_utils import ( build_instrument_context, get_balance_sheet, diff --git a/tradingagents/agents/analysts/market_analyst.py b/tradingagents/agents/analysts/market_analyst.py index 680f9019..fef8f751 100644 --- a/tradingagents/agents/analysts/market_analyst.py +++ b/tradingagents/agents/analysts/market_analyst.py @@ -1,6 +1,4 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder -import time -import json from tradingagents.agents.utils.agent_utils import ( build_instrument_context, get_indicators, diff --git a/tradingagents/agents/analysts/news_analyst.py b/tradingagents/agents/analysts/news_analyst.py index 42fc7a61..e0fe93c5 100644 --- a/tradingagents/agents/analysts/news_analyst.py +++ b/tradingagents/agents/analysts/news_analyst.py @@ -1,6 +1,4 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder -import time -import json from tradingagents.agents.utils.agent_utils import ( build_instrument_context, get_global_news, diff --git a/tradingagents/agents/analysts/social_media_analyst.py b/tradingagents/agents/analysts/social_media_analyst.py index 67d78f4c..34a53c46 100644 --- a/tradingagents/agents/analysts/social_media_analyst.py +++ b/tradingagents/agents/analysts/social_media_analyst.py @@ -1,6 +1,4 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder -import time -import json from tradingagents.agents.utils.agent_utils import build_instrument_context, get_language_instruction, get_news from tradingagents.dataflows.config import get_config diff --git a/tradingagents/agents/managers/research_manager.py b/tradingagents/agents/managers/research_manager.py index 3ac4b150..5b4b4fdc 100644 --- a/tradingagents/agents/managers/research_manager.py +++ b/tradingagents/agents/managers/research_manager.py @@ -1,5 +1,3 @@ -import time -import json from tradingagents.agents.utils.agent_utils import build_instrument_context diff --git a/tradingagents/agents/researchers/bear_researcher.py b/tradingagents/agents/researchers/bear_researcher.py index 6634490a..a44212dc 100644 --- a/tradingagents/agents/researchers/bear_researcher.py +++ b/tradingagents/agents/researchers/bear_researcher.py @@ -1,6 +1,3 @@ -from langchain_core.messages import AIMessage -import time -import json def create_bear_researcher(llm, memory): diff --git a/tradingagents/agents/researchers/bull_researcher.py b/tradingagents/agents/researchers/bull_researcher.py index b03ef755..d23d4d76 100644 --- a/tradingagents/agents/researchers/bull_researcher.py +++ b/tradingagents/agents/researchers/bull_researcher.py @@ -1,6 +1,3 @@ -from langchain_core.messages import AIMessage -import time -import json def create_bull_researcher(llm, memory): diff --git a/tradingagents/agents/risk_mgmt/aggressive_debator.py b/tradingagents/agents/risk_mgmt/aggressive_debator.py index 651114a7..2dab1152 100644 --- a/tradingagents/agents/risk_mgmt/aggressive_debator.py +++ b/tradingagents/agents/risk_mgmt/aggressive_debator.py @@ -1,5 +1,3 @@ -import time -import json def create_aggressive_debator(llm): diff --git a/tradingagents/agents/risk_mgmt/conservative_debator.py b/tradingagents/agents/risk_mgmt/conservative_debator.py index 7c8c0fd1..99a8315e 100644 --- a/tradingagents/agents/risk_mgmt/conservative_debator.py +++ b/tradingagents/agents/risk_mgmt/conservative_debator.py @@ -1,6 +1,3 @@ -from langchain_core.messages import AIMessage -import time -import json def create_conservative_debator(llm): diff --git a/tradingagents/agents/risk_mgmt/neutral_debator.py b/tradingagents/agents/risk_mgmt/neutral_debator.py index 9ed490da..e99ff0af 100644 --- a/tradingagents/agents/risk_mgmt/neutral_debator.py +++ b/tradingagents/agents/risk_mgmt/neutral_debator.py @@ -1,5 +1,3 @@ -import time -import json def create_neutral_debator(llm): diff --git a/tradingagents/agents/trader/trader.py b/tradingagents/agents/trader/trader.py index 6298f239..07e9f262 100644 --- a/tradingagents/agents/trader/trader.py +++ b/tradingagents/agents/trader/trader.py @@ -1,6 +1,4 @@ import functools -import time -import json from tradingagents.agents.utils.agent_utils import build_instrument_context diff --git a/tradingagents/agents/utils/agent_states.py b/tradingagents/agents/utils/agent_states.py index 813b00ee..6423b936 100644 --- a/tradingagents/agents/utils/agent_states.py +++ b/tradingagents/agents/utils/agent_states.py @@ -1,10 +1,6 @@ -from typing import Annotated, Sequence -from datetime import date, timedelta, datetime -from typing_extensions import TypedDict, Optional -from langchain_openai import ChatOpenAI -from tradingagents.agents import * -from langgraph.prebuilt import ToolNode -from langgraph.graph import END, StateGraph, START, MessagesState +from typing import Annotated +from typing_extensions import TypedDict +from langgraph.graph import MessagesState # Researcher team state diff --git a/tradingagents/graph/reflection.py b/tradingagents/graph/reflection.py index 85438595..2a680038 100644 --- a/tradingagents/graph/reflection.py +++ b/tradingagents/graph/reflection.py @@ -1,13 +1,12 @@ # TradingAgents/graph/reflection.py -from typing import Dict, Any -from langchain_openai import ChatOpenAI +from typing import Any, Dict class Reflector: """Handles reflection on decisions and updating memory.""" - def __init__(self, quick_thinking_llm: ChatOpenAI): + def __init__(self, quick_thinking_llm: Any): """Initialize the reflector with an LLM.""" self.quick_thinking_llm = quick_thinking_llm self.reflection_system_prompt = self._get_reflection_prompt() diff --git a/tradingagents/graph/setup.py b/tradingagents/graph/setup.py index e0771c65..ae90489c 100644 --- a/tradingagents/graph/setup.py +++ b/tradingagents/graph/setup.py @@ -1,8 +1,7 @@ # TradingAgents/graph/setup.py -from typing import Dict, Any -from langchain_openai import ChatOpenAI -from langgraph.graph import END, StateGraph, START +from typing import Any, Dict +from langgraph.graph import END, START, StateGraph from langgraph.prebuilt import ToolNode from tradingagents.agents import * @@ -16,8 +15,8 @@ class GraphSetup: def __init__( self, - quick_thinking_llm: ChatOpenAI, - deep_thinking_llm: ChatOpenAI, + quick_thinking_llm: Any, + deep_thinking_llm: Any, tool_nodes: Dict[str, ToolNode], bull_memory, bear_memory, diff --git a/tradingagents/graph/signal_processing.py b/tradingagents/graph/signal_processing.py index f96c1efa..5ac66c1d 100644 --- a/tradingagents/graph/signal_processing.py +++ b/tradingagents/graph/signal_processing.py @@ -1,12 +1,12 @@ # TradingAgents/graph/signal_processing.py -from langchain_openai import ChatOpenAI +from typing import Any class SignalProcessor: """Processes trading signals to extract actionable decisions.""" - def __init__(self, quick_thinking_llm: ChatOpenAI): + def __init__(self, quick_thinking_llm: Any): """Initialize with an LLM for processing.""" self.quick_thinking_llm = quick_thinking_llm diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index c8cd7492..8e18f9c4 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -259,15 +259,12 @@ class TradingAgentsGraph: } # Save to file - directory = Path(f"eval_results/{self.ticker}/TradingAgentsStrategy_logs/") + directory = Path(self.config["results_dir"]) / self.ticker / "TradingAgentsStrategy_logs" directory.mkdir(parents=True, exist_ok=True) - with open( - f"eval_results/{self.ticker}/TradingAgentsStrategy_logs/full_states_log_{trade_date}.json", - "w", - encoding="utf-8", - ) as f: - json.dump(self.log_states_dict, f, indent=4) + log_path = directory / f"full_states_log_{trade_date}.json" + with open(log_path, "w", encoding="utf-8") as f: + json.dump(self.log_states_dict[str(trade_date)], f, indent=4) def reflect_and_remember(self, returns_losses): """Reflect on decisions and update memory based on returns.""" From 4f965bf46af0c1de294993334af3aaf7a5bc79bd Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sat, 4 Apr 2026 07:56:44 +0000 Subject: [PATCH 29/30] feat: dynamic OpenRouter model selection with search (#482, #337) --- cli/utils.py | 46 ++++++++++++++++++++++ tradingagents/llm_clients/model_catalog.py | 12 +----- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/cli/utils.py b/cli/utils.py index 15c4a056..e071ce06 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -134,9 +134,52 @@ def select_research_depth() -> int: return choice +def _fetch_openrouter_models() -> List[Tuple[str, str]]: + """Fetch available models from the OpenRouter API.""" + import requests + try: + resp = requests.get("https://openrouter.ai/api/v1/models", timeout=10) + resp.raise_for_status() + models = resp.json().get("data", []) + return [(m.get("name") or m["id"], m["id"]) for m in models] + except Exception as e: + console.print(f"\n[yellow]Could not fetch OpenRouter models: {e}[/yellow]") + return [] + + +def select_openrouter_model() -> str: + """Select an OpenRouter model from the newest available, or enter a custom ID.""" + models = _fetch_openrouter_models() + + choices = [questionary.Choice(name, value=mid) for name, mid in models[:5]] + choices.append(questionary.Choice("Custom model ID", value="custom")) + + choice = questionary.select( + "Select OpenRouter Model (latest available):", + choices=choices, + instruction="\n- Use arrow keys to navigate\n- Press Enter to select", + style=questionary.Style([ + ("selected", "fg:magenta noinherit"), + ("highlighted", "fg:magenta noinherit"), + ("pointer", "fg:magenta noinherit"), + ]), + ).ask() + + if choice is None or choice == "custom": + return questionary.text( + "Enter OpenRouter model ID (e.g. google/gemma-4-26b-a4b-it):", + validate=lambda x: len(x.strip()) > 0 or "Please enter a model ID.", + ).ask().strip() + + return choice + + def select_shallow_thinking_agent(provider) -> str: """Select shallow thinking llm engine using an interactive selection.""" + if provider.lower() == "openrouter": + return select_openrouter_model() + choice = questionary.select( "Select Your [Quick-Thinking LLM Engine]:", choices=[ @@ -165,6 +208,9 @@ def select_shallow_thinking_agent(provider) -> str: def select_deep_thinking_agent(provider) -> str: """Select deep thinking llm engine using an interactive selection.""" + if provider.lower() == "openrouter": + return select_openrouter_model() + choice = questionary.select( "Select Your [Deep-Thinking LLM Engine]:", choices=[ diff --git a/tradingagents/llm_clients/model_catalog.py b/tradingagents/llm_clients/model_catalog.py index 91e1659c..fd91c66d 100644 --- a/tradingagents/llm_clients/model_catalog.py +++ b/tradingagents/llm_clients/model_catalog.py @@ -63,16 +63,8 @@ MODEL_OPTIONS: ProviderModeOptions = { ("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"), ], }, - "openrouter": { - "quick": [ - ("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"), - ("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"), - ], - "deep": [ - ("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"), - ("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"), - ], - }, + # OpenRouter models are fetched dynamically at CLI runtime. + # No static entries needed; any model ID is accepted by the validator. "ollama": { "quick": [ ("Qwen3:latest (8B, local)", "qwen3:latest"), From 10c136f49c82e11f0e324c9c50cda1638a8ed5a7 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sat, 4 Apr 2026 08:14:01 +0000 Subject: [PATCH 30/30] feat: add Docker support for cross-platform deployment --- .dockerignore | 15 +++++++++++++++ Dockerfile | 27 +++++++++++++++++++++++++++ README.md | 13 +++++++++++++ docker-compose.yml | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..cac71018 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +.venv +.env +.claude +.idea +.vscode +.DS_Store +__pycache__ +*.egg-info +build +dist +results +eval_results +Dockerfile +docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..940609d3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.12-slim AS builder + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +WORKDIR /build +COPY . . +RUN pip install --no-cache-dir . + +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +COPY --from=builder /opt/venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +RUN useradd --create-home appuser +USER appuser +WORKDIR /home/appuser/app + +COPY --from=builder --chown=appuser:appuser /build . + +ENTRYPOINT ["tradingagents"] diff --git a/README.md b/README.md index 4cfeb4e5..9a92bff9 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,19 @@ Install the package and its dependencies: pip install . ``` +### Docker + +Alternatively, run with Docker: +```bash +cp .env.example .env # add your API keys +docker compose run --rm tradingagents +``` + +For local models with Ollama: +```bash +docker compose --profile ollama run --rm tradingagents-ollama +``` + ### Required APIs TradingAgents supports multiple LLM providers. Set the API key for your chosen provider: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..3a5d4e29 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + tradingagents: + build: . + env_file: + - .env + volumes: + - ./results:/home/appuser/app/results + tty: true + stdin_open: true + + ollama: + image: ollama/ollama:latest + volumes: + - ollama_data:/root/.ollama + profiles: + - ollama + + tradingagents-ollama: + build: . + env_file: + - .env + environment: + - LLM_PROVIDER=ollama + volumes: + - ./results:/home/appuser/app/results + depends_on: + - ollama + tty: true + stdin_open: true + profiles: + - ollama + +volumes: + ollama_data: