Merge pull request #97 from aguzererler/copilot/optimize-llm-round-trips
Parallel pre-fetch for analyst agents to reduce LLM round-trips
This commit is contained in:
commit
383b414698
|
|
@ -1,4 +1,4 @@
|
|||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
||||
from langchain_core.runnables import Runnable
|
||||
|
|
@ -7,6 +7,7 @@ from tradingagents.agents.analysts.market_analyst import create_market_analyst
|
|||
from tradingagents.agents.analysts.social_media_analyst import create_social_media_analyst
|
||||
from tradingagents.agents.analysts.news_analyst import create_news_analyst
|
||||
|
||||
|
||||
class MockRunnable(Runnable):
|
||||
def __init__(self, invoke_responses):
|
||||
self.invoke_responses = invoke_responses
|
||||
|
|
@ -17,6 +18,7 @@ class MockRunnable(Runnable):
|
|||
self.call_count += 1
|
||||
return response
|
||||
|
||||
|
||||
class MockLLM(Runnable):
|
||||
def __init__(self, invoke_responses):
|
||||
self.runnable = MockRunnable(invoke_responses)
|
||||
|
|
@ -29,6 +31,7 @@ class MockLLM(Runnable):
|
|||
self.tools_bound = tools
|
||||
return self.runnable
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_state():
|
||||
return {
|
||||
|
|
@ -37,8 +40,10 @@ def mock_state():
|
|||
"company_of_interest": "AAPL",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_llm_with_tool_call():
|
||||
"""LLM that makes one tool call then writes the final report (iterative loop)."""
|
||||
# 1. First call: The LLM decides to use a tool
|
||||
tool_call_msg = AIMessage(
|
||||
content="",
|
||||
|
|
@ -52,22 +57,108 @@ def mock_llm_with_tool_call():
|
|||
)
|
||||
return MockLLM([tool_call_msg, final_report_msg])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_llm_direct_report():
|
||||
"""LLM that returns the final report directly (no tool calls — full pre-fetch path)."""
|
||||
final_report_msg = AIMessage(
|
||||
content="This is the final report after running the tool."
|
||||
)
|
||||
return MockLLM([final_report_msg])
|
||||
|
||||
|
||||
def test_fundamentals_analyst_tool_loop(mock_state, mock_llm_with_tool_call):
|
||||
"""Fundamentals analyst: pre-fetches 4 tools, runs iterative loop for raw statements."""
|
||||
node = create_fundamentals_analyst(mock_llm_with_tool_call)
|
||||
result = node(mock_state)
|
||||
assert "This is the final report after running the tool." in result["fundamentals_report"]
|
||||
|
||||
|
||||
def test_market_analyst_tool_loop(mock_state, mock_llm_with_tool_call):
|
||||
"""Market analyst: pre-fetches macro + stock data, keeps indicator selection iterative."""
|
||||
node = create_market_analyst(mock_llm_with_tool_call)
|
||||
result = node(mock_state)
|
||||
assert "This is the final report after running the tool." in result["market_report"]
|
||||
|
||||
def test_social_media_analyst_tool_loop(mock_state, mock_llm_with_tool_call):
|
||||
node = create_social_media_analyst(mock_llm_with_tool_call)
|
||||
|
||||
def test_social_media_analyst_direct_invoke(mock_state, mock_llm_direct_report):
|
||||
"""Social analyst: full pre-fetch, direct LLM invoke (no tool loop)."""
|
||||
node = create_social_media_analyst(mock_llm_direct_report)
|
||||
result = node(mock_state)
|
||||
assert "This is the final report after running the tool." in result["sentiment_report"]
|
||||
|
||||
def test_news_analyst_tool_loop(mock_state, mock_llm_with_tool_call):
|
||||
node = create_news_analyst(mock_llm_with_tool_call)
|
||||
|
||||
def test_news_analyst_direct_invoke(mock_state, mock_llm_direct_report):
|
||||
"""News analyst: full pre-fetch, direct LLM invoke (no tool loop)."""
|
||||
node = create_news_analyst(mock_llm_direct_report)
|
||||
result = node(mock_state)
|
||||
assert "This is the final report after running the tool." in result["news_report"]
|
||||
|
||||
|
||||
def test_market_analyst_macro_regime_from_prefetch(mock_state, mock_llm_with_tool_call):
|
||||
"""Market analyst populates macro_regime_report from pre-fetched data when available."""
|
||||
with patch(
|
||||
"tradingagents.agents.analysts.market_analyst.prefetch_tools_parallel",
|
||||
return_value={
|
||||
"Macro Regime Classification": "## Risk-On\nMarket is RISK-ON.",
|
||||
"Stock Price Data": "Date,Close\n2024-05-14,189.0",
|
||||
},
|
||||
):
|
||||
node = create_market_analyst(mock_llm_with_tool_call)
|
||||
result = node(mock_state)
|
||||
assert result["macro_regime_report"] == "## Risk-On\nMarket is RISK-ON."
|
||||
|
||||
|
||||
def test_social_media_analyst_no_bind_tools(mock_state, mock_llm_direct_report):
|
||||
"""Social analyst must not call bind_tools since there are no tools."""
|
||||
node = create_social_media_analyst(mock_llm_direct_report)
|
||||
node(mock_state)
|
||||
# bind_tools should never have been called (no tools in the list)
|
||||
assert mock_llm_direct_report.tools_bound is None
|
||||
|
||||
|
||||
def test_prefetched_context_injected_into_prompt(mock_state, mock_llm_with_tool_call):
|
||||
"""Market analyst injects pre-fetched context into the prompt sent to the LLM."""
|
||||
captured_inputs = []
|
||||
|
||||
class CapturingRunnable(Runnable):
|
||||
def invoke(self, input, config=None, **kwargs):
|
||||
captured_inputs.append(input)
|
||||
# Return final report directly to end the loop early
|
||||
return AIMessage(content="This is the final report after running the tool.")
|
||||
|
||||
class CapturingLLM(Runnable):
|
||||
def invoke(self, input, config=None, **kwargs):
|
||||
captured_inputs.append(input)
|
||||
return AIMessage(content="This is the final report after running the tool.")
|
||||
|
||||
def bind_tools(self, tools):
|
||||
return CapturingRunnable()
|
||||
|
||||
with patch(
|
||||
"tradingagents.agents.analysts.market_analyst.prefetch_tools_parallel",
|
||||
return_value={
|
||||
"Macro Regime Classification": "**RISK-ON** regime detected.",
|
||||
"Stock Price Data": "Date,Close\n2024-05-14,189.0",
|
||||
},
|
||||
):
|
||||
node = create_market_analyst(CapturingLLM())
|
||||
node(mock_state)
|
||||
|
||||
# The prompt was captured; find the system message and verify injected context
|
||||
assert captured_inputs, "LLM was never called"
|
||||
# The input to the runnable is a list of messages; find the system message text
|
||||
messages = captured_inputs[0]
|
||||
full_text = " ".join(
|
||||
m.content if hasattr(m, "content") else str(m)
|
||||
for m in messages
|
||||
)
|
||||
assert "RISK-ON" in full_text
|
||||
assert "Pre-loaded Context" in full_text
|
||||
|
||||
|
||||
def test_news_analyst_no_bind_tools(mock_state, mock_llm_direct_report):
|
||||
"""News analyst must not call bind_tools since there are no tools."""
|
||||
node = create_news_analyst(mock_llm_direct_report)
|
||||
node(mock_state)
|
||||
assert mock_llm_direct_report.tools_bound is None
|
||||
|
|
|
|||
|
|
@ -1,47 +1,100 @@
|
|||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
import time
|
||||
import json
|
||||
|
||||
from tradingagents.agents.utils.agent_utils import (
|
||||
build_instrument_context,
|
||||
format_prefetched_context,
|
||||
prefetch_tools_parallel,
|
||||
)
|
||||
from tradingagents.agents.utils.fundamental_data_tools import (
|
||||
get_fundamentals,
|
||||
get_balance_sheet,
|
||||
get_cashflow,
|
||||
get_fundamentals,
|
||||
get_income_statement,
|
||||
get_ttm_analysis,
|
||||
get_peer_comparison,
|
||||
get_sector_relative,
|
||||
get_ttm_analysis,
|
||||
)
|
||||
from tradingagents.agents.utils.news_data_tools import get_insider_transactions
|
||||
from tradingagents.agents.utils.tool_runner import run_tool_loop
|
||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
||||
from tradingagents.dataflows.config import get_config
|
||||
|
||||
|
||||
def create_fundamentals_analyst(llm):
|
||||
def fundamentals_analyst_node(state):
|
||||
current_date = state["trade_date"]
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
ticker = state["company_of_interest"]
|
||||
instrument_context = build_instrument_context(ticker)
|
||||
|
||||
tools = [
|
||||
get_ttm_analysis,
|
||||
get_fundamentals,
|
||||
get_balance_sheet,
|
||||
get_cashflow,
|
||||
get_income_statement,
|
||||
get_peer_comparison,
|
||||
get_sector_relative,
|
||||
]
|
||||
# ── Pre-fetch the four mandatory foundational datasets in parallel ────
|
||||
# get_ttm_analysis, get_fundamentals, get_peer_comparison, and
|
||||
# get_sector_relative are always called — pre-fetching them removes
|
||||
# 4 LLM round-trips. The raw financial statements (balance sheet,
|
||||
# cashflow, income statement) stay iterative: the LLM may request them
|
||||
# only if it spots anomalies worth investigating in the pre-loaded data.
|
||||
prefetched = prefetch_tools_parallel(
|
||||
[
|
||||
{
|
||||
"tool": get_ttm_analysis,
|
||||
"args": {"ticker": ticker, "curr_date": current_date},
|
||||
"label": "TTM Analysis (8-Quarter Trend)",
|
||||
},
|
||||
{
|
||||
"tool": get_fundamentals,
|
||||
"args": {"ticker": ticker, "curr_date": current_date},
|
||||
"label": "Fundamental Ratios Snapshot",
|
||||
},
|
||||
{
|
||||
"tool": get_peer_comparison,
|
||||
"args": {"ticker": ticker, "curr_date": current_date},
|
||||
"label": "Peer Comparison",
|
||||
},
|
||||
{
|
||||
"tool": get_sector_relative,
|
||||
"args": {"ticker": ticker, "curr_date": current_date},
|
||||
"label": "Sector Relative Performance",
|
||||
},
|
||||
]
|
||||
)
|
||||
prefetched_context = format_prefetched_context(prefetched)
|
||||
|
||||
# ── Only the raw statement tools remain iterative ─────────────────────
|
||||
tools = [get_balance_sheet, get_cashflow, get_income_statement]
|
||||
|
||||
system_message = (
|
||||
"You are a researcher tasked with performing deep fundamental analysis of a company over the last 8 quarters (2 years) to support medium-term investment decisions."
|
||||
" Follow this sequence:"
|
||||
" 1. Call `get_ttm_analysis` first — this provides a Trailing Twelve Months (TTM) trend report covering revenue growth (QoQ and YoY), margin trajectories (gross, operating, net), return on equity trend, debt/equity trend, and free cash flow over 8 quarters."
|
||||
" 2. Call `get_fundamentals` for the latest snapshot of key ratios (PE, PEG, price-to-book, beta, 52-week range)."
|
||||
" 3. Call `get_peer_comparison` to see how the company ranks against sector peers over 1-week, 1-month, 3-month, and 6-month periods."
|
||||
" 4. Call `get_sector_relative` to compute the company's alpha vs its sector ETF benchmark."
|
||||
" 5. Optionally call `get_balance_sheet`, `get_cashflow`, or `get_income_statement` for additional detail."
|
||||
" Write a comprehensive report covering: multi-quarter revenue and margin trends, TTM metrics, relative valuation vs peers, sector outperformance or underperformance, and a clear medium-term fundamental thesis."
|
||||
" Do not simply state trends are mixed — provide detailed, fine-grained analysis that identifies inflection points, acceleration or deceleration in growth, and specific risks and opportunities."
|
||||
" Make sure to append a Markdown summary table at the end of the report organising key metrics for easy reference.",
|
||||
"You are a researcher tasked with performing deep fundamental analysis of a company "
|
||||
"over the last 8 quarters (2 years) to support medium-term investment decisions.\n\n"
|
||||
"## Pre-loaded Foundational Data\n\n"
|
||||
"The following datasets have already been fetched and are provided in the "
|
||||
"**Pre-loaded Context** section below. Do NOT call `get_ttm_analysis`, "
|
||||
"`get_fundamentals`, `get_peer_comparison`, or `get_sector_relative` — "
|
||||
"that data is already available:\n\n"
|
||||
"- **TTM Analysis**: 8-quarter Trailing Twelve Months trends — revenue growth "
|
||||
"(QoQ and YoY), margin trajectories (gross, operating, net), ROE trend, "
|
||||
"debt/equity trend, and free cash flow.\n"
|
||||
"- **Fundamental Ratios**: Latest snapshot of key ratios (PE, PEG, price-to-book, "
|
||||
"beta, 52-week range).\n"
|
||||
"- **Peer Comparison**: How the company ranks against sector peers over 1-week, "
|
||||
"1-month, 3-month, and 6-month periods.\n"
|
||||
"- **Sector Relative Performance**: The company's alpha vs its sector ETF benchmark.\n\n"
|
||||
"## Your Task\n\n"
|
||||
"Interpret the pre-loaded data analytically. Look for:\n"
|
||||
"- Revenue and margin inflection points — acceleration, deceleration, or trend reversals\n"
|
||||
"- Suspicious deviations in FCF vs reported net income (earnings quality signals)\n"
|
||||
"- Peer divergence — is the company outperforming or underperforming its sector?\n"
|
||||
"- Valuation anomalies vs growth trajectory (PEG vs actual growth rate)\n\n"
|
||||
"If you identify anything suspicious in the TTM or fundamentals data that warrants "
|
||||
"deeper investigation — for example, a margin inflection without an obvious revenue "
|
||||
"driver, an FCF deviation from net income, or an unusual balance-sheet move — you "
|
||||
"may call `get_balance_sheet`, `get_cashflow`, or `get_income_statement` to examine "
|
||||
"the raw quarterly data directly.\n\n"
|
||||
"Write a comprehensive report covering: multi-quarter revenue and margin trends, "
|
||||
"TTM metrics, relative valuation vs peers, sector outperformance or underperformance, "
|
||||
"and a clear medium-term fundamental thesis. "
|
||||
"Do not simply state trends are mixed — provide detailed, fine-grained analysis that "
|
||||
"identifies inflection points, acceleration or deceleration in growth, and specific "
|
||||
"risks and opportunities. "
|
||||
"Make sure to append a Markdown summary table at the end of the report organising "
|
||||
"key metrics for easy reference."
|
||||
)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
|
|
@ -55,7 +108,8 @@ 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}. {instrument_context}",
|
||||
"For your reference, the current date is {current_date}. {instrument_context}\n\n"
|
||||
"## Pre-loaded Context\n\n{prefetched_context}",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
|
|
@ -65,6 +119,7 @@ def create_fundamentals_analyst(llm):
|
|||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
||||
prompt = prompt.partial(current_date=current_date)
|
||||
prompt = prompt.partial(instrument_context=instrument_context)
|
||||
prompt = prompt.partial(prefetched_context=prefetched_context)
|
||||
|
||||
chain = prompt | llm.bind_tools(tools)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
import time
|
||||
|
||||
from tradingagents.agents.utils.agent_utils import (
|
||||
build_instrument_context,
|
||||
format_prefetched_context,
|
||||
prefetch_tools_parallel,
|
||||
)
|
||||
from tradingagents.agents.utils.core_stock_tools import get_stock_data
|
||||
from tradingagents.agents.utils.technical_indicators_tools import get_indicators
|
||||
from tradingagents.agents.utils.fundamental_data_tools import get_macro_regime
|
||||
from tradingagents.agents.utils.technical_indicators_tools import get_indicators
|
||||
from tradingagents.agents.utils.tool_runner import run_tool_loop
|
||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
||||
from tradingagents.dataflows.config import get_config
|
||||
|
||||
|
||||
|
|
@ -12,43 +18,107 @@ def create_market_analyst(llm):
|
|||
|
||||
def market_analyst_node(state):
|
||||
current_date = state["trade_date"]
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
ticker = state["company_of_interest"]
|
||||
instrument_context = build_instrument_context(ticker)
|
||||
|
||||
tools = [
|
||||
get_macro_regime,
|
||||
get_stock_data,
|
||||
get_indicators,
|
||||
]
|
||||
# ── Pre-fetch macro regime and stock price data in parallel ──────────
|
||||
# Both are always required; fetching them upfront removes 2 LLM round-
|
||||
# trips and lets the LLM focus its single tool-call budget on choosing
|
||||
# the right indicators based on the macro regime it sees.
|
||||
trade_date = datetime.strptime(current_date, "%Y-%m-%d")
|
||||
stock_start = (trade_date - timedelta(days=365)).strftime("%Y-%m-%d")
|
||||
|
||||
prefetched = prefetch_tools_parallel(
|
||||
[
|
||||
{
|
||||
"tool": get_macro_regime,
|
||||
"args": {"curr_date": current_date},
|
||||
"label": "Macro Regime Classification",
|
||||
},
|
||||
{
|
||||
"tool": get_stock_data,
|
||||
"args": {
|
||||
"symbol": ticker,
|
||||
"start_date": stock_start,
|
||||
"end_date": current_date,
|
||||
},
|
||||
"label": "Stock Price Data",
|
||||
},
|
||||
]
|
||||
)
|
||||
prefetched_context = format_prefetched_context(prefetched)
|
||||
|
||||
# ── Only get_indicators remains iterative ─────────────────────────────
|
||||
# The LLM reads the macro regime from the pre-loaded context and decides
|
||||
# which indicators are most relevant before calling get_indicators.
|
||||
tools = [get_indicators]
|
||||
|
||||
system_message = (
|
||||
"""You are a trading assistant tasked with analyzing financial markets. Start by calling `get_macro_regime` to classify the current macro environment as risk-on, risk-off, or transition. Use this macro context to frame all subsequent technical analysis — for example, in risk-off environments weight bearish signals more heavily, and in risk-on environments favour momentum and breakout signals.
|
||||
|
||||
Then, select the **most relevant indicators** for the given market condition from the following list. The goal is to choose up to **8 indicators** that provide complementary insights without redundancy. Categories and each category's indicators are:
|
||||
|
||||
Moving Averages:
|
||||
- close_50_sma: 50 SMA: A medium-term trend indicator. Usage: Identify trend direction and serve as dynamic support/resistance. Tips: It lags price; combine with faster indicators for timely signals.
|
||||
- close_200_sma: 200 SMA: A long-term trend benchmark. Usage: Confirm overall market trend and identify golden/death cross setups. Tips: It reacts slowly; best for strategic trend confirmation rather than frequent trading entries.
|
||||
- close_10_ema: 10 EMA: A responsive short-term average. Usage: Capture quick shifts in momentum and potential entry points. Tips: Prone to noise in choppy markets; use alongside longer averages for filtering false signals.
|
||||
|
||||
MACD Related:
|
||||
- macd: MACD: Computes momentum via differences of EMAs. Usage: Look for crossovers and divergence as signals of trend changes. Tips: Confirm with other indicators in low-volatility or sideways markets.
|
||||
- macds: MACD Signal: An EMA smoothing of the MACD line. Usage: Use crossovers with the MACD line to trigger trades. Tips: Should be part of a broader strategy to avoid false positives.
|
||||
- macdh: MACD Histogram: Shows the gap between the MACD line and its signal. Usage: Visualize momentum strength and spot divergence early. Tips: Can be volatile; complement with additional filters in fast-moving markets.
|
||||
|
||||
Momentum Indicators:
|
||||
- rsi: RSI: Measures momentum to flag overbought/oversold conditions. Usage: Apply 70/30 thresholds and watch for divergence to signal reversals. Tips: In strong trends, RSI may remain extreme; always cross-check with trend analysis.
|
||||
|
||||
Volatility Indicators:
|
||||
- boll: Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. Usage: Acts as a dynamic benchmark for price movement. Tips: Combine with the upper and lower bands to effectively spot breakouts or reversals.
|
||||
- boll_ub: Bollinger Upper Band: Typically 2 standard deviations above the middle line. Usage: Signals potential overbought conditions and breakout zones. Tips: Confirm signals with other tools; prices may ride the band in strong trends.
|
||||
- boll_lb: Bollinger Lower Band: Typically 2 standard deviations below the middle line. Usage: Indicates potential oversold conditions. Tips: Use additional analysis to avoid false reversal signals.
|
||||
- atr: ATR: Averages true range to measure volatility. Usage: Set stop-loss levels and adjust position sizes based on current market volatility. Tips: It's a reactive measure, so use it as part of a broader risk management strategy.
|
||||
|
||||
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. 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."""
|
||||
"You are a trading assistant tasked with analyzing financial markets.\n\n"
|
||||
"## Pre-loaded Data\n\n"
|
||||
"The macro regime classification and recent stock price data for the company under "
|
||||
"analysis have already been fetched and are provided in the **Pre-loaded Context** "
|
||||
"section below. "
|
||||
"Do NOT call `get_macro_regime` or `get_stock_data` — the data is already available.\n\n"
|
||||
"## Your Task\n\n"
|
||||
"1. Read the macro regime classification from the pre-loaded context. "
|
||||
"The macro regime has been classified above — use it to weight your indicator "
|
||||
"choices before calling `get_indicators`. For example, in risk-off environments "
|
||||
"favour ATR, Bollinger Bands, and long-term SMAs; in risk-on environments favour "
|
||||
"momentum indicators like MACD and short EMAs.\n\n"
|
||||
"2. Select the **most relevant indicators** for the given market condition from "
|
||||
"the list below. Choose up to **8 indicators** that provide complementary insights "
|
||||
"without redundancy.\n\n"
|
||||
"Moving Averages:\n"
|
||||
"- close_50_sma: 50 SMA: A medium-term trend indicator. Usage: Identify trend "
|
||||
"direction and serve as dynamic support/resistance. Tips: It lags price; combine "
|
||||
"with faster indicators for timely signals.\n"
|
||||
"- close_200_sma: 200 SMA: A long-term trend benchmark. Usage: Confirm overall "
|
||||
"market trend and identify golden/death cross setups. Tips: It reacts slowly; best "
|
||||
"for strategic trend confirmation rather than frequent trading entries.\n"
|
||||
"- close_10_ema: 10 EMA: A responsive short-term average. Usage: Capture quick "
|
||||
"shifts in momentum and potential entry points. Tips: Prone to noise in choppy "
|
||||
"markets; use alongside longer averages for filtering false signals.\n\n"
|
||||
"MACD Related:\n"
|
||||
"- macd: MACD: Computes momentum via differences of EMAs. Usage: Look for "
|
||||
"crossovers and divergence as signals of trend changes. Tips: Confirm with other "
|
||||
"indicators in low-volatility or sideways markets.\n"
|
||||
"- macds: MACD Signal: An EMA smoothing of the MACD line. Usage: Use crossovers "
|
||||
"with the MACD line to trigger trades. Tips: Should be part of a broader strategy "
|
||||
"to avoid false positives.\n"
|
||||
"- macdh: MACD Histogram: Shows the gap between the MACD line and its signal. "
|
||||
"Usage: Visualize momentum strength and spot divergence early. Tips: Can be "
|
||||
"volatile; complement with additional filters in fast-moving markets.\n\n"
|
||||
"Momentum Indicators:\n"
|
||||
"- rsi: RSI: Measures momentum to flag overbought/oversold conditions. Usage: "
|
||||
"Apply 70/30 thresholds and watch for divergence to signal reversals. Tips: In "
|
||||
"strong trends, RSI may remain extreme; always cross-check with trend analysis.\n\n"
|
||||
"Volatility Indicators:\n"
|
||||
"- boll: Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. "
|
||||
"Usage: Acts as a dynamic benchmark for price movement. Tips: Combine with the "
|
||||
"upper and lower bands to effectively spot breakouts or reversals.\n"
|
||||
"- boll_ub: Bollinger Upper Band: Typically 2 standard deviations above the middle "
|
||||
"line. Usage: Signals potential overbought conditions and breakout zones. Tips: "
|
||||
"Confirm signals with other tools; prices may ride the band in strong trends.\n"
|
||||
"- boll_lb: Bollinger Lower Band: Typically 2 standard deviations below the middle "
|
||||
"line. Usage: Indicates potential oversold conditions. Tips: Use additional "
|
||||
"analysis to avoid false reversal signals.\n"
|
||||
"- atr: ATR: Averages true range to measure volatility. Usage: Set stop-loss "
|
||||
"levels and adjust position sizes based on current market volatility. Tips: It's "
|
||||
"a reactive measure, so use it as part of a broader risk management strategy.\n\n"
|
||||
"Volume-Based Indicators:\n"
|
||||
"- 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.\n\n"
|
||||
"3. Select indicators that provide diverse and complementary information. Avoid "
|
||||
"redundancy (e.g., do not select both rsi and stochrsi). Briefly explain why each "
|
||||
"chosen indicator is suitable for the current macro context. When calling "
|
||||
"`get_indicators`, use the exact indicator names listed above — they are defined "
|
||||
"parameters and any deviation will cause the call to fail.\n\n"
|
||||
"4. 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 organise key points, making it easy to read."
|
||||
)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
|
|
@ -62,7 +132,8 @@ 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}. {instrument_context}",
|
||||
"For your reference, the current date is {current_date}. {instrument_context}\n\n"
|
||||
"## Pre-loaded Context\n\n{prefetched_context}",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
|
|
@ -72,6 +143,7 @@ Volume-Based Indicators:
|
|||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
||||
prompt = prompt.partial(current_date=current_date)
|
||||
prompt = prompt.partial(instrument_context=instrument_context)
|
||||
prompt = prompt.partial(prefetched_context=prefetched_context)
|
||||
|
||||
chain = prompt | llm.bind_tools(tools)
|
||||
|
||||
|
|
@ -80,8 +152,16 @@ Volume-Based Indicators:
|
|||
report = result.content or ""
|
||||
macro_regime_report = ""
|
||||
|
||||
# Extract macro regime section if present
|
||||
if report and ("Macro Regime Classification" in report or "RISK-ON" in report.upper() or "RISK-OFF" in report.upper() or "TRANSITION" in report.upper()):
|
||||
# Extract macro regime section if present (from pre-loaded context or report)
|
||||
regime_data = prefetched.get("Macro Regime Classification", "")
|
||||
if regime_data and not regime_data.startswith("[Error"):
|
||||
macro_regime_report = regime_data
|
||||
elif report and (
|
||||
"Macro Regime Classification" in report
|
||||
or "RISK-ON" in report.upper()
|
||||
or "RISK-OFF" in report.upper()
|
||||
or "TRANSITION" in report.upper()
|
||||
):
|
||||
macro_regime_report = report
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,65 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
import json
|
||||
from tradingagents.agents.utils.news_data_tools import get_news, get_global_news
|
||||
from tradingagents.agents.utils.tool_runner import run_tool_loop
|
||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
||||
|
||||
from tradingagents.agents.utils.agent_utils import (
|
||||
build_instrument_context,
|
||||
format_prefetched_context,
|
||||
prefetch_tools_parallel,
|
||||
)
|
||||
from tradingagents.agents.utils.news_data_tools import get_global_news, get_news
|
||||
from tradingagents.dataflows.config import get_config
|
||||
|
||||
|
||||
def create_news_analyst(llm):
|
||||
def news_analyst_node(state):
|
||||
current_date = state["trade_date"]
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
ticker = state["company_of_interest"]
|
||||
instrument_context = build_instrument_context(ticker)
|
||||
|
||||
tools = [
|
||||
get_news,
|
||||
get_global_news,
|
||||
]
|
||||
# ── Pre-fetch company-specific and global news in parallel ────────────
|
||||
trade_date = datetime.strptime(current_date, "%Y-%m-%d")
|
||||
start_date = (trade_date - timedelta(days=7)).strftime("%Y-%m-%d")
|
||||
|
||||
prefetched = prefetch_tools_parallel(
|
||||
[
|
||||
{
|
||||
"tool": get_news,
|
||||
"args": {
|
||||
"ticker": ticker,
|
||||
"start_date": start_date,
|
||||
"end_date": current_date,
|
||||
},
|
||||
"label": "Company-Specific News (Last 7 Days)",
|
||||
},
|
||||
{
|
||||
"tool": get_global_news,
|
||||
"args": {
|
||||
"curr_date": current_date,
|
||||
"look_back_days": 7,
|
||||
"limit": 5,
|
||||
},
|
||||
"label": "Global Macroeconomic News (Last 7 Days)",
|
||||
},
|
||||
]
|
||||
)
|
||||
prefetched_context = format_prefetched_context(prefetched)
|
||||
|
||||
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."""
|
||||
"You are a news researcher tasked with analyzing recent news and trends over "
|
||||
"the past week.\n\n"
|
||||
"## Pre-loaded Data\n\n"
|
||||
"Both company-specific news and global macroeconomic news for the past 7 days "
|
||||
"have already been fetched and are provided in the **Pre-loaded Context** section "
|
||||
"below. Do NOT call `get_news` or `get_global_news` — the data is already available.\n\n"
|
||||
"## Your Task\n\n"
|
||||
"Synthesize the pre-loaded news feeds into a comprehensive report covering the "
|
||||
"current state of the world as it is relevant to trading and macroeconomics. "
|
||||
"Cross-reference company-specific developments with the broader macro backdrop. "
|
||||
"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 organise key "
|
||||
"points, making it easy to read."
|
||||
)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
|
|
@ -26,25 +67,27 @@ def create_news_analyst(llm):
|
|||
(
|
||||
"system",
|
||||
"You are a helpful AI assistant, collaborating with other assistants."
|
||||
" Use the provided tools to progress towards answering the question."
|
||||
" If you are unable to fully answer, that's OK; another assistant with different tools"
|
||||
" will help where you left off. Execute what you can to make progress."
|
||||
" 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}. {instrument_context}",
|
||||
"\n{system_message}"
|
||||
"For your reference, the current date is {current_date}. {instrument_context}\n\n"
|
||||
"## Pre-loaded Context\n\n{prefetched_context}",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
)
|
||||
|
||||
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(instrument_context=instrument_context)
|
||||
prompt = prompt.partial(prefetched_context=prefetched_context)
|
||||
|
||||
chain = prompt | llm.bind_tools(tools)
|
||||
result = run_tool_loop(chain, state["messages"], tools)
|
||||
# No tools remain — use direct invocation (no bind_tools, no tool loop)
|
||||
chain = prompt | llm
|
||||
|
||||
result = chain.invoke(state["messages"])
|
||||
|
||||
report = result.content or ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,59 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
import time
|
||||
import json
|
||||
|
||||
from tradingagents.agents.utils.agent_utils import (
|
||||
build_instrument_context,
|
||||
format_prefetched_context,
|
||||
prefetch_tools_parallel,
|
||||
)
|
||||
from tradingagents.agents.utils.news_data_tools import get_news
|
||||
from tradingagents.agents.utils.tool_runner import run_tool_loop
|
||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
||||
from tradingagents.dataflows.config import get_config
|
||||
|
||||
|
||||
def create_social_media_analyst(llm):
|
||||
def social_media_analyst_node(state):
|
||||
current_date = state["trade_date"]
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
ticker = state["company_of_interest"]
|
||||
instrument_context = build_instrument_context(ticker)
|
||||
|
||||
tools = [
|
||||
get_news,
|
||||
]
|
||||
# ── Pre-fetch company news for the past 7 days ────────────────────────
|
||||
trade_date = datetime.strptime(current_date, "%Y-%m-%d")
|
||||
start_date = (trade_date - timedelta(days=7)).strftime("%Y-%m-%d")
|
||||
|
||||
prefetched = prefetch_tools_parallel(
|
||||
[
|
||||
{
|
||||
"tool": get_news,
|
||||
"args": {
|
||||
"ticker": ticker,
|
||||
"start_date": start_date,
|
||||
"end_date": current_date,
|
||||
},
|
||||
"label": "Company News & Social Media (Last 7 Days)",
|
||||
},
|
||||
]
|
||||
)
|
||||
prefetched_context = format_prefetched_context(prefetched)
|
||||
|
||||
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."""
|
||||
"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.\n\n"
|
||||
"## Pre-loaded Data\n\n"
|
||||
"Company-specific news and social media discussions for the past 7 days have already "
|
||||
"been fetched and are provided in the **Pre-loaded Context** section below. "
|
||||
"Do NOT call `get_news` — the data is already available.\n\n"
|
||||
"## Your Task\n\n"
|
||||
"Using the pre-loaded news and social media data, write a comprehensive long report "
|
||||
"detailing your analysis, insights, and implications for traders and investors on "
|
||||
"this company's current state. Cover:\n"
|
||||
"- Social media sentiment and what people are saying about the company\n"
|
||||
"- Daily sentiment shifts over the past week\n"
|
||||
"- Recent company news and its implications\n\n"
|
||||
"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 organise key points, making it easy to read."
|
||||
)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
|
|
@ -26,26 +61,27 @@ def create_social_media_analyst(llm):
|
|||
(
|
||||
"system",
|
||||
"You are a helpful AI assistant, collaborating with other assistants."
|
||||
" Use the provided tools to progress towards answering the question."
|
||||
" If you are unable to fully answer, that's OK; another assistant with different tools"
|
||||
" will help where you left off. Execute what you can to make progress."
|
||||
" 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}. {instrument_context}",
|
||||
"\n{system_message}"
|
||||
"For your reference, the current date is {current_date}. {instrument_context}\n\n"
|
||||
"## Pre-loaded Context\n\n{prefetched_context}",
|
||||
),
|
||||
MessagesPlaceholder(variable_name="messages"),
|
||||
]
|
||||
)
|
||||
|
||||
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(instrument_context=instrument_context)
|
||||
prompt = prompt.partial(prefetched_context=prefetched_context)
|
||||
|
||||
chain = prompt | llm.bind_tools(tools)
|
||||
# No tools remain — use direct invocation (no bind_tools, no tool loop)
|
||||
chain = prompt | llm
|
||||
|
||||
result = run_tool_loop(chain, state["messages"], tools)
|
||||
result = chain.invoke(state["messages"])
|
||||
|
||||
report = result.content or ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,57 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from langchain_core.messages import HumanMessage, RemoveMessage
|
||||
|
||||
|
||||
def prefetch_tools_parallel(tool_calls: list[dict]) -> dict[str, str]:
|
||||
"""Pre-fetch multiple LangChain tools in parallel using ThreadPoolExecutor.
|
||||
|
||||
Each entry in *tool_calls* must be a dict with:
|
||||
- ``"tool"``: the LangChain tool object (must have an ``.invoke()`` method)
|
||||
- ``"args"``: dict of keyword arguments to pass to ``tool.invoke()``
|
||||
- ``"label"``: human-readable section header for the injected context block
|
||||
|
||||
Per-tool exceptions are caught so that a failure in one pre-fetch never
|
||||
crashes the analyst node. The failing entry is replaced with an error
|
||||
placeholder so the LLM can fall back to calling that tool itself.
|
||||
|
||||
Returns:
|
||||
dict mapping ``label`` → result string (or error placeholder)
|
||||
"""
|
||||
results: dict[str, str] = {}
|
||||
|
||||
def _fetch_one(tc: dict) -> tuple[str, str]:
|
||||
label: str = tc["label"]
|
||||
try:
|
||||
result = tc["tool"].invoke(tc["args"])
|
||||
return label, str(result)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return label, f"[Error fetching {label}: {exc}]"
|
||||
|
||||
with ThreadPoolExecutor() as executor:
|
||||
futures = {executor.submit(_fetch_one, tc): tc["label"] for tc in tool_calls}
|
||||
for future in as_completed(futures):
|
||||
label, result = future.result()
|
||||
results[label] = result
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def format_prefetched_context(results: dict[str, str]) -> str:
|
||||
"""Format a prefetched-results dict into a clean Markdown block.
|
||||
|
||||
Each key becomes a ``## <label>`` section header, with the corresponding
|
||||
content beneath it. Sections are separated by a horizontal rule so the
|
||||
LLM can locate each dataset quickly.
|
||||
|
||||
Returns:
|
||||
A single multi-line string ready for injection into a system prompt.
|
||||
"""
|
||||
sections = [f"## {label}\n\n{content}" for label, content in results.items()]
|
||||
return "\n\n---\n\n".join(sections)
|
||||
|
||||
|
||||
def build_instrument_context(ticker: str) -> str:
|
||||
"""Describe the exact instrument so agents preserve exchange-qualified tickers."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue