mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-06-16 21:06:15 +03:00
fix(graph): resolve instrument identity to stop wrong-company hallucination
Agents had no ground-truth ticker→company mapping, so the market analyst could pattern-match a price chart to the wrong company (e.g. TOTDY read as "TotalEnergies"), and every downstream agent inherited the bad framing. Resolve identity once at run start via a cached, fail-open yfinance lookup and inject company/sector/exchange into the shared instrument context that all twelve agents consume, with an explicit do-not-substitute instruction. Resolution runs on both the propagate() and CLI entry points. Also replaces the bare "Continue" message-clear placeholder, which some OpenAI-compatible providers interpreted as the user task, with a context-anchored placeholder carrying the resolved identity and date. #814 #888
This commit is contained in:
@@ -1098,11 +1098,18 @@ def run_analysis(checkpoint: bool = False):
|
|||||||
)
|
)
|
||||||
update_display(layout, spinner_text, stats_handler=stats_handler, start_time=start_time)
|
update_display(layout, spinner_text, stats_handler=stats_handler, start_time=start_time)
|
||||||
|
|
||||||
# Initialize state and get graph args with callbacks
|
# Initialize state and get graph args with callbacks.
|
||||||
|
# Resolve the instrument identity once here so all agents anchor to
|
||||||
|
# the real company (#814); the CLI builds state directly rather than
|
||||||
|
# going through propagate(), so this must happen on the CLI path too.
|
||||||
|
instrument_context = graph.resolve_instrument_context(
|
||||||
|
selections["ticker"], selections["asset_type"]
|
||||||
|
)
|
||||||
init_agent_state = graph.propagator.create_initial_state(
|
init_agent_state = graph.propagator.create_initial_state(
|
||||||
selections["ticker"],
|
selections["ticker"],
|
||||||
selections["analysis_date"],
|
selections["analysis_date"],
|
||||||
asset_type=selections["asset_type"],
|
asset_type=selections["asset_type"],
|
||||||
|
instrument_context=instrument_context,
|
||||||
)
|
)
|
||||||
# Pass callbacks to graph config for tool execution tracking
|
# Pass callbacks to graph config for tool execution tracking
|
||||||
# (LLM tracking is handled separately via LLM constructor)
|
# (LLM tracking is handled separately via LLM constructor)
|
||||||
|
|||||||
170
tests/test_instrument_identity.py
Normal file
170
tests/test_instrument_identity.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
"""Tests for deterministic instrument-identity resolution (#814) and the
|
||||||
|
context-anchored message placeholder (#888)."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
|
||||||
|
|
||||||
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
|
build_instrument_context,
|
||||||
|
create_msg_delete,
|
||||||
|
get_instrument_context_from_state,
|
||||||
|
resolve_instrument_identity,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class ResolveInstrumentIdentityTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
resolve_instrument_identity.cache_clear()
|
||||||
|
|
||||||
|
def test_resolves_company_metadata_from_yfinance(self):
|
||||||
|
with patch("tradingagents.agents.utils.agent_utils.yf.Ticker") as mock:
|
||||||
|
mock.return_value.info = {
|
||||||
|
"longName": "TOTO LTD.",
|
||||||
|
"shortName": "TOTO",
|
||||||
|
"sector": "Industrials",
|
||||||
|
"industry": "Building Products & Equipment",
|
||||||
|
"exchange": "PNK",
|
||||||
|
"quoteType": "EQUITY",
|
||||||
|
}
|
||||||
|
identity = resolve_instrument_identity("totdy")
|
||||||
|
mock.assert_called_once_with("TOTDY")
|
||||||
|
self.assertEqual(identity["company_name"], "TOTO LTD.")
|
||||||
|
self.assertEqual(identity["sector"], "Industrials")
|
||||||
|
self.assertEqual(identity["industry"], "Building Products & Equipment")
|
||||||
|
self.assertEqual(identity["exchange"], "PNK")
|
||||||
|
|
||||||
|
def test_falls_back_to_short_name(self):
|
||||||
|
with patch("tradingagents.agents.utils.agent_utils.yf.Ticker") as mock:
|
||||||
|
mock.return_value.info = {"shortName": "TOTO", "sector": "Industrials"}
|
||||||
|
identity = resolve_instrument_identity("TOTDY")
|
||||||
|
self.assertEqual(identity["company_name"], "TOTO")
|
||||||
|
|
||||||
|
def test_skips_placeholder_values(self):
|
||||||
|
with patch("tradingagents.agents.utils.agent_utils.yf.Ticker") as mock:
|
||||||
|
mock.return_value.info = {"longName": " ", "sector": "None", "industry": "n/a"}
|
||||||
|
identity = resolve_instrument_identity("TOTDY")
|
||||||
|
self.assertEqual(identity, {})
|
||||||
|
|
||||||
|
def test_fails_open_on_exception(self):
|
||||||
|
with patch(
|
||||||
|
"tradingagents.agents.utils.agent_utils.yf.Ticker",
|
||||||
|
side_effect=RuntimeError("rate limited"),
|
||||||
|
):
|
||||||
|
self.assertEqual(resolve_instrument_identity("TOTDY"), {})
|
||||||
|
|
||||||
|
def test_result_is_cached(self):
|
||||||
|
with patch("tradingagents.agents.utils.agent_utils.yf.Ticker") as mock:
|
||||||
|
mock.return_value.info = {"longName": "TOTO LTD."}
|
||||||
|
first = resolve_instrument_identity("TOTDY")
|
||||||
|
second = resolve_instrument_identity("TOTDY")
|
||||||
|
mock.assert_called_once() # second call served from cache
|
||||||
|
self.assertEqual(first, second)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class BuildInstrumentContextTests(unittest.TestCase):
|
||||||
|
def test_mentions_exact_symbol_without_identity(self):
|
||||||
|
context = build_instrument_context("7203.T")
|
||||||
|
self.assertIn("7203.T", context)
|
||||||
|
self.assertIn("exchange suffix", context)
|
||||||
|
self.assertNotIn("Resolved identity", context)
|
||||||
|
|
||||||
|
def test_injects_resolved_identity(self):
|
||||||
|
context = build_instrument_context(
|
||||||
|
"TOTDY", "stock",
|
||||||
|
{
|
||||||
|
"company_name": "TOTO LTD.",
|
||||||
|
"sector": "Industrials",
|
||||||
|
"industry": "Building Products & Equipment",
|
||||||
|
"exchange": "PNK",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertIn("Company: TOTO LTD.", context)
|
||||||
|
self.assertIn("Industrials / Building Products & Equipment", context)
|
||||||
|
self.assertIn("Exchange: PNK", context)
|
||||||
|
self.assertIn("Do not substitute a different company", context)
|
||||||
|
|
||||||
|
def test_crypto_uses_name_label_and_keeps_hint(self):
|
||||||
|
context = build_instrument_context(
|
||||||
|
"BTC-USD", "crypto", {"company_name": "Bitcoin USD"}
|
||||||
|
)
|
||||||
|
self.assertIn("Name: Bitcoin USD", context)
|
||||||
|
self.assertIn("crypto asset rather than a company", context)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class GetInstrumentContextFromStateTests(unittest.TestCase):
|
||||||
|
def test_prefers_precomputed_context(self):
|
||||||
|
state = {"company_of_interest": "TOTDY", "instrument_context": "PRECOMPUTED"}
|
||||||
|
self.assertEqual(get_instrument_context_from_state(state), "PRECOMPUTED")
|
||||||
|
|
||||||
|
def test_fallback_is_network_free_ticker_only(self):
|
||||||
|
# No instrument_context and no yfinance call — must not hit the network.
|
||||||
|
with patch("tradingagents.agents.utils.agent_utils.yf.Ticker") as mock:
|
||||||
|
context = get_instrument_context_from_state(
|
||||||
|
{"company_of_interest": "NVDA", "asset_type": "stock"}
|
||||||
|
)
|
||||||
|
mock.assert_not_called()
|
||||||
|
self.assertIn("NVDA", context)
|
||||||
|
|
||||||
|
def test_fallback_respects_asset_type(self):
|
||||||
|
context = get_instrument_context_from_state(
|
||||||
|
{"company_of_interest": "BTC-USD", "asset_type": "crypto"}
|
||||||
|
)
|
||||||
|
self.assertIn("crypto asset", context)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class ContextAnchoredPlaceholderTests(unittest.TestCase):
|
||||||
|
"""#888 — the message-clear placeholder must not be a bare 'Continue'."""
|
||||||
|
|
||||||
|
def _run(self, state_extra):
|
||||||
|
state = {
|
||||||
|
"messages": [
|
||||||
|
HumanMessage(content="old", id="h1"),
|
||||||
|
AIMessage(content="reply", id="a1"),
|
||||||
|
],
|
||||||
|
**state_extra,
|
||||||
|
}
|
||||||
|
return create_msg_delete()(state)
|
||||||
|
|
||||||
|
def test_placeholder_is_not_bare_continue(self):
|
||||||
|
result = self._run(
|
||||||
|
{"company_of_interest": "EC", "asset_type": "stock", "trade_date": "2026-05-28"}
|
||||||
|
)
|
||||||
|
placeholder = result["messages"][-1]
|
||||||
|
self.assertIsInstance(placeholder, HumanMessage)
|
||||||
|
self.assertNotEqual(placeholder.content.strip(), "Continue")
|
||||||
|
|
||||||
|
def test_placeholder_carries_resolved_identity(self):
|
||||||
|
result = self._run(
|
||||||
|
{
|
||||||
|
"company_of_interest": "EC",
|
||||||
|
"instrument_context": "The instrument to analyze is `EC`. Resolved identity: Company: Ecopetrol.",
|
||||||
|
"trade_date": "2026-05-28",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
content = result["messages"][-1].content
|
||||||
|
self.assertIn("Ecopetrol", content)
|
||||||
|
self.assertIn("2026-05-28", content)
|
||||||
|
|
||||||
|
def test_old_messages_are_removed(self):
|
||||||
|
result = self._run({"company_of_interest": "EC", "trade_date": "2026-05-28"})
|
||||||
|
removals = [m for m in result["messages"] if isinstance(m, RemoveMessage)]
|
||||||
|
humans = [m for m in result["messages"] if isinstance(m, HumanMessage)]
|
||||||
|
self.assertEqual(len(removals), 2)
|
||||||
|
self.assertEqual(len(humans), 1)
|
||||||
|
|
||||||
|
def test_safe_defaults_when_state_minimal(self):
|
||||||
|
result = create_msg_delete()({"messages": [], "company_of_interest": "EC"})
|
||||||
|
placeholder = result["messages"][-1]
|
||||||
|
self.assertNotEqual(placeholder.content.strip(), "Continue")
|
||||||
|
self.assertIn("EC", placeholder.content)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||||
from tradingagents.agents.utils.agent_utils import (
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
build_instrument_context,
|
get_instrument_context_from_state,
|
||||||
get_balance_sheet,
|
get_balance_sheet,
|
||||||
get_cashflow,
|
get_cashflow,
|
||||||
get_fundamentals,
|
get_fundamentals,
|
||||||
@@ -14,7 +14,7 @@ from tradingagents.dataflows.config import get_config
|
|||||||
def create_fundamentals_analyst(llm):
|
def create_fundamentals_analyst(llm):
|
||||||
def fundamentals_analyst_node(state):
|
def fundamentals_analyst_node(state):
|
||||||
current_date = state["trade_date"]
|
current_date = state["trade_date"]
|
||||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
|
|
||||||
tools = [
|
tools = [
|
||||||
get_fundamentals,
|
get_fundamentals,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||||
from tradingagents.agents.utils.agent_utils import (
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
build_instrument_context,
|
get_instrument_context_from_state,
|
||||||
get_indicators,
|
get_indicators,
|
||||||
get_language_instruction,
|
get_language_instruction,
|
||||||
get_stock_data,
|
get_stock_data,
|
||||||
@@ -12,10 +12,7 @@ def create_market_analyst(llm):
|
|||||||
|
|
||||||
def market_analyst_node(state):
|
def market_analyst_node(state):
|
||||||
current_date = state["trade_date"]
|
current_date = state["trade_date"]
|
||||||
asset_type = state.get("asset_type", "stock")
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
instrument_context = build_instrument_context(
|
|
||||||
state["company_of_interest"], asset_type
|
|
||||||
)
|
|
||||||
|
|
||||||
tools = [
|
tools = [
|
||||||
get_stock_data,
|
get_stock_data,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||||
from tradingagents.agents.utils.agent_utils import (
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
build_instrument_context,
|
get_instrument_context_from_state,
|
||||||
get_global_news,
|
get_global_news,
|
||||||
get_language_instruction,
|
get_language_instruction,
|
||||||
get_news,
|
get_news,
|
||||||
@@ -13,9 +13,7 @@ def create_news_analyst(llm):
|
|||||||
current_date = state["trade_date"]
|
current_date = state["trade_date"]
|
||||||
asset_type = state.get("asset_type", "stock")
|
asset_type = state.get("asset_type", "stock")
|
||||||
asset_label = "company" if asset_type == "stock" else "asset"
|
asset_label = "company" if asset_type == "stock" else "asset"
|
||||||
instrument_context = build_instrument_context(
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
state["company_of_interest"], asset_type
|
|
||||||
)
|
|
||||||
|
|
||||||
tools = [
|
tools = [
|
||||||
get_news,
|
get_news,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||||
from tradingagents.agents.utils.agent_utils import (
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
build_instrument_context,
|
get_instrument_context_from_state,
|
||||||
get_language_instruction,
|
get_language_instruction,
|
||||||
get_news,
|
get_news,
|
||||||
)
|
)
|
||||||
@@ -47,7 +47,7 @@ def create_sentiment_analyst(llm):
|
|||||||
ticker = state["company_of_interest"]
|
ticker = state["company_of_interest"]
|
||||||
end_date = state["trade_date"]
|
end_date = state["trade_date"]
|
||||||
start_date = _seven_days_back(end_date)
|
start_date = _seven_days_back(end_date)
|
||||||
instrument_context = build_instrument_context(ticker)
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
|
|
||||||
# Pre-fetch all three sources. Each fetcher degrades gracefully and
|
# Pre-fetch all three sources. Each fetcher degrades gracefully and
|
||||||
# returns a string (no exceptions surface from here), so the LLM
|
# returns a string (no exceptions surface from here), so the LLM
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from tradingagents.agents.schemas import PortfolioDecision, render_pm_decision
|
from tradingagents.agents.schemas import PortfolioDecision, render_pm_decision
|
||||||
from tradingagents.agents.utils.agent_utils import (
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
build_instrument_context,
|
get_instrument_context_from_state,
|
||||||
get_language_instruction,
|
get_language_instruction,
|
||||||
)
|
)
|
||||||
from tradingagents.agents.utils.structured import (
|
from tradingagents.agents.utils.structured import (
|
||||||
@@ -25,7 +25,7 @@ def create_portfolio_manager(llm):
|
|||||||
structured_llm = bind_structured(llm, PortfolioDecision, "Portfolio Manager")
|
structured_llm = bind_structured(llm, PortfolioDecision, "Portfolio Manager")
|
||||||
|
|
||||||
def portfolio_manager_node(state) -> dict:
|
def portfolio_manager_node(state) -> dict:
|
||||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
|
|
||||||
history = state["risk_debate_state"]["history"]
|
history = state["risk_debate_state"]["history"]
|
||||||
risk_debate_state = state["risk_debate_state"]
|
risk_debate_state = state["risk_debate_state"]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from tradingagents.agents.schemas import ResearchPlan, render_research_plan
|
from tradingagents.agents.schemas import ResearchPlan, render_research_plan
|
||||||
from tradingagents.agents.utils.agent_utils import (
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
build_instrument_context,
|
get_instrument_context_from_state,
|
||||||
get_language_instruction,
|
get_language_instruction,
|
||||||
)
|
)
|
||||||
from tradingagents.agents.utils.structured import (
|
from tradingagents.agents.utils.structured import (
|
||||||
@@ -17,7 +17,7 @@ def create_research_manager(llm):
|
|||||||
structured_llm = bind_structured(llm, ResearchPlan, "Research Manager")
|
structured_llm = bind_structured(llm, ResearchPlan, "Research Manager")
|
||||||
|
|
||||||
def research_manager_node(state) -> dict:
|
def research_manager_node(state) -> dict:
|
||||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
history = state["investment_debate_state"].get("history", "")
|
history = state["investment_debate_state"].get("history", "")
|
||||||
|
|
||||||
investment_debate_state = state["investment_debate_state"]
|
investment_debate_state = state["investment_debate_state"]
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from tradingagents.agents.utils.agent_utils import get_language_instruction
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
|
get_instrument_context_from_state,
|
||||||
|
get_language_instruction,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_bear_researcher(llm):
|
def create_bear_researcher(llm):
|
||||||
@@ -12,6 +15,7 @@ def create_bear_researcher(llm):
|
|||||||
sentiment_report = state["sentiment_report"]
|
sentiment_report = state["sentiment_report"]
|
||||||
news_report = state["news_report"]
|
news_report = state["news_report"]
|
||||||
fundamentals_report = state["fundamentals_report"]
|
fundamentals_report = state["fundamentals_report"]
|
||||||
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
asset_type = state.get("asset_type", "stock")
|
asset_type = state.get("asset_type", "stock")
|
||||||
target_label = "stock" if asset_type == "stock" else "asset"
|
target_label = "stock" if asset_type == "stock" else "asset"
|
||||||
fundamentals_label = (
|
fundamentals_label = (
|
||||||
@@ -32,6 +36,7 @@ Key points to focus on:
|
|||||||
|
|
||||||
Resources available:
|
Resources available:
|
||||||
|
|
||||||
|
{instrument_context}
|
||||||
Market research report: {market_research_report}
|
Market research report: {market_research_report}
|
||||||
Social media sentiment report: {sentiment_report}
|
Social media sentiment report: {sentiment_report}
|
||||||
Latest world affairs news: {news_report}
|
Latest world affairs news: {news_report}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from tradingagents.agents.utils.agent_utils import get_language_instruction
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
|
get_instrument_context_from_state,
|
||||||
|
get_language_instruction,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_bull_researcher(llm):
|
def create_bull_researcher(llm):
|
||||||
@@ -12,6 +15,7 @@ def create_bull_researcher(llm):
|
|||||||
sentiment_report = state["sentiment_report"]
|
sentiment_report = state["sentiment_report"]
|
||||||
news_report = state["news_report"]
|
news_report = state["news_report"]
|
||||||
fundamentals_report = state["fundamentals_report"]
|
fundamentals_report = state["fundamentals_report"]
|
||||||
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
asset_type = state.get("asset_type", "stock")
|
asset_type = state.get("asset_type", "stock")
|
||||||
target_label = "stock" if asset_type == "stock" else "asset"
|
target_label = "stock" if asset_type == "stock" else "asset"
|
||||||
fundamentals_label = (
|
fundamentals_label = (
|
||||||
@@ -30,6 +34,7 @@ Key points to focus on:
|
|||||||
- Engagement: Present your argument in a conversational style, engaging directly with the bear analyst's points and debating effectively rather than just listing data.
|
- Engagement: Present your argument in a conversational style, engaging directly with the bear analyst's points and debating effectively rather than just listing data.
|
||||||
|
|
||||||
Resources available:
|
Resources available:
|
||||||
|
{instrument_context}
|
||||||
Market research report: {market_research_report}
|
Market research report: {market_research_report}
|
||||||
Social media sentiment report: {sentiment_report}
|
Social media sentiment report: {sentiment_report}
|
||||||
Latest world affairs news: {news_report}
|
Latest world affairs news: {news_report}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from tradingagents.agents.utils.agent_utils import get_language_instruction
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
|
get_instrument_context_from_state,
|
||||||
|
get_language_instruction,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_aggressive_debator(llm):
|
def create_aggressive_debator(llm):
|
||||||
@@ -14,6 +17,7 @@ def create_aggressive_debator(llm):
|
|||||||
sentiment_report = state["sentiment_report"]
|
sentiment_report = state["sentiment_report"]
|
||||||
news_report = state["news_report"]
|
news_report = state["news_report"]
|
||||||
fundamentals_report = state["fundamentals_report"]
|
fundamentals_report = state["fundamentals_report"]
|
||||||
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
|
|
||||||
trader_decision = state["trader_investment_plan"]
|
trader_decision = state["trader_investment_plan"]
|
||||||
|
|
||||||
@@ -23,6 +27,7 @@ def create_aggressive_debator(llm):
|
|||||||
|
|
||||||
Your task is to create a compelling case for the trader's decision by questioning and critiquing the conservative and neutral stances to demonstrate why your high-reward perspective offers the best path forward. Incorporate insights from the following sources into your arguments:
|
Your task is to create a compelling case for the trader's decision by questioning and critiquing the conservative and neutral stances to demonstrate why your high-reward perspective offers the best path forward. Incorporate insights from the following sources into your arguments:
|
||||||
|
|
||||||
|
{instrument_context}
|
||||||
Market Research Report: {market_research_report}
|
Market Research Report: {market_research_report}
|
||||||
Social Media Sentiment Report: {sentiment_report}
|
Social Media Sentiment Report: {sentiment_report}
|
||||||
Latest World Affairs Report: {news_report}
|
Latest World Affairs Report: {news_report}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from tradingagents.agents.utils.agent_utils import get_language_instruction
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
|
get_instrument_context_from_state,
|
||||||
|
get_language_instruction,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_conservative_debator(llm):
|
def create_conservative_debator(llm):
|
||||||
@@ -14,6 +17,7 @@ def create_conservative_debator(llm):
|
|||||||
sentiment_report = state["sentiment_report"]
|
sentiment_report = state["sentiment_report"]
|
||||||
news_report = state["news_report"]
|
news_report = state["news_report"]
|
||||||
fundamentals_report = state["fundamentals_report"]
|
fundamentals_report = state["fundamentals_report"]
|
||||||
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
|
|
||||||
trader_decision = state["trader_investment_plan"]
|
trader_decision = state["trader_investment_plan"]
|
||||||
|
|
||||||
@@ -23,6 +27,7 @@ def create_conservative_debator(llm):
|
|||||||
|
|
||||||
Your task is to actively counter the arguments of the Aggressive and Neutral Analysts, highlighting where their views may overlook potential threats or fail to prioritize sustainability. Respond directly to their points, drawing from the following data sources to build a convincing case for a low-risk approach adjustment to the trader's decision:
|
Your task is to actively counter the arguments of the Aggressive and Neutral Analysts, highlighting where their views may overlook potential threats or fail to prioritize sustainability. Respond directly to their points, drawing from the following data sources to build a convincing case for a low-risk approach adjustment to the trader's decision:
|
||||||
|
|
||||||
|
{instrument_context}
|
||||||
Market Research Report: {market_research_report}
|
Market Research Report: {market_research_report}
|
||||||
Social Media Sentiment Report: {sentiment_report}
|
Social Media Sentiment Report: {sentiment_report}
|
||||||
Latest World Affairs Report: {news_report}
|
Latest World Affairs Report: {news_report}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from tradingagents.agents.utils.agent_utils import get_language_instruction
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
|
get_instrument_context_from_state,
|
||||||
|
get_language_instruction,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_neutral_debator(llm):
|
def create_neutral_debator(llm):
|
||||||
@@ -14,6 +17,7 @@ def create_neutral_debator(llm):
|
|||||||
sentiment_report = state["sentiment_report"]
|
sentiment_report = state["sentiment_report"]
|
||||||
news_report = state["news_report"]
|
news_report = state["news_report"]
|
||||||
fundamentals_report = state["fundamentals_report"]
|
fundamentals_report = state["fundamentals_report"]
|
||||||
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
|
|
||||||
trader_decision = state["trader_investment_plan"]
|
trader_decision = state["trader_investment_plan"]
|
||||||
|
|
||||||
@@ -23,6 +27,7 @@ def create_neutral_debator(llm):
|
|||||||
|
|
||||||
Your task is to challenge both the Aggressive and Conservative Analysts, pointing out where each perspective may be overly optimistic or overly cautious. Use insights from the following data sources to support a moderate, sustainable strategy to adjust the trader's decision:
|
Your task is to challenge both the Aggressive and Conservative Analysts, pointing out where each perspective may be overly optimistic or overly cautious. Use insights from the following data sources to support a moderate, sustainable strategy to adjust the trader's decision:
|
||||||
|
|
||||||
|
{instrument_context}
|
||||||
Market Research Report: {market_research_report}
|
Market Research Report: {market_research_report}
|
||||||
Social Media Sentiment Report: {sentiment_report}
|
Social Media Sentiment Report: {sentiment_report}
|
||||||
Latest World Affairs Report: {news_report}
|
Latest World Affairs Report: {news_report}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from langchain_core.messages import AIMessage
|
|||||||
|
|
||||||
from tradingagents.agents.schemas import TraderProposal, render_trader_proposal
|
from tradingagents.agents.schemas import TraderProposal, render_trader_proposal
|
||||||
from tradingagents.agents.utils.agent_utils import (
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
build_instrument_context,
|
get_instrument_context_from_state,
|
||||||
get_language_instruction,
|
get_language_instruction,
|
||||||
)
|
)
|
||||||
from tradingagents.agents.utils.structured import (
|
from tradingagents.agents.utils.structured import (
|
||||||
@@ -22,8 +22,7 @@ def create_trader(llm):
|
|||||||
|
|
||||||
def trader_node(state, name):
|
def trader_node(state, name):
|
||||||
company_name = state["company_of_interest"]
|
company_name = state["company_of_interest"]
|
||||||
asset_type = state.get("asset_type", "stock")
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
instrument_context = build_instrument_context(company_name, asset_type)
|
|
||||||
investment_plan = state["investment_plan"]
|
investment_plan = state["investment_plan"]
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class RiskDebateState(TypedDict):
|
|||||||
class AgentState(MessagesState):
|
class AgentState(MessagesState):
|
||||||
company_of_interest: Annotated[str, "Company that we are interested in trading"]
|
company_of_interest: Annotated[str, "Company that we are interested in trading"]
|
||||||
asset_type: Annotated[str, "Asset type under analysis such as stock or crypto"]
|
asset_type: Annotated[str, "Asset type under analysis such as stock or crypto"]
|
||||||
|
instrument_context: Annotated[str, "Deterministic ticker identity resolved at run start"]
|
||||||
trade_date: Annotated[str, "What date we are trading at"]
|
trade_date: Annotated[str, "What date we are trading at"]
|
||||||
|
|
||||||
sender: Annotated[str, "Agent that sent this message"]
|
sender: Annotated[str, "Agent that sent this message"]
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
from typing import Any, Mapping, Optional
|
||||||
|
|
||||||
|
import yfinance as yf
|
||||||
from langchain_core.messages import HumanMessage, RemoveMessage
|
from langchain_core.messages import HumanMessage, RemoveMessage
|
||||||
|
|
||||||
# Import tools from separate utility files
|
# Import tools from separate utility files
|
||||||
@@ -19,6 +24,8 @@ from tradingagents.agents.utils.news_data_tools import (
|
|||||||
get_global_news
|
get_global_news
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_language_instruction() -> str:
|
def get_language_instruction() -> str:
|
||||||
"""Return a prompt instruction for the configured output language.
|
"""Return a prompt instruction for the configured output language.
|
||||||
@@ -36,32 +43,145 @@ def get_language_instruction() -> str:
|
|||||||
return f" Write your entire response in {lang}."
|
return f" Write your entire response in {lang}."
|
||||||
|
|
||||||
|
|
||||||
def build_instrument_context(ticker: str, asset_type: str = "stock") -> str:
|
def _clean_identity_value(value: Any) -> Optional[str]:
|
||||||
"""Describe the exact instrument so agents preserve exchange-qualified tickers."""
|
"""Return a trimmed string, or None for empty / placeholder-ish values."""
|
||||||
instrument_label = "asset" if asset_type == "crypto" else "instrument"
|
if not isinstance(value, str):
|
||||||
extra_hint = (
|
return None
|
||||||
" Treat it as a crypto asset rather than a company, and do not assume company fundamentals are available."
|
cleaned = value.strip()
|
||||||
if asset_type == "crypto"
|
if not cleaned or cleaned.lower() in {"none", "n/a", "nan", "null"}:
|
||||||
else ""
|
return None
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=256)
|
||||||
|
def resolve_instrument_identity(ticker: str) -> dict:
|
||||||
|
"""Resolve deterministic identity metadata (company name, sector, …) for a ticker.
|
||||||
|
|
||||||
|
This exists to stop the pipeline from hallucinating a *different* company
|
||||||
|
when a chart pattern suggests a different industry than the real one
|
||||||
|
(#814): without a ground-truth name, the market analyst would pattern-match
|
||||||
|
the price action to a narrative and invent an identity that then cascaded
|
||||||
|
through every downstream agent.
|
||||||
|
|
||||||
|
Best-effort by design: if yfinance is unavailable, rate-limited, or doesn't
|
||||||
|
recognise the ticker, we return ``{}`` and the caller falls back to
|
||||||
|
ticker-only context rather than failing before analysis starts. Cached so
|
||||||
|
the lookup happens at most once per ticker per process.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
info = yf.Ticker(ticker.upper()).info or {}
|
||||||
|
except Exception as exc: # noqa: BLE001 — fail open, never block the run
|
||||||
|
logger.debug("Could not resolve instrument identity for %s: %s", ticker, exc)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
identity: dict[str, str] = {}
|
||||||
|
company_name = _clean_identity_value(info.get("longName")) or _clean_identity_value(
|
||||||
|
info.get("shortName")
|
||||||
)
|
)
|
||||||
return (
|
if company_name:
|
||||||
|
identity["company_name"] = company_name
|
||||||
|
for source_key, target_key in (
|
||||||
|
("sector", "sector"),
|
||||||
|
("industry", "industry"),
|
||||||
|
("exchange", "exchange"),
|
||||||
|
("quoteType", "quote_type"),
|
||||||
|
):
|
||||||
|
value = _clean_identity_value(info.get(source_key))
|
||||||
|
if value:
|
||||||
|
identity[target_key] = value
|
||||||
|
return identity
|
||||||
|
|
||||||
|
|
||||||
|
def build_instrument_context(
|
||||||
|
ticker: str,
|
||||||
|
asset_type: str = "stock",
|
||||||
|
identity: Optional[Mapping[str, str]] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Describe the exact instrument so agents preserve identity and ticker.
|
||||||
|
|
||||||
|
When ``identity`` is provided (resolved deterministically via
|
||||||
|
:func:`resolve_instrument_identity`), the company name and business
|
||||||
|
classification are injected so agents anchor to the real company rather
|
||||||
|
than pattern-matching the price chart to a wrong one (#814).
|
||||||
|
"""
|
||||||
|
is_crypto = asset_type == "crypto"
|
||||||
|
instrument_label = "asset" if is_crypto else "instrument"
|
||||||
|
context = (
|
||||||
f"The {instrument_label} to analyze is `{ticker}`. "
|
f"The {instrument_label} to analyze is `{ticker}`. "
|
||||||
"Use this exact ticker in every tool call, report, and recommendation, "
|
"Use this exact ticker in every tool call, report, and recommendation, "
|
||||||
"preserving any exchange suffix (e.g. `.TO`, `.L`, `.HK`, `.T`, `-USD`)."
|
"preserving any exchange suffix (e.g. `.TO`, `.L`, `.HK`, `.T`, `-USD`)."
|
||||||
+ extra_hint
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
details = []
|
||||||
|
if identity:
|
||||||
|
name = identity.get("company_name") or identity.get("name")
|
||||||
|
if name:
|
||||||
|
details.append(f"{'Name' if is_crypto else 'Company'}: {name}")
|
||||||
|
sector, industry = identity.get("sector"), identity.get("industry")
|
||||||
|
if sector and industry:
|
||||||
|
details.append(f"Business classification: {sector} / {industry}")
|
||||||
|
elif sector:
|
||||||
|
details.append(f"Sector: {sector}")
|
||||||
|
elif industry:
|
||||||
|
details.append(f"Industry: {industry}")
|
||||||
|
if identity.get("exchange"):
|
||||||
|
details.append(f"Exchange: {identity['exchange']}")
|
||||||
|
|
||||||
|
if details:
|
||||||
|
context += (
|
||||||
|
f" Resolved identity: {'; '.join(details)}. "
|
||||||
|
"Do not substitute a different company or ticker unless a tool "
|
||||||
|
"result explicitly disproves this resolved identity."
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_crypto:
|
||||||
|
context += (
|
||||||
|
" Treat it as a crypto asset rather than a company, and do not "
|
||||||
|
"assume company fundamentals are available."
|
||||||
|
)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def get_instrument_context_from_state(state: Mapping[str, Any]) -> str:
|
||||||
|
"""Return the instrument context for the current run.
|
||||||
|
|
||||||
|
Prefers the identity-resolved context computed once at run start and
|
||||||
|
stored on the state (see ``TradingAgentsGraph.resolve_instrument_context``).
|
||||||
|
Falls back to a ticker-only context — with no network lookup — when the
|
||||||
|
state was constructed without it (bare programmatic states, tests), so a
|
||||||
|
consumer is never forced to make a yfinance call mid-graph.
|
||||||
|
"""
|
||||||
|
context = state.get("instrument_context")
|
||||||
|
if isinstance(context, str) and context.strip():
|
||||||
|
return context
|
||||||
|
return build_instrument_context(
|
||||||
|
str(state["company_of_interest"]),
|
||||||
|
state.get("asset_type", "stock"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_msg_delete():
|
def create_msg_delete():
|
||||||
def delete_messages(state):
|
def delete_messages(state):
|
||||||
"""Clear messages and add placeholder for Anthropic compatibility"""
|
"""Clear messages and add a context-anchored placeholder.
|
||||||
messages = state["messages"]
|
|
||||||
|
|
||||||
# Remove all messages
|
The placeholder must not be a bare ``"Continue"``: some
|
||||||
|
OpenAI-compatible providers interpret that literally as the user task
|
||||||
|
and produce output about the word "continue" instead of analysing the
|
||||||
|
instrument (#888). Anchoring it to the resolved instrument context and
|
||||||
|
date keeps the next analyst on-task even if the provider treats the
|
||||||
|
placeholder as a standalone request.
|
||||||
|
"""
|
||||||
|
messages = state["messages"]
|
||||||
removal_operations = [RemoveMessage(id=m.id) for m in messages]
|
removal_operations = [RemoveMessage(id=m.id) for m in messages]
|
||||||
|
|
||||||
# Add a minimal placeholder message
|
instrument_context = get_instrument_context_from_state(state)
|
||||||
placeholder = HumanMessage(content="Continue")
|
trade_date = state.get("trade_date", "the requested date")
|
||||||
|
placeholder = HumanMessage(
|
||||||
|
content=(
|
||||||
|
f"Proceed with your assigned analysis for this workflow. "
|
||||||
|
f"{instrument_context} The analysis date is {trade_date}."
|
||||||
|
)
|
||||||
|
)
|
||||||
return {"messages": removal_operations + [placeholder]}
|
return {"messages": removal_operations + [placeholder]}
|
||||||
|
|
||||||
return delete_messages
|
return delete_messages
|
||||||
|
|||||||
@@ -21,12 +21,21 @@ class Propagator:
|
|||||||
trade_date: str,
|
trade_date: str,
|
||||||
asset_type: str = "stock",
|
asset_type: str = "stock",
|
||||||
past_context: str = "",
|
past_context: str = "",
|
||||||
|
instrument_context: str = "",
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Create the initial state for the agent graph."""
|
"""Create the initial state for the agent graph.
|
||||||
|
|
||||||
|
``instrument_context`` is the deterministic ticker-identity string
|
||||||
|
resolved once at run start (see
|
||||||
|
``TradingAgentsGraph.resolve_instrument_context``). When empty, agents
|
||||||
|
fall back to ticker-only context via
|
||||||
|
``get_instrument_context_from_state``.
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
"messages": [("human", company_name)],
|
"messages": [("human", company_name)],
|
||||||
"company_of_interest": company_name,
|
"company_of_interest": company_name,
|
||||||
"asset_type": asset_type,
|
"asset_type": asset_type,
|
||||||
|
"instrument_context": instrument_context,
|
||||||
"trade_date": str(trade_date),
|
"trade_date": str(trade_date),
|
||||||
"past_context": past_context,
|
"past_context": past_context,
|
||||||
"investment_debate_state": InvestDebateState(
|
"investment_debate_state": InvestDebateState(
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ from tradingagents.dataflows.config import set_config
|
|||||||
|
|
||||||
# Import the new abstract tool methods from agent_utils
|
# Import the new abstract tool methods from agent_utils
|
||||||
from tradingagents.agents.utils.agent_utils import (
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
|
build_instrument_context,
|
||||||
|
resolve_instrument_identity,
|
||||||
get_stock_data,
|
get_stock_data,
|
||||||
get_indicators,
|
get_indicators,
|
||||||
get_fundamentals,
|
get_fundamentals,
|
||||||
@@ -292,6 +294,18 @@ class TradingAgentsGraph:
|
|||||||
if updates:
|
if updates:
|
||||||
self.memory_log.batch_update_with_outcomes(updates)
|
self.memory_log.batch_update_with_outcomes(updates)
|
||||||
|
|
||||||
|
def resolve_instrument_context(self, ticker: str, asset_type: str = "stock") -> str:
|
||||||
|
"""Resolve ticker identity once and return the full instrument context.
|
||||||
|
|
||||||
|
Deterministic yfinance lookup (cached, fail-open) injected into a
|
||||||
|
context string so every agent anchors to the real company instead of
|
||||||
|
hallucinating one from the price chart (#814). Both the propagate()
|
||||||
|
path and the CLI call this so the resolved identity reaches the whole
|
||||||
|
graph regardless of entry point.
|
||||||
|
"""
|
||||||
|
identity = resolve_instrument_identity(ticker)
|
||||||
|
return build_instrument_context(ticker, asset_type, identity)
|
||||||
|
|
||||||
def propagate(self, company_name, trade_date, asset_type: str = "stock"):
|
def propagate(self, company_name, trade_date, asset_type: str = "stock"):
|
||||||
"""Run the trading agents graph for a company on a specific date.
|
"""Run the trading agents graph for a company on a specific date.
|
||||||
|
|
||||||
@@ -335,10 +349,16 @@ class TradingAgentsGraph:
|
|||||||
|
|
||||||
def _run_graph(self, company_name, trade_date, asset_type: str = "stock"):
|
def _run_graph(self, company_name, trade_date, asset_type: str = "stock"):
|
||||||
"""Execute the graph and write the resulting state to disk and memory log."""
|
"""Execute the graph and write the resulting state to disk and memory log."""
|
||||||
# Initialize state — inject memory log context for PM.
|
# Initialize state — inject memory log context for PM and the
|
||||||
|
# deterministically resolved instrument identity for all agents.
|
||||||
past_context = self.memory_log.get_past_context(company_name)
|
past_context = self.memory_log.get_past_context(company_name)
|
||||||
|
instrument_context = self.resolve_instrument_context(company_name, asset_type)
|
||||||
init_agent_state = self.propagator.create_initial_state(
|
init_agent_state = self.propagator.create_initial_state(
|
||||||
company_name, trade_date, asset_type=asset_type, past_context=past_context
|
company_name,
|
||||||
|
trade_date,
|
||||||
|
asset_type=asset_type,
|
||||||
|
past_context=past_context,
|
||||||
|
instrument_context=instrument_context,
|
||||||
)
|
)
|
||||||
args = self.propagator.get_graph_args()
|
args = self.propagator.get_graph_args()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user