The problem
Most retail trading tools show you a chart and a headline feed side by side and leave the correlation to you. TradeX exists because that gap — between "here's the news" and "here's what it means for the stock" — is exactly the part that's hard to automate well, and exactly the part that matters.
TradeX is a full-stack system our team built that continuously scrapes financial news from 68 international sources across 10 regions, runs FinBERT (a finance-domain sentiment model) over every mention of an S&P 500 company, and turns that into a weighted BUY/SELL signal with a confidence score and a written rationale — all pushed to a dashboard over WebSockets within seconds of a new article landing. It's not a backtesting toy; the whole design is built around one constraint the client needed: the signal has to be live, not batch-computed overnight.
Architecture at a glance
TradeX/
├── Engine/ FastAPI backend (Python 3.12)
│ ├── scrapers/ 68 RSS + HTML scrapers, source-country tagged
│ ├── analysis/ FinBERT sentiment, entity extraction, signal scoring
│ ├── market_data/ Alpaca (live) → Yahoo Finance → Alpha Vantage fallback chain
│ ├── prediction/ Dual LSTM price predictors (tick-level + bulk S&P 500)
│ ├── realtime.py WebSocket fan-out, optional Redis pub/sub for scale-out
│ └── scheduler.py Fast-polling background jobs
└── Next/ Next.js 16 + React 19 + Tailwind 4 dashboard
The backend and frontend are decoupled by a single /ws WebSocket contract — the API is the source of truth, the socket is just a notification channel that tells the dashboard "something changed, come re-fetch" or, for price ticks, pushes the value directly.
The event-driven pipeline, not a cron job
The obvious way to build this is: scrape on a timer, analyze on a timer, regenerate signals on a timer. That's what most side-project "sentiment trackers" do, and it's why they feel stale. TradeX instead treats new articles as an event that triggers a synchronous pipeline:
async def process_new_articles(articles, db):
# 1. de-dupe against existing URLs, persist new rows
# 2. broadcast_event("new_articles", ...) immediately
# 3. for each new article: extract company mentions -> FinBERT batch score
# -> broadcast_event("new_mention", ...) per hit
# 4. if any tickers were touched, regenerate signals for those companies
# -> broadcast_event("signals_refreshed", ...)
Every stage broadcasts as it completes rather than waiting for the whole batch to finish, so the dashboard's live feed shows articles landing, sentiment resolving, and signals recalculating as three separate, visible moments instead of one opaque update. Entity extraction itself is a single pre-compiled regex — all S&P 500 names and aliases sorted longest-first and joined into one alternation — rather than 500 individual string searches per article, which matters when you're doing this across every scraped article in real time.
Signal scoring is a transparent weighted formula, not a black box
A lot of "AI trading signal" projects hide the scoring behind an opaque model and ask you to trust it. Our team built TradeX's SignalGenerator to be a deliberately readable, tunable formula instead:
| Factor | Weight | What it measures |
|---|---|---|
| Sentiment | 35% | Average FinBERT score across mentions, last 10 days |
| Trend | 25% | Is sentiment improving, declining, or flat |
| Volume | 15% | Trading volume vs. 10-day average |
| Momentum | 15% | Second-half vs. first-half sentiment velocity |
| Coverage | 10% | Article count (more coverage = more confidence) |
A signal only fires if it clears a minimum article count (3) and a minimum confidence floor (0.3) — thin coverage doesn't get to masquerade as a strong signal. Every signal also ships with a generated, human-readable reasoning string built from the same inputs, so a BUY on NVDA reads as an actual sentence about why, not just a number.
Two different LSTM predictors, for two different data realities
The most interesting engineering decision in TradeX is that there isn't one price predictor — there are two, because the two available data feeds have fundamentally different characteristics:
- Tick-level predictor: Alpaca's free IEX WebSocket tier caps out at 30 symbols, but for those it delivers real trade-level ticks with no delay. A small
TickPriceLSTM(2-layer, hidden size 64, trained on log-returns over a 60-tick rolling window) predicts the next tick price for the highest-volume names (NVDA, TSLA, AAPL, and so on), re-scored every time a new trade comes in. - Bulk predictor: the other ~470 S&P 500 names only have Yahoo Finance's 15-minute-cadence bulk data. Rather than leave them out, a second instance of the same architecture runs against that slower feed — and, because 15-minute bars don't produce enough fresh data to bother with a scheduled retrain, it does online learning: every new bar is a training step against a low learning-rate Adam optimizer, so the model keeps adapting between the periodic full retrains.
class TickPriceLSTM(nn.Module):
def __init__(self, input_size=1, hidden_size=64, num_layers=2, dropout=0.2):
super().__init__()
self.lstm = nn.LSTM(input_size, hidden_size, num_layers,
dropout=dropout if num_layers > 1 else 0.0,
batch_first=True)
self.fc = nn.Linear(hidden_size, 1)
Both share the same model code and device-detection logic — one architecture, two deployment shapes, matched to what each data source can actually support. That's a more honest solution than pretending every ticker has the same data quality.
GPU inference with a real fallback path, not a GPU requirement
FinBERT inference is the most expensive step in the pipeline, so TradeX supports three precision modes — FP32, FP16, and INT8 dynamic quantization — switchable live from the dashboard without restarting the process:
| Mode | VRAM | Speed | Constraint |
|---|---|---|---|
| FP32 | ~440 MB | baseline | CPU or GPU |
| FP16 | ~220 MB | ~2x | CUDA only |
| INT8 | ~110 MB | ~1.5x | CPU or GPU |
The SentimentAnalyzer is a lazily-initialized singleton that detects CUDA at load time and silently falls back to FP32-on-CPU if no GPU is present — the same model class runs identically on a CI box or a laptop as it does on a CUDA 12.4 container, it's just slower. The whole thing ships as a Docker image with GPU passthrough wired through docker-compose.yml's device reservations, so "works on a dev machine with a 4090" and "works in a plain container" are the same code path, not two.
Real-time delivery: WebSockets with an optional Redis hop
The /ws endpoint is a plain FastAPI WebSocket fan-out for a single-instance deployment, but ConnectionManager also supports an optional Redis pub/sub layer: if REDIS_URL is configured, broadcasts go through a tradex:events channel instead of being pushed directly to local sockets, which means the same broadcast code scales horizontally across multiple backend instances without a rewrite. If Redis isn't configured, or the connection fails, it logs a warning and falls back to local-only broadcast rather than crashing the pipeline — a deliberately soft dependency, not a hard requirement.
Frontend: Next.js 16, React 19, and a dashboard that reflects the real sky
The dashboard (Next.js 16, React 19, Tailwind 4, shadcn) is intentionally glassmorphic, but the one genuinely novel touch is SkyBackground: it uses the browser's geolocation (with a manual city override and localStorage persistence) plus the WeatherAPI to render a dynamic sky gradient that reflects real dawn/dusk timing and current conditions — cloud density, rain/snow intensity, wind direction — behind the trading data. It's a small thing, but it's the kind of detail that separates "a dashboard" from "a dashboard someone cared about."
What this project demonstrates
TradeX is a system our team built under a hard real-time constraint end to end: scraping, NLP inference, signal math, and neural prediction all had to stay in the seconds-not-minutes range, and every architectural choice — event-driven broadcasts instead of polling, precision-switchable GPU inference with CPU fallback, two LSTM models matched to two data feeds, Redis as an optional rather than mandatory scaling layer — was made to protect that constraint without over-engineering the parts that didn't need it. It's the kind of project that shows the difference between wiring up an ML model and building the infrastructure around it that makes the model's output actually arrive on time.
