mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-03 16:12:44 +02:00
feat(sentiment): search crypto communities on Reddit for a crypto ticker (#1461)
- a crypto pair is searched in r/CryptoCurrency and r/CryptoMarkets, plus r/Bitcoin, r/ethereum or r/solana for those coins, still in one request - the Sentiment Analyst's prompt names the subreddits actually searched
This commit is contained in:
@@ -311,3 +311,25 @@ def test_an_unavailable_screen_keeps_every_post_and_says_so():
|
||||
screened = reddit.fetch_reddit_posts("NVDA", subreddits=("a", "b"), screen=unavailable)
|
||||
plain = reddit.fetch_reddit_posts("NVDA", subreddits=("a", "b"))
|
||||
assert screened == "<Jev screening unavailable (HTTP 529); posts are unscreened>\n\n" + plain
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("ticker, expected", [
|
||||
("NVDA", ("wallstreetbets", "stocks", "investing")),
|
||||
("BTC-USD", ("Bitcoin", "CryptoCurrency", "CryptoMarkets")),
|
||||
("ETH-USD", ("ethereum", "CryptoCurrency", "CryptoMarkets")),
|
||||
("SOL-USD", ("solana", "CryptoCurrency", "CryptoMarkets")),
|
||||
("DOGE-USD", ("CryptoCurrency", "CryptoMarkets")),
|
||||
])
|
||||
def test_a_crypto_ticker_is_searched_in_crypto_communities(ticker, expected):
|
||||
assert reddit.subreddits_for(ticker) == expected
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_crypto_pair_is_fetched_from_its_communities_in_one_request():
|
||||
with patch.object(reddit, "_fetch_subreddit_rss", return_value=[]) as fetch:
|
||||
out = reddit.fetch_reddit_posts("ETH-USD")
|
||||
fetch.assert_called_once()
|
||||
query, subs = fetch.call_args.args[:2]
|
||||
assert (query, subs) == ("ETH", "ethereum+CryptoCurrency+CryptoMarkets")
|
||||
assert "r/wallstreetbets" not in out
|
||||
|
||||
@@ -146,3 +146,23 @@ def test_constraint_text_is_unambiguous():
|
||||
# No template braces: it is embedded in ChatPromptTemplate strings, where
|
||||
# braces would be parsed as input variables.
|
||||
assert "{" not in NO_EXTERNAL_TOOLS and "}" not in NO_EXTERNAL_TOOLS
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_the_sentiment_prompt_names_the_subreddits_searched(monkeypatch):
|
||||
from tradingagents.agents.schemas import SentimentBand, SentimentReport
|
||||
|
||||
monkeypatch.setattr(sentiment, "fetch_stocktwits_messages", lambda *a, **k: "st")
|
||||
monkeypatch.setattr(sentiment, "fetch_reddit_posts", lambda *a, **k: "rd")
|
||||
monkeypatch.setattr(sentiment.get_news, "func", lambda *a, **k: "news", raising=False)
|
||||
captured = {}
|
||||
llm = _capturing_llm(captured, SentimentReport(
|
||||
overall_band=SentimentBand.NEUTRAL, overall_score=5.0, confidence="low", narrative="n",
|
||||
))
|
||||
sentiment.create_sentiment_analyst(llm)({
|
||||
"company_of_interest": "BTC-USD", "trade_date": "2026-01-15",
|
||||
"asset_type": "crypto", "messages": [],
|
||||
})
|
||||
text = _prompt_text(captured["prompt"])
|
||||
assert "r/Bitcoin, r/CryptoCurrency, r/CryptoMarkets" in text
|
||||
assert "wallstreetbets" not in text
|
||||
|
||||
@@ -5,7 +5,7 @@ prompt, so the model reports on data it was given rather than inventing posts:
|
||||
|
||||
1. News headlines: Yahoo Finance
|
||||
2. StockTwits messages: the cashtag stream, with Bullish/Bearish tags
|
||||
3. Reddit posts: r/wallstreetbets, r/stocks, r/investing
|
||||
3. Reddit posts: r/wallstreetbets, r/stocks, r/investing, or crypto communities for a crypto pair
|
||||
|
||||
Each source is trimmed to the analysis window. With a TypeSafe key, the social
|
||||
posts are screened by Jev first (see post_screen). These feeds serve recent items
|
||||
@@ -31,7 +31,12 @@ from tradingagents.agents.structured import (
|
||||
invoke_structured_or_freetext,
|
||||
)
|
||||
from tradingagents.agents.tools import get_news
|
||||
from tradingagents.dataflows.vendors.reddit import fetch_reddit_posts
|
||||
from tradingagents.dataflows.vendors.reddit import (
|
||||
CRYPTO_SUBREDDITS,
|
||||
DEFAULT_SUBREDDITS,
|
||||
fetch_reddit_posts,
|
||||
subreddits_for,
|
||||
)
|
||||
from tradingagents.dataflows.vendors.stocktwits import fetch_stocktwits_messages
|
||||
|
||||
|
||||
@@ -65,7 +70,10 @@ def create_sentiment_analyst(llm):
|
||||
stocktwits_block = fetch_stocktwits_messages(
|
||||
ticker, limit=30, start_date=start_date, end_date=end_date, screen=screen
|
||||
)
|
||||
reddit_block = fetch_reddit_posts(ticker, start_date=start_date, end_date=end_date, screen=screen)
|
||||
subreddits = subreddits_for(ticker)
|
||||
reddit_block = fetch_reddit_posts(
|
||||
ticker, subreddits, start_date=start_date, end_date=end_date, screen=screen
|
||||
)
|
||||
|
||||
system_message = _build_system_message(
|
||||
ticker=ticker,
|
||||
@@ -74,6 +82,7 @@ def create_sentiment_analyst(llm):
|
||||
news_block=news_block,
|
||||
stocktwits_block=stocktwits_block,
|
||||
reddit_block=reddit_block,
|
||||
subreddits=subreddits,
|
||||
)
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
@@ -126,8 +135,15 @@ def _build_system_message(
|
||||
news_block: str,
|
||||
stocktwits_block: str,
|
||||
reddit_block: str,
|
||||
subreddits: tuple[str, ...] = DEFAULT_SUBREDDITS,
|
||||
) -> str:
|
||||
"""Assemble the sentiment-analyst system message with structured data blocks."""
|
||||
if subreddits == DEFAULT_SUBREDDITS:
|
||||
character = "r/wallstreetbets is often contrarian/exuberant; r/stocks more measured; r/investing longer-term"
|
||||
elif len(subreddits) > len(CRYPTO_SUBREDDITS):
|
||||
character = f"r/{subreddits[0]} leans toward the coin's holders; r/CryptoCurrency and r/CryptoMarkets are broader"
|
||||
else:
|
||||
character = "r/CryptoCurrency and r/CryptoMarkets are broad crypto communities"
|
||||
return f"""You are a financial market sentiment analyst. Your task is to produce a comprehensive sentiment report for {ticker} covering the period from {start_date} to {end_date}, drawing on three complementary data sources that have already been collected for you.
|
||||
|
||||
## Data sources (pre-fetched, in this prompt)
|
||||
@@ -146,8 +162,8 @@ Fast-moving signal. Each message carries a user-labeled sentiment tag (Bullish /
|
||||
{stocktwits_block}
|
||||
<end_of_stocktwits>
|
||||
|
||||
### Reddit posts — r/wallstreetbets, r/stocks, r/investing (past 7 days)
|
||||
Community discussion, without vote or comment counts. Subreddit character matters (r/wallstreetbets is often contrarian/exuberant; r/stocks more measured; r/investing longer-term).
|
||||
### Reddit posts — {", ".join(f"r/{sub}" for sub in subreddits)} (past 7 days)
|
||||
Community discussion, without vote or comment counts. Subreddit character matters ({character}).
|
||||
|
||||
<start_of_reddit>
|
||||
{reddit_block}
|
||||
|
||||
+18
-3
@@ -78,6 +78,20 @@ _ATOM_NS = {"atom": "http://www.w3.org/2005/Atom"}
|
||||
# investing trend more measured. Caller can override.
|
||||
DEFAULT_SUBREDDITS = ("wallstreetbets", "stocks", "investing")
|
||||
|
||||
# Crypto is discussed in crypto communities, where a coin's own subreddit carries
|
||||
# most of it; the stock subreddits above barely mention it.
|
||||
CRYPTO_SUBREDDITS = ("CryptoCurrency", "CryptoMarkets")
|
||||
_COIN_SUBREDDITS = {"BTC": "Bitcoin", "ETH": "ethereum", "SOL": "solana"}
|
||||
|
||||
|
||||
def subreddits_for(ticker: str) -> tuple[str, ...]:
|
||||
"""The subreddits searched for ``ticker``: crypto communities for a crypto pair."""
|
||||
base = crypto_base(ticker)
|
||||
if not base:
|
||||
return DEFAULT_SUBREDDITS
|
||||
coin = _COIN_SUBREDDITS.get(base)
|
||||
return ((coin,) if coin else ()) + CRYPTO_SUBREDDITS
|
||||
|
||||
# Reddit's maximum page size. A week of posts for a ticker across the default
|
||||
# subreddits fits well inside one page, which keeps a high-volume subreddit from
|
||||
# crowding the others out of a combined search.
|
||||
@@ -233,7 +247,7 @@ def _fetch_subreddit_rss(
|
||||
|
||||
def fetch_reddit_posts(
|
||||
ticker: str,
|
||||
subreddits: Iterable[str] = DEFAULT_SUBREDDITS,
|
||||
subreddits: Iterable[str] | None = None,
|
||||
*,
|
||||
limit_per_sub: int = 5,
|
||||
timeout: float = 10.0,
|
||||
@@ -242,7 +256,8 @@ def fetch_reddit_posts(
|
||||
screen=None,
|
||||
) -> str:
|
||||
"""Fetch recent Reddit posts mentioning ``ticker`` across finance
|
||||
subreddits and return them as a formatted plaintext block.
|
||||
subreddits (by default ``subreddits_for(ticker)``) and return them as a
|
||||
formatted plaintext block.
|
||||
|
||||
All subreddits are searched in one combined feed (``r/a+b+c``): anonymous
|
||||
RSS allows about one request per minute per IP, so a request per subreddit
|
||||
@@ -257,10 +272,10 @@ def fetch_reddit_posts(
|
||||
flag per post and a note line that heads the block. It runs before the
|
||||
per-subreddit cut, so the posts it keeps fill the slots.
|
||||
"""
|
||||
subreddits = list(subreddits or subreddits_for(ticker))
|
||||
# Crypto reaches us as a Yahoo pair (BTC-USD); search Reddit for the base
|
||||
# ("BTC") so the query actually matches discussion instead of near-nothing.
|
||||
ticker = crypto_base(ticker) or ticker
|
||||
subreddits = list(subreddits)
|
||||
label = ", ".join(f"r/{s}" for s in subreddits)
|
||||
fetched = _fetch_subreddit_rss(ticker, "+".join(subreddits), _FEED_PAGE, timeout)
|
||||
if fetched is None:
|
||||
|
||||
Reference in New Issue
Block a user