From 1a06904b5f10553bc1821ec27d90d7e3d4614b9a Mon Sep 17 00:00:00 2001 From: Alex Schneider Date: Wed, 25 Mar 2026 20:23:09 +0100 Subject: [PATCH 1/3] Add optional Adanos social sentiment vendor --- .env.example | 4 + README.md | 24 +++ tests/test_social_sentiment_tools.py | 85 ++++++++++ .../agents/analysts/social_media_analyst.py | 5 +- tradingagents/agents/utils/agent_utils.py | 3 + .../agents/utils/social_data_tools.py | 18 +++ tradingagents/dataflows/adanos_social.py | 151 ++++++++++++++++++ tradingagents/dataflows/interface.py | 14 +- tradingagents/default_config.py | 1 + tradingagents/graph/trading_graph.py | 6 +- 10 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 tests/test_social_sentiment_tools.py create mode 100644 tradingagents/agents/utils/social_data_tools.py create mode 100644 tradingagents/dataflows/adanos_social.py diff --git a/.env.example b/.env.example index 1328b838..f25df0c2 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,7 @@ GOOGLE_API_KEY= ANTHROPIC_API_KEY= XAI_API_KEY= OPENROUTER_API_KEY= + +# Optional social sentiment provider +ADANOS_API_KEY= +ADANOS_BASE_URL=https://api.adanos.org diff --git a/README.md b/README.md index 4c4856d1..df142658 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ export ANTHROPIC_API_KEY=... # Anthropic (Claude) export XAI_API_KEY=... # xAI (Grok) export OPENROUTER_API_KEY=... # OpenRouter export ALPHA_VANTAGE_API_KEY=... # Alpha Vantage +export ADANOS_API_KEY=... # Optional: Adanos social sentiment ``` For local models, configure Ollama with `llm_provider: "ollama"` in your config. @@ -137,6 +138,27 @@ Alternatively, copy `.env.example` to `.env` and fill in your keys: cp .env.example .env ``` +### Optional Social Sentiment Provider + +The sentiment analyst can optionally use Adanos as a structured social sentiment provider for Reddit, News, X, and Polymarket coverage. Configure it with: + +```bash +export ADANOS_API_KEY=... +export ADANOS_BASE_URL=https://api.adanos.org +``` + +To enable the Adanos-backed social tool in code, set the `social_data` vendor to `adanos`: + +```python +from tradingagents.default_config import DEFAULT_CONFIG + +config = DEFAULT_CONFIG.copy() +config["data_vendors"] = DEFAULT_CONFIG["data_vendors"].copy() +config["data_vendors"]["social_data"] = "adanos" +``` + +The Adanos integration is currently best suited for letter-based tickers such as `NVDA` or `TSLA`. Exchange-qualified or numeric symbols may still rely on the framework's existing news tools for coverage. + ### CLI Usage Launch the interactive CLI: @@ -192,6 +214,8 @@ config["llm_provider"] = "openai" # openai, google, anthropic, xai, openr config["deep_think_llm"] = "gpt-5.2" # Model for complex reasoning config["quick_think_llm"] = "gpt-5-mini" # Model for quick tasks config["max_debate_rounds"] = 2 +config["data_vendors"] = DEFAULT_CONFIG["data_vendors"].copy() +config["data_vendors"]["social_data"] = "adanos" # Optional: structured social sentiment ta = TradingAgentsGraph(debug=True, config=config) _, decision = ta.propagate("NVDA", "2026-01-15") diff --git a/tests/test_social_sentiment_tools.py b/tests/test_social_sentiment_tools.py new file mode 100644 index 00000000..22d96dae --- /dev/null +++ b/tests/test_social_sentiment_tools.py @@ -0,0 +1,85 @@ +import unittest +from unittest.mock import patch + +from tradingagents.agents.utils.social_data_tools import get_social_sentiment as social_tool +from tradingagents.dataflows import adanos_social, interface + + +class SocialSentimentToolTests(unittest.TestCase): + def test_route_to_vendor_supports_social_data(self): + with patch.dict( + interface.VENDOR_METHODS["get_social_sentiment"], + {"adanos": lambda ticker, curr_date, look_back_days: f"{ticker}|{curr_date}|{look_back_days}"}, + clear=True, + ): + result = interface.route_to_vendor("get_social_sentiment", "NVDA", "2026-01-15", 5) + + self.assertEqual(result, "NVDA|2026-01-15|5") + + def test_social_tool_routes_to_vendor(self): + with patch("tradingagents.agents.utils.social_data_tools.route_to_vendor", return_value="ok") as mock_route: + result = social_tool.invoke( + {"ticker": "NVDA", "curr_date": "2026-01-15", "look_back_days": 7} + ) + + self.assertEqual(result, "ok") + mock_route.assert_called_once_with("get_social_sentiment", "NVDA", "2026-01-15", 7) + + def test_adanos_social_formats_multiple_sources(self): + payloads = { + "/reddit/stocks/v1/stock/NVDA": { + "company_name": "NVIDIA Corporation", + "buzz_score": 72.4, + "sentiment_score": 0.31, + "bullish_pct": 61, + "bearish_pct": 18, + "trend": "rising", + "total_mentions": 142, + "unique_posts": 48, + }, + "/news/stocks/v1/stock/NVDA": { + "source_count": 23, + "sentiment_score": 0.22, + "bullish_pct": 54, + "bearish_pct": 16, + }, + "/x/stocks/v1/stock/NVDA": { + "unique_tweets": 305, + "sentiment_score": 0.27, + "trend": "rising", + }, + "/polymarket/stocks/v1/stock/NVDA": { + "trade_count": 91, + "market_count": 4, + "total_liquidity": 120000.0, + "sentiment_score": 0.14, + }, + } + + def fake_request(path, *, api_key, base_url, params): + self.assertEqual(api_key, "test-key") + self.assertEqual(base_url, "https://api.adanos.org") + self.assertEqual(params, {"days": 7}) + return payloads[path] + + with patch.dict("os.environ", {"ADANOS_API_KEY": "test-key"}, clear=False): + with patch("tradingagents.dataflows.adanos_social._request_json", side_effect=fake_request): + result = adanos_social.get_social_sentiment("NVDA", "2026-01-15", 7) + + self.assertIn("# NVDA Adanos social sentiment", result) + self.assertIn("## Reddit", result) + self.assertIn("## News", result) + self.assertIn("## X/Twitter", result) + self.assertIn("## Polymarket", result) + self.assertIn("Buzz score: 72.4", result) + self.assertIn("Trades: 91", result) + + def test_adanos_social_requires_api_key(self): + with patch.dict("os.environ", {}, clear=True): + result = adanos_social.get_social_sentiment("NVDA", "2026-01-15", 7) + + self.assertIn("ADANOS_API_KEY", result) + + +if __name__ == "__main__": + unittest.main() diff --git a/tradingagents/agents/analysts/social_media_analyst.py b/tradingagents/agents/analysts/social_media_analyst.py index 43df2258..f3e6dc9b 100644 --- a/tradingagents/agents/analysts/social_media_analyst.py +++ b/tradingagents/agents/analysts/social_media_analyst.py @@ -1,7 +1,7 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder import time import json -from tradingagents.agents.utils.agent_utils import build_instrument_context, get_news +from tradingagents.agents.utils.agent_utils import build_instrument_context, get_news, get_social_sentiment from tradingagents.dataflows.config import get_config @@ -11,11 +11,12 @@ def create_social_media_analyst(llm): instrument_context = build_instrument_context(state["company_of_interest"]) tools = [ + get_social_sentiment, get_news, ] system_message = ( - "You are a social media and company specific news researcher/analyst tasked with analyzing social media posts, recent company news, and public sentiment for a specific company over the past week. You will be given a company's name your objective is to write a comprehensive long report detailing your analysis, insights, and implications for traders and investors on this company's current state after looking at social media and what people are saying about that company, analyzing sentiment data of what people feel each day about the company, and looking at recent company news. Use the get_news(query, start_date, end_date) tool to search for company-specific news and social media discussions. Try to look at all sources possible from social media to sentiment to news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions." + "You are a social media and company specific news researcher/analyst tasked with analyzing social media posts, recent company news, and public sentiment for a specific company over the past week. You will be given a company's name your objective is to write a comprehensive long report detailing your analysis, insights, and implications for traders and investors on this company's current state after looking at social media and what people are saying about that company, analyzing sentiment data of what people feel each day about the company, and looking at recent company news. Use the get_social_sentiment(ticker, curr_date, look_back_days) tool for structured multi-source sentiment data when it is available, and use the get_news(query, start_date, end_date) tool to search for company-specific news and public discussion context. Try to look at all sources possible from social media to sentiment to news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions." + """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read.""" ) diff --git a/tradingagents/agents/utils/agent_utils.py b/tradingagents/agents/utils/agent_utils.py index e4abc4cd..f42091e4 100644 --- a/tradingagents/agents/utils/agent_utils.py +++ b/tradingagents/agents/utils/agent_utils.py @@ -18,6 +18,9 @@ from tradingagents.agents.utils.news_data_tools import ( get_insider_transactions, get_global_news ) +from tradingagents.agents.utils.social_data_tools import ( + get_social_sentiment, +) def build_instrument_context(ticker: str) -> str: diff --git a/tradingagents/agents/utils/social_data_tools.py b/tradingagents/agents/utils/social_data_tools.py new file mode 100644 index 00000000..4fb32bf3 --- /dev/null +++ b/tradingagents/agents/utils/social_data_tools.py @@ -0,0 +1,18 @@ +from typing import Annotated + +from langchain_core.tools import tool + +from tradingagents.dataflows.interface import route_to_vendor + + +@tool +def get_social_sentiment( + ticker: Annotated[str, "Ticker symbol"], + curr_date: Annotated[str, "Current date in yyyy-mm-dd format"], + look_back_days: Annotated[int, "Number of days to look back"] = 7, +) -> str: + """ + Retrieve structured social and public sentiment for a ticker. + Uses the configured social_data vendor. + """ + return route_to_vendor("get_social_sentiment", ticker, curr_date, look_back_days) diff --git a/tradingagents/dataflows/adanos_social.py b/tradingagents/dataflows/adanos_social.py new file mode 100644 index 00000000..42ea68a1 --- /dev/null +++ b/tradingagents/dataflows/adanos_social.py @@ -0,0 +1,151 @@ +import os +import re +from typing import Any + +import requests + + +ADANOS_BASE_URL = "https://api.adanos.org" +_LETTER_TICKER_RE = re.compile(r"^[A-Z]{1,10}$") +_REDDIT_NEWS_TICKER_RE = re.compile(r"^[A-Z][A-Z0-9]{0,9}(?:\.[A-Z])?$") + + +def _normalize_ticker(ticker: str) -> str: + return ticker.strip().upper().lstrip("$") + + +def _request_json(path: str, *, api_key: str, base_url: str, params: dict[str, Any] | None = None) -> dict[str, Any]: + response = requests.get( + f"{base_url.rstrip('/')}{path}", + headers={"X-API-Key": api_key, "Accept": "application/json"}, + params=params or {}, + timeout=float(os.getenv("ADANOS_TIMEOUT", "20")), + ) + response.raise_for_status() + return response.json() + + +def _iter_source_requests(ticker: str) -> list[tuple[str, str]]: + requests_to_make: list[tuple[str, str]] = [] + + if _REDDIT_NEWS_TICKER_RE.fullmatch(ticker): + requests_to_make.extend( + [ + ("Reddit", f"/reddit/stocks/v1/stock/{ticker}"), + ("News", f"/news/stocks/v1/stock/{ticker}"), + ] + ) + + if _LETTER_TICKER_RE.fullmatch(ticker): + requests_to_make.extend( + [ + ("X/Twitter", f"/x/stocks/v1/stock/{ticker}"), + ("Polymarket", f"/polymarket/stocks/v1/stock/{ticker}"), + ] + ) + + return requests_to_make + + +def _format_source_section(source_name: str, payload: dict[str, Any]) -> str: + lines = [f"## {source_name}"] + + company_name = payload.get("company_name") + if company_name: + lines.append(f"- Company: {company_name}") + + if payload.get("buzz_score") is not None: + lines.append(f"- Buzz score: {payload['buzz_score']}") + if payload.get("sentiment_score") is not None: + lines.append(f"- Sentiment score: {payload['sentiment_score']}") + if payload.get("bullish_pct") is not None or payload.get("bearish_pct") is not None: + lines.append( + f"- Bullish/Bearish: {payload.get('bullish_pct', 'n/a')}% / {payload.get('bearish_pct', 'n/a')}%" + ) + if payload.get("trend"): + lines.append(f"- Trend: {payload['trend']}") + + for key, label in ( + ("total_mentions", "Mentions"), + ("unique_posts", "Unique posts"), + ("subreddit_count", "Subreddits"), + ("source_count", "Sources"), + ("unique_tweets", "Unique tweets"), + ("market_count", "Active markets"), + ("trade_count", "Trades"), + ("total_liquidity", "Total liquidity"), + ): + value = payload.get(key) + if value is not None: + lines.append(f"- {label}: {value}") + + explanation = payload.get("explanation") + if explanation: + lines.append(f"- Explanation: {explanation}") + + return "\n".join(lines) + + +def get_social_sentiment(ticker: str, curr_date: str, look_back_days: int = 7) -> str: + """Retrieve multi-source social sentiment from Adanos when available.""" + api_key = os.getenv("ADANOS_API_KEY") + if not api_key: + return ( + "Adanos social sentiment is unavailable because ADANOS_API_KEY is not set. " + "Configure ADANOS_API_KEY to enable Reddit, X/Twitter, News, and Polymarket sentiment lookups." + ) + + normalized_ticker = _normalize_ticker(ticker) + source_requests = _iter_source_requests(normalized_ticker) + if not source_requests: + return ( + f"Adanos does not currently support the exact ticker format `{ticker}` for per-symbol sentiment lookup. " + "Exchange-qualified or numeric symbols should fall back to the framework's existing news tools." + ) + + base_url = os.getenv("ADANOS_BASE_URL", ADANOS_BASE_URL) + days = max(1, int(look_back_days or 7)) + + sections: list[str] = [] + notes: list[str] = [] + + for source_name, path in source_requests: + try: + payload = _request_json(path, api_key=api_key, base_url=base_url, params={"days": days}) + except requests.HTTPError as exc: + status_code = exc.response.status_code if exc.response is not None else None + if status_code == 404: + notes.append(f"- {source_name}: no coverage for {normalized_ticker}") + continue + if status_code in {401, 403}: + return "Adanos social sentiment request failed due to invalid API credentials." + notes.append(f"- {source_name}: request failed with HTTP {status_code}") + continue + except requests.RequestException as exc: + notes.append(f"- {source_name}: request failed ({exc.__class__.__name__})") + continue + + sections.append(_format_source_section(source_name, payload)) + + if not sections: + note_block = "\n".join(notes) if notes else "- No compatible Adanos sources were available." + return ( + f"# {normalized_ticker} Adanos social sentiment\n\n" + f"Analysis date: {curr_date}\n" + f"Lookback window: {days} days\n\n" + "No Adanos sentiment sources returned usable data.\n" + f"{note_block}" + ) + + output = [ + f"# {normalized_ticker} Adanos social sentiment", + "", + f"Analysis date: {curr_date}", + f"Lookback window: {days} days", + "", + *sections, + ] + if notes: + output.extend(["", "## Coverage notes", *notes]) + + return "\n".join(output) diff --git a/tradingagents/dataflows/interface.py b/tradingagents/dataflows/interface.py index 0caf4b68..4540931d 100644 --- a/tradingagents/dataflows/interface.py +++ b/tradingagents/dataflows/interface.py @@ -23,6 +23,7 @@ from .alpha_vantage import ( get_global_news as get_alpha_vantage_global_news, ) from .alpha_vantage_common import AlphaVantageRateLimitError +from .adanos_social import get_social_sentiment as get_adanos_social_sentiment # Configuration and routing logic from .config import get_config @@ -57,12 +58,19 @@ TOOLS_CATEGORIES = { "get_global_news", "get_insider_transactions", ] + }, + "social_data": { + "description": "Structured social and public sentiment data", + "tools": [ + "get_social_sentiment", + ], } } VENDOR_LIST = [ "yfinance", "alpha_vantage", + "adanos", ] # Mapping of methods to their vendor-specific implementations @@ -107,6 +115,10 @@ VENDOR_METHODS = { "alpha_vantage": get_alpha_vantage_insider_transactions, "yfinance": get_yfinance_insider_transactions, }, + # social_data + "get_social_sentiment": { + "adanos": get_adanos_social_sentiment, + }, } def get_category_for_method(method: str) -> str: @@ -159,4 +171,4 @@ def route_to_vendor(method: str, *args, **kwargs): except AlphaVantageRateLimitError: continue # Only rate limits trigger fallback - raise RuntimeError(f"No available vendor for '{method}'") \ No newline at end of file + raise RuntimeError(f"No available vendor for '{method}'") diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py index 898e1e1e..fbedbe1d 100644 --- a/tradingagents/default_config.py +++ b/tradingagents/default_config.py @@ -27,6 +27,7 @@ DEFAULT_CONFIG = { "technical_indicators": "yfinance", # Options: alpha_vantage, yfinance "fundamental_data": "yfinance", # Options: alpha_vantage, yfinance "news_data": "yfinance", # Options: alpha_vantage, yfinance + "social_data": "default", # Options: default, adanos }, # Tool-level configuration (takes precedence over category-level) "tool_vendors": { diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index c8cd7492..afc4f19c 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -30,7 +30,8 @@ from tradingagents.agents.utils.agent_utils import ( get_income_statement, get_news, get_insider_transactions, - get_global_news + get_global_news, + get_social_sentiment, ) from .conditional_logic import ConditionalLogic @@ -168,7 +169,8 @@ class TradingAgentsGraph: ), "social": ToolNode( [ - # News tools for social media analysis + # Dedicated social sentiment first, then news context + get_social_sentiment, get_news, ] ), From c3bce5f360ae0f9d383601f31fbc3f864fe0b49f Mon Sep 17 00:00:00 2001 From: Alex Schneider Date: Wed, 25 Mar 2026 20:28:29 +0100 Subject: [PATCH 2/3] Gate Adanos social sentiment behind explicit config --- tests/test_social_sentiment_tools.py | 18 ++++++++++------ .../agents/analysts/social_media_analyst.py | 21 +++++++++++++------ tradingagents/dataflows/interface.py | 15 +++++++++++++ tradingagents/default_config.py | 2 +- tradingagents/graph/trading_graph.py | 13 ++++++------ 5 files changed, 49 insertions(+), 20 deletions(-) diff --git a/tests/test_social_sentiment_tools.py b/tests/test_social_sentiment_tools.py index 22d96dae..aa56ff8a 100644 --- a/tests/test_social_sentiment_tools.py +++ b/tests/test_social_sentiment_tools.py @@ -7,15 +7,21 @@ from tradingagents.dataflows import adanos_social, interface class SocialSentimentToolTests(unittest.TestCase): def test_route_to_vendor_supports_social_data(self): - with patch.dict( - interface.VENDOR_METHODS["get_social_sentiment"], - {"adanos": lambda ticker, curr_date, look_back_days: f"{ticker}|{curr_date}|{look_back_days}"}, - clear=True, - ): - result = interface.route_to_vendor("get_social_sentiment", "NVDA", "2026-01-15", 5) + with patch("tradingagents.dataflows.interface.get_vendor", return_value="adanos"): + with patch.dict( + interface.VENDOR_METHODS["get_social_sentiment"], + {"adanos": lambda ticker, curr_date, look_back_days: f"{ticker}|{curr_date}|{look_back_days}"}, + clear=True, + ): + result = interface.route_to_vendor("get_social_sentiment", "NVDA", "2026-01-15", 5) self.assertEqual(result, "NVDA|2026-01-15|5") + def test_route_to_vendor_requires_explicit_social_vendor(self): + with patch("tradingagents.dataflows.interface.get_vendor", return_value="default"): + with self.assertRaises(RuntimeError): + interface.route_to_vendor("get_social_sentiment", "NVDA", "2026-01-15", 5) + def test_social_tool_routes_to_vendor(self): with patch("tradingagents.agents.utils.social_data_tools.route_to_vendor", return_value="ok") as mock_route: result = social_tool.invoke( diff --git a/tradingagents/agents/analysts/social_media_analyst.py b/tradingagents/agents/analysts/social_media_analyst.py index f3e6dc9b..0e91a519 100644 --- a/tradingagents/agents/analysts/social_media_analyst.py +++ b/tradingagents/agents/analysts/social_media_analyst.py @@ -2,7 +2,7 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder import time import json from tradingagents.agents.utils.agent_utils import build_instrument_context, get_news, get_social_sentiment -from tradingagents.dataflows.config import get_config +from tradingagents.dataflows.interface import is_tool_configured def create_social_media_analyst(llm): @@ -10,13 +10,22 @@ def create_social_media_analyst(llm): current_date = state["trade_date"] instrument_context = build_instrument_context(state["company_of_interest"]) - tools = [ - get_social_sentiment, - get_news, - ] + social_tool_enabled = is_tool_configured("get_social_sentiment") + tools = [get_news] + social_tool_guidance = ( + "Use the get_news(query, start_date, end_date) tool to search for company-specific news and public discussion context." + ) + if social_tool_enabled: + tools.insert(0, get_social_sentiment) + social_tool_guidance = ( + "Use the get_social_sentiment(ticker, curr_date, look_back_days) tool for structured multi-source sentiment data when it is available, " + "and use the get_news(query, start_date, end_date) tool to search for company-specific news and public discussion context." + ) 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_social_sentiment(ticker, curr_date, look_back_days) tool for structured multi-source sentiment data when it is available, and use the get_news(query, start_date, end_date) tool to search for company-specific news and public discussion context. 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." + "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. " + + social_tool_guidance + + " Try to look at all sources possible from social media to sentiment to news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions." + """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read.""" ) diff --git a/tradingagents/dataflows/interface.py b/tradingagents/dataflows/interface.py index 4540931d..4d013224 100644 --- a/tradingagents/dataflows/interface.py +++ b/tradingagents/dataflows/interface.py @@ -143,10 +143,25 @@ def get_vendor(category: str, method: str = None) -> str: # Fall back to category-level configuration return config.get("data_vendors", {}).get(category, "default") +def is_tool_configured(method: str) -> bool: + """Return True when a tool has an explicit non-default vendor configured.""" + category = get_category_for_method(method) + vendor_config = get_vendor(category, method) + configured_vendors = [ + vendor.strip().lower() + for vendor in str(vendor_config or "").split(",") + if vendor.strip() + ] + return any(vendor not in {"default", "none", "disabled"} for vendor in configured_vendors) + def route_to_vendor(method: str, *args, **kwargs): """Route method calls to appropriate vendor implementation with fallback support.""" category = get_category_for_method(method) vendor_config = get_vendor(category, method) + + if method == "get_social_sentiment" and not is_tool_configured(method): + raise RuntimeError("No configured vendor for 'get_social_sentiment'") + primary_vendors = [v.strip() for v in vendor_config.split(',')] if method not in VENDOR_METHODS: diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py index fbedbe1d..b6b3b475 100644 --- a/tradingagents/default_config.py +++ b/tradingagents/default_config.py @@ -27,7 +27,7 @@ DEFAULT_CONFIG = { "technical_indicators": "yfinance", # Options: alpha_vantage, yfinance "fundamental_data": "yfinance", # Options: alpha_vantage, yfinance "news_data": "yfinance", # Options: alpha_vantage, yfinance - "social_data": "default", # Options: default, adanos + "social_data": "default", # Options: default (disabled), adanos }, # Tool-level configuration (takes precedence over category-level) "tool_vendors": { diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index afc4f19c..63989bdd 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -19,6 +19,7 @@ from tradingagents.agents.utils.agent_states import ( RiskDebateState, ) from tradingagents.dataflows.config import set_config +from tradingagents.dataflows.interface import is_tool_configured # Import the new abstract tool methods from agent_utils from tradingagents.agents.utils.agent_utils import ( @@ -158,6 +159,10 @@ class TradingAgentsGraph: def _create_tool_nodes(self) -> Dict[str, ToolNode]: """Create tool nodes for different data sources using abstract methods.""" + social_tools = [get_news] + if is_tool_configured("get_social_sentiment"): + social_tools.insert(0, get_social_sentiment) + return { "market": ToolNode( [ @@ -167,13 +172,7 @@ class TradingAgentsGraph: get_indicators, ] ), - "social": ToolNode( - [ - # Dedicated social sentiment first, then news context - get_social_sentiment, - get_news, - ] - ), + "social": ToolNode(social_tools), "news": ToolNode( [ # News and insider information From bd1aa6949d4c95778bd244981b2a13c44f15b7ea Mon Sep 17 00:00:00 2001 From: Alex Schneider Date: Wed, 25 Mar 2026 20:37:43 +0100 Subject: [PATCH 3/3] Refine Adanos sentiment constants --- tradingagents/dataflows/adanos_social.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tradingagents/dataflows/adanos_social.py b/tradingagents/dataflows/adanos_social.py index 42ea68a1..34b97603 100644 --- a/tradingagents/dataflows/adanos_social.py +++ b/tradingagents/dataflows/adanos_social.py @@ -5,9 +5,11 @@ from typing import Any import requests -ADANOS_BASE_URL = "https://api.adanos.org" -_LETTER_TICKER_RE = re.compile(r"^[A-Z]{1,10}$") -_REDDIT_NEWS_TICKER_RE = re.compile(r"^[A-Z][A-Z0-9]{0,9}(?:\.[A-Z])?$") +ADANOS_DEFAULT_BASE_URL = "https://api.adanos.org" +ADANOS_DEFAULT_TIMEOUT_SECONDS = 20 +DEFAULT_SOCIAL_LOOK_BACK_DAYS = 7 +LETTER_TICKER_REGEX = re.compile(r"^[A-Z]{1,10}$") +REDDIT_NEWS_TICKER_REGEX = re.compile(r"^[A-Z][A-Z0-9]{0,9}(?:\.[A-Z])?$") def _normalize_ticker(ticker: str) -> str: @@ -19,7 +21,7 @@ def _request_json(path: str, *, api_key: str, base_url: str, params: dict[str, A f"{base_url.rstrip('/')}{path}", headers={"X-API-Key": api_key, "Accept": "application/json"}, params=params or {}, - timeout=float(os.getenv("ADANOS_TIMEOUT", "20")), + timeout=float(os.getenv("ADANOS_TIMEOUT", str(ADANOS_DEFAULT_TIMEOUT_SECONDS))), ) response.raise_for_status() return response.json() @@ -28,7 +30,7 @@ def _request_json(path: str, *, api_key: str, base_url: str, params: dict[str, A def _iter_source_requests(ticker: str) -> list[tuple[str, str]]: requests_to_make: list[tuple[str, str]] = [] - if _REDDIT_NEWS_TICKER_RE.fullmatch(ticker): + if REDDIT_NEWS_TICKER_REGEX.fullmatch(ticker): requests_to_make.extend( [ ("Reddit", f"/reddit/stocks/v1/stock/{ticker}"), @@ -36,7 +38,7 @@ def _iter_source_requests(ticker: str) -> list[tuple[str, str]]: ] ) - if _LETTER_TICKER_RE.fullmatch(ticker): + if LETTER_TICKER_REGEX.fullmatch(ticker): requests_to_make.extend( [ ("X/Twitter", f"/x/stocks/v1/stock/{ticker}"), @@ -86,7 +88,11 @@ def _format_source_section(source_name: str, payload: dict[str, Any]) -> str: return "\n".join(lines) -def get_social_sentiment(ticker: str, curr_date: str, look_back_days: int = 7) -> str: +def get_social_sentiment( + ticker: str, + curr_date: str, + look_back_days: int = DEFAULT_SOCIAL_LOOK_BACK_DAYS, +) -> str: """Retrieve multi-source social sentiment from Adanos when available.""" api_key = os.getenv("ADANOS_API_KEY") if not api_key: @@ -103,8 +109,8 @@ def get_social_sentiment(ticker: str, curr_date: str, look_back_days: int = 7) - "Exchange-qualified or numeric symbols should fall back to the framework's existing news tools." ) - base_url = os.getenv("ADANOS_BASE_URL", ADANOS_BASE_URL) - days = max(1, int(look_back_days or 7)) + base_url = os.getenv("ADANOS_BASE_URL", ADANOS_DEFAULT_BASE_URL) + days = max(1, int(look_back_days or DEFAULT_SOCIAL_LOOK_BACK_DAYS)) sections: list[str] = [] notes: list[str] = []