feat(028-strategy-signals-contrib): wire strategy signals into analyst prompts

- Add strategy_signals field to AgentState (list of StrategySignal dicts)
- Compute signals once in Propagator.create_initial_state() with graceful fallback
- Add strategy_utils.get_signal_section() helper for role-based formatting
- Inject signals into all 4 analyst nodes (market, fundamentals, news, social)
- Inject signals into bull/bear researchers and research manager
- Inject signals into all 3 risk debators (aggressive, conservative, neutral)
- Inject signals into portfolio manager and trader
- Log strategy_signals in state output JSON
This commit is contained in:
Clayton Brown 2026-04-21 08:42:35 +10:00
parent 4e97302e22
commit 99815917c7
16 changed files with 91 additions and 13 deletions

View File

@ -8,6 +8,7 @@ from tradingagents.agents.utils.agent_utils import (
get_insider_transactions,
get_language_instruction,
)
from tradingagents.agents.utils.strategy_utils import get_signal_section
from tradingagents.dataflows.config import get_config
@ -15,6 +16,7 @@ def create_fundamentals_analyst(llm):
def fundamentals_analyst_node(state):
current_date = state["trade_date"]
instrument_context = build_instrument_context(state["company_of_interest"])
signal_section = get_signal_section(state, "fundamentals")
tools = [
get_fundamentals,
@ -41,7 +43,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}. {instrument_context}",
"For your reference, the current date is {current_date}. {instrument_context}{signal_section}",
),
MessagesPlaceholder(variable_name="messages"),
]
@ -51,6 +53,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(signal_section=signal_section)
chain = prompt | llm.bind_tools(tools)

View File

@ -5,6 +5,7 @@ from tradingagents.agents.utils.agent_utils import (
get_language_instruction,
get_stock_data,
)
from tradingagents.agents.utils.strategy_utils import get_signal_section
from tradingagents.dataflows.config import get_config
@ -13,6 +14,7 @@ def create_market_analyst(llm):
def market_analyst_node(state):
current_date = state["trade_date"]
instrument_context = build_instrument_context(state["company_of_interest"])
signal_section = get_signal_section(state, "market")
tools = [
get_stock_data,
@ -60,7 +62,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}. {instrument_context}",
"For your reference, the current date is {current_date}. {instrument_context}{signal_section}",
),
MessagesPlaceholder(variable_name="messages"),
]
@ -70,6 +72,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(signal_section=signal_section)
chain = prompt | llm.bind_tools(tools)

View File

@ -5,6 +5,7 @@ from tradingagents.agents.utils.agent_utils import (
get_language_instruction,
get_news,
)
from tradingagents.agents.utils.strategy_utils import get_signal_section
from tradingagents.dataflows.config import get_config
@ -12,6 +13,7 @@ def create_news_analyst(llm):
def news_analyst_node(state):
current_date = state["trade_date"]
instrument_context = build_instrument_context(state["company_of_interest"])
signal_section = get_signal_section(state, "news")
tools = [
get_news,
@ -35,7 +37,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}. {instrument_context}",
"For your reference, the current date is {current_date}. {instrument_context}{signal_section}",
),
MessagesPlaceholder(variable_name="messages"),
]
@ -45,6 +47,7 @@ def create_news_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(signal_section=signal_section)
chain = prompt | llm.bind_tools(tools)
result = chain.invoke(state["messages"])

View File

@ -1,5 +1,6 @@
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from tradingagents.agents.utils.agent_utils import build_instrument_context, get_language_instruction, get_news
from tradingagents.agents.utils.strategy_utils import get_signal_section
from tradingagents.dataflows.config import get_config
@ -7,6 +8,7 @@ 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"])
signal_section = get_signal_section(state, "social")
tools = [
get_news,
@ -29,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}. {instrument_context}",
"For your reference, the current date is {current_date}. {instrument_context}{signal_section}",
),
MessagesPlaceholder(variable_name="messages"),
]
@ -39,6 +41,7 @@ def create_social_media_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(signal_section=signal_section)
chain = prompt | llm.bind_tools(tools)

View File

@ -1,10 +1,12 @@
from tradingagents.agents.utils.agent_utils import build_instrument_context, get_language_instruction
from tradingagents.agents.utils.strategy_utils import get_signal_section
def create_portfolio_manager(llm, memory):
def portfolio_manager_node(state) -> dict:
instrument_context = build_instrument_context(state["company_of_interest"])
signal_section = get_signal_section(state, "risk")
history = state["risk_debate_state"]["history"]
risk_debate_state = state["risk_debate_state"]
@ -52,7 +54,8 @@ def create_portfolio_manager(llm, memory):
---
Be decisive and ground every conclusion in specific evidence from the analysts.{get_language_instruction()}"""
Be decisive and ground every conclusion in specific evidence from the analysts.{get_language_instruction()}
{signal_section}"""
response = llm.invoke(prompt)

View File

@ -1,10 +1,12 @@
from tradingagents.agents.utils.agent_utils import build_instrument_context
from tradingagents.agents.utils.strategy_utils import get_signal_section
def create_research_manager(llm, memory):
def research_manager_node(state) -> dict:
instrument_context = build_instrument_context(state["company_of_interest"])
signal_section = get_signal_section(state, "researcher")
history = state["investment_debate_state"].get("history", "")
market_research_report = state["market_report"]
sentiment_report = state["sentiment_report"]
@ -38,7 +40,8 @@ Here are your past reflections on mistakes:
Here is the debate:
Debate History:
{history}"""
{history}
{signal_section}"""
response = llm.invoke(prompt)
new_investment_debate_state = {

View File

@ -1,5 +1,8 @@
from tradingagents.agents.utils.strategy_utils import get_signal_section
def create_bear_researcher(llm, memory):
def bear_node(state) -> dict:
investment_debate_state = state["investment_debate_state"]
@ -11,6 +14,7 @@ def create_bear_researcher(llm, memory):
sentiment_report = state["sentiment_report"]
news_report = state["news_report"]
fundamentals_report = state["fundamentals_report"]
signal_section = get_signal_section(state, "researcher")
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)
@ -39,7 +43,7 @@ Conversation history of the debate: {history}
Last bull argument: {current_response}
Reflections from similar situations and lessons learned: {past_memory_str}
Use this information to deliver a compelling bear argument, refute the bull's claims, and engage in a dynamic debate that demonstrates the risks and weaknesses of investing in the stock. You must also address reflections and learn from lessons and mistakes you made in the past.
"""
{signal_section}"""
response = llm.invoke(prompt)

View File

@ -1,5 +1,8 @@
from tradingagents.agents.utils.strategy_utils import get_signal_section
def create_bull_researcher(llm, memory):
def bull_node(state) -> dict:
investment_debate_state = state["investment_debate_state"]
@ -11,6 +14,7 @@ def create_bull_researcher(llm, memory):
sentiment_report = state["sentiment_report"]
news_report = state["news_report"]
fundamentals_report = state["fundamentals_report"]
signal_section = get_signal_section(state, "researcher")
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)
@ -37,7 +41,7 @@ Conversation history of the debate: {history}
Last bear argument: {current_response}
Reflections from similar situations and lessons learned: {past_memory_str}
Use this information to deliver a compelling bull argument, refute the bear's concerns, and engage in a dynamic debate that demonstrates the strengths of the bull position. You must also address reflections and learn from lessons and mistakes you made in the past.
"""
{signal_section}"""
response = llm.invoke(prompt)

View File

@ -1,5 +1,8 @@
from tradingagents.agents.utils.strategy_utils import get_signal_section
def create_aggressive_debator(llm):
def aggressive_node(state) -> dict:
risk_debate_state = state["risk_debate_state"]
@ -13,6 +16,7 @@ def create_aggressive_debator(llm):
sentiment_report = state["sentiment_report"]
news_report = state["news_report"]
fundamentals_report = state["fundamentals_report"]
signal_section = get_signal_section(state, "risk")
trader_decision = state["trader_investment_plan"]
@ -28,7 +32,8 @@ 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 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."""
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.
{signal_section}"""
response = llm.invoke(prompt)

View File

@ -1,5 +1,8 @@
from tradingagents.agents.utils.strategy_utils import get_signal_section
def create_conservative_debator(llm):
def conservative_node(state) -> dict:
risk_debate_state = state["risk_debate_state"]
@ -13,6 +16,7 @@ def create_conservative_debator(llm):
sentiment_report = state["sentiment_report"]
news_report = state["news_report"]
fundamentals_report = state["fundamentals_report"]
signal_section = get_signal_section(state, "risk")
trader_decision = state["trader_investment_plan"]
@ -28,7 +32,8 @@ 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 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."""
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.
{signal_section}"""
response = llm.invoke(prompt)

View File

@ -1,5 +1,8 @@
from tradingagents.agents.utils.strategy_utils import get_signal_section
def create_neutral_debator(llm):
def neutral_node(state) -> dict:
risk_debate_state = state["risk_debate_state"]
@ -13,6 +16,7 @@ def create_neutral_debator(llm):
sentiment_report = state["sentiment_report"]
news_report = state["news_report"]
fundamentals_report = state["fundamentals_report"]
signal_section = get_signal_section(state, "risk")
trader_decision = state["trader_investment_plan"]
@ -28,7 +32,8 @@ 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 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."""
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.
{signal_section}"""
response = llm.invoke(prompt)

View File

@ -1,12 +1,14 @@
import functools
from tradingagents.agents.utils.agent_utils import build_instrument_context
from tradingagents.agents.utils.strategy_utils import get_signal_section
def create_trader(llm, memory):
def trader_node(state, name):
company_name = state["company_of_interest"]
instrument_context = build_instrument_context(company_name)
signal_section = get_signal_section(state, "researcher")
investment_plan = state["investment_plan"]
market_research_report = state["market_report"]
sentiment_report = state["sentiment_report"]
@ -31,7 +33,8 @@ 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. 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}""",
"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}
{signal_section}""",
},
context,
]

View File

@ -1,4 +1,4 @@
from typing import Annotated
from typing import Annotated, Any
from typing_extensions import TypedDict
from langgraph.graph import MessagesState
@ -70,3 +70,6 @@ class AgentState(MessagesState):
RiskDebateState, "Current state of the debate on evaluating risk"
]
final_trade_decision: Annotated[str, "Final decision made by the Risk Analysts"]
# Quantitative strategy signals (computed once, consumed by all nodes)
strategy_signals: Annotated[list[dict[str, Any]], "Deterministic strategy signals from the strategies framework"]

View File

@ -0,0 +1,18 @@
"""Utility to extract formatted strategy signals from agent state."""
from __future__ import annotations
from typing import Any
def get_signal_section(state: dict[str, Any], role: str) -> str:
"""Return a formatted strategy signals section for *role*, or empty string."""
signals = state.get("strategy_signals")
if not signals:
return ""
try:
from tradingagents.strategies import format_signals_for_role
section = format_signals_for_role(signals, role)
return f"\n\n{section}" if section else ""
except Exception:
return ""

View File

@ -1,5 +1,6 @@
# TradingAgents/graph/propagation.py
import logging
from typing import Dict, Any, List, Optional
from tradingagents.agents.utils.agent_states import (
AgentState,
@ -7,6 +8,8 @@ from tradingagents.agents.utils.agent_states import (
RiskDebateState,
)
logger = logging.getLogger(__name__)
class Propagator:
"""Handles state initialization and propagation through the graph."""
@ -19,6 +22,14 @@ class Propagator:
self, company_name: str, trade_date: str
) -> Dict[str, Any]:
"""Create the initial state for the agent graph."""
# Compute strategy signals once up-front
strategy_signals: list[dict[str, Any]] = []
try:
from tradingagents.strategies import compute_signals
strategy_signals = compute_signals(company_name, str(trade_date))
except Exception:
logger.warning("Strategy signal computation failed; continuing without signals", exc_info=True)
return {
"messages": [("human", company_name)],
"company_of_interest": company_name,
@ -51,6 +62,7 @@ class Propagator:
"fundamentals_report": "",
"sentiment_report": "",
"news_report": "",
"strategy_signals": strategy_signals,
}
def get_graph_args(self, callbacks: Optional[List] = None) -> Dict[str, Any]:

View File

@ -254,6 +254,7 @@ class TradingAgentsGraph:
},
"investment_plan": final_state["investment_plan"],
"final_trade_decision": final_state["final_trade_decision"],
"strategy_signals": final_state.get("strategy_signals", []),
}
# Save to file