Reddit Stock Analyzer 📊
Introduction
Scrapes recent posts from finance subreddits, works out which stocks each
post is about using
stock-recognizer,
scores their sentiment with
FinTwitBERT,
and ranks what is trending — not just what is loud, but what is getting
louder.
It is built as a library first: every entry point returns plain dataclasses
with to_dict(), so fintwit-web
can serve it straight out of a FastAPI route. See
docs/fintwit-web-integration.md.
Table of Contents 🗂
Key Features 🔑
- Curated subreddit catalogue —
retail,stocks,research,trading,macroandcryptogroups, so you can ask for "everything retail traders shout about" instead of hard-coding names. - Credentials optional — uses authenticated
asyncprawwhen Reddit API keys are configured, and the public JSON endpoints when they are not. - Real ticker recognition —
stock-recognizerhandles cashtags, market-data validation, "DD" the company vs. "DD" the due diligence, and company-name mapping (TSMC→TSM). - WSB-native sentiment — FinTwitBERT-wsb reads "puts printing" the way a trader does, per post and per ticker.
- Trend analytics — two-window momentum, spike z-scores, emerging/fading detection, per-subreddit breakdowns and an hourly mention timeline.
- Async and shareable — concurrent scraping, models loaded once, CPU-bound work pushed off the event loop.
Installation ⚙️
pip install git+https://github.com/StephanAkkerman/reddit-stock-analyzer.git
Optional extras:
pip install "reddit-stock-analyzer[praw]" # authenticated scraping
pip install "reddit-stock-analyzer[sentiment]" # FinTwitBERT post sentiment
[sentiment] is usually redundant: transformers and torch already arrive
with stock-recognizer, so sentiment works out of the box. If they are
missing, every post is labelled neutral and everything else still works.
Credentials are read from the environment or a .env file — see
.env.example.
Usage ⌨️
Library
import asyncio
from reddit_stock_analyzer import RedditTrendService, subreddits_for
async def main():
async with RedditTrendService() as service:
report = await service.trend_report(
subreddits_for("retail", "trading"),
window_hours=24, # the period under analysis
baseline_hours=24, # the period it is compared against
)
for trend in report.tickers[:5]:
print(
f"{trend.symbol:<6} {trend.mentions:>3} mentions "
f"(was {trend.previous_mentions}) "
f"momentum {trend.momentum:+.2f} "
f"{trend.sentiment} ({trend.sentiment_score:+.2f})"
)
print("emerging:", report.emerging)
print("fading:", report.fading)
asyncio.run(main())
A single subreddit snapshot, without the trend maths:
overview = await service.subreddit_overview("wallstreetbets", limit=50)
# {'subreddit': ..., 'overall_mood': 'bullish', 'top_tickers': {'NVDA': 7, ...},
# 'sentiment_breakdown': {...}, 'posts': [...]}
Scraping and analysis are separable — cache a scrape and re-rank it with different parameters for free:
from reddit_stock_analyzer import compute_trends
posts = await service.scrape(["wallstreetbets"], max_age_hours=48)
day = compute_trends(posts, window_hours=24)
morning = compute_trends(posts, window_hours=4, baseline_hours=44)
Command line
python -m reddit_stock_analyzer --category retail --window 12 --top 10
python -m reddit_stock_analyzer -s wallstreetbets -s options --json
python -m reddit_stock_analyzer --no-ai # skip the GLiNER2 model
r/wallstreetbets + r/options
412 posts in the last 12h (of 780 scraped) — mood: bullish (+0.21)
TICKER MENTIONS PREV MOM SPIKE HEAT SENTIMENT
NVDA 41 18 +1.21 +2.84 0.912 bullish (+0.44)
SPY 33 35 -0.06 +2.01 0.604 neutral (+0.03)
...
rising: NVDA, SMCI
emerging: RKLB
fading: AMC
Sentiment 🐂🐻
Posts are classified by
StephanAkkerman/FinTwitBERT-wsb-sentiment
— BERT pre-trained on financial tweets and fine-tuned on WallStreetBets-style
text, so it reads "puts printing" and "she's gonna rip" as a trader would.
Labels are normalised to bullish / bearish / neutral, and every score is
signed by direction: +0.9 is confidently bullish, -0.9 confidently
bearish, 0.0 neutral. That makes them averageable, which is what every
aggregate in a report is built on.
Sentiment surfaces at four levels:
| Level | Field |
|---|---|
| Post | AnalyzedPost.sentiment, .sentiment_score, .sentiment_confidence |
| Ticker within a post | AnalyzedPost.sentiment_for("INTC") |
| Ticker across the window | TickerTrend.sentiment, .sentiment_score, .sentiment_breakdown |
| Subreddit / overall | SubredditSummary.mood, TrendReport.mood |
Per-ticker attribution
A post reading "Long NVDA, short INTC" is bullish and bearish at once. One post-level label would give one of the two the wrong sign, so posts that mention more than one ticker are split into segments, and each ticker is scored from the segments that name only it — a sentence mentioning both says nothing about either in particular, and is used only when a ticker has no sentence of its own:
analyzed.sentiment # 'bullish' — the post as a whole
analyzed.sentiment_for("NVDA") # +0.9
analyzed.sentiment_for("INTC") # -0.8
Single-ticker posts — the majority — cost nothing extra: the post label is
already about that ticker. A ticker the recognizer resolved from a company
name ("TSMC" → TSM) has no literal segment to match, and falls back to the
post's score. Switch the whole thing off with
PostAnalyzer(per_ticker_sentiment=False).
TickerTrend.sentiment_score is the mean of these attributed scores, so a
ticker that everyone is shorting reads bearish even in a bullish subreddit.
Swapping the model
from reddit_stock_analyzer import PostAnalyzer, SentimentAnalyzer
analyzer = PostAnalyzer(sentiment=SentimentAnalyzer("your-org/your-model"))
Labels named BULLISH/BEARISH/NEUTRAL (or POSITIVE/NEGATIVE) are
understood as-is. A checkpoint published without an id2label mapping reports
LABEL_0, LABEL_1... — that order is not guessed, since guessing wrong
silently inverts every score. Those read neutral and log a warning until you
say which is which:
SentimentAnalyzer(
"your-org/your-model",
label_map={"LABEL_0": "bearish", "LABEL_1": "neutral", "LABEL_2": "bullish"},
)
Trend Metrics 📈
Posts are split into a window (the recent period, default 24h) and an equally long baseline immediately before it. Raw counts say how loud a ticker is; the comparison says whether it is getting louder.
| Metric | Meaning |
|---|---|
mentions |
Posts in the window mentioning the ticker — counted once per post, so one ranting post cannot manufacture a trend |
previous_mentions |
The same count over the baseline window |
momentum |
(mentions - previous) / (previous + 1) — smoothed so 0 → 5 is finite (4.0) and ranks above 50 → 60 (0.2) |
change_ratio |
mentions / previous, or null when there is no baseline |
spike_score |
Standard deviations above this scrape's mean mention count — "unusual for today" |
heat_score |
0–1 blend of mentions (40%), engagement (25%), unique authors (15%) and momentum (20%); the default ranking |
is_emerging |
No baseline mentions at all, and at least emerging_min_mentions now |
Alongside the ranked tickers a report carries rising / emerging / fading
shortlists, a by_subreddit breakdown (is this hype confined to r/wallstreetbets,
or has it reached r/investing?) and an hourly timeline per top ticker.
Every weight and threshold is a keyword argument:
report = compute_trends(
posts,
window_hours=6,
min_mentions=3, # ignore one-off noise
emerging_min_mentions=5,
weights={"momentum": 0.5, "mentions": 0.2}, # rank by acceleration
)
Configuration 🔧
| Subreddit category | Contents |
|---|---|
retail |
wallstreetbets, smallstreetbets, WallStreetbetsELITE, Shortsqueeze, pennystocks |
stocks |
stocks, StockMarket, investing, dividends |
research |
SecurityAnalysis, ValueInvesting, stocks |
trading |
Daytrading, swingtrading, options, thetagang, algotrading |
macro |
economics, finance, economy |
crypto |
CryptoCurrency, CryptoMarkets, satoshistreetbets, ethtrader, Bitcoin |
from reddit_stock_analyzer import DEFAULT_SUBREDDITS, all_subreddits, subreddits_for
subreddits_for("retail", "trading") # merged, de-duplicated, declaration order
Citation ✍️
If you use this project in your research, please cite as follows:
@misc{reddit_stock_analyzer,
author = {Stephan Akkerman},
title = {Reddit Stock Analyzer},
year = {2026},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/StephanAkkerman/reddit-stock-analyzer}}
}
Contributing 🛠
Contributions are welcome! If you have a feature request, bug report, or proposal for code refactoring, please feel free to open an issue on GitHub. We appreciate your help in improving this project.
License 📜
This project is licensed under the MIT License. See the LICENSE file for details.
Metadata
Release files for reddit-stock-analyzer 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| reddit_stock_analyzer-0.1.0.tar.gz | 49.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| reddit_stock_analyzer-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 87.7 kB
Release files / reddit_stock_analyzer-0.1.0.tar.gz
| Download URL | reddit_stock_analyzer-0.1.0.tar.gz |
|---|---|
| Size | 49.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
76f8b159b7937208ca67cc604930a349f459b2c563acc1f68da49d1f6a24dff6
|
|
BLAKE2b-256 checksum How to use checksums |
c27376a731b5084bc7dc9d18efd0164fa086c5a1be8ec7f312301352e522196a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency logRelease files / reddit_stock_analyzer-0.1.0-py3-none-any.whl
| Download URL | reddit_stock_analyzer-0.1.0-py3-none-any.whl |
|---|---|
| Size | 37.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
abc61bbbb3d4014619c55bfe0ff442510f13e2bf91b04f2d1fb93320a422620c
|
|
BLAKE2b-256 checksum How to use checksums |
dd7c60b21d86e52a76d5a494c0ef426e98fae9ad5c5ec299647a53615d98d076
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency log