┌─────────────────────────────────────────────────────────────────────────────┐
│ External Sources │
│ ┌──────────────┐ ┌───────────────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ SEC EDGAR │ │ Yahoo Finance RSS │ │ Reuters │ │ GNews API │ │
│ │ 8-K / 6-K │ │ (50 tickers) │ │ RSS │ │ (optional) │ │
│ └──────┬───────┘ └────────┬──────────┘ └────┬─────┘ └──────┬───────┘ │
└─────────┼───────────────────┼──────────────────┼───────────────┼───────────┘
└─────────┬─────────┘ └───────┬───────┘
▼ ▼
┌──────────────────────────────────────────────────────┐
│ sentiment-ingestor (Python · APScheduler · 60s) │
│ Redis dedup (48h TTL) → drop seen URLs │
└──────────────────────────┬───────────────────────────┘
│ Kafka PRODUCE
▼
┌──────────────────────┐
│ raw.headlines │
│ 4 partitions │
│ key = source │
│ retention = 24h │
└──────────┬────────────┘
│ Kafka CONSUME (batch 64)
▼
┌──────────────────────────────────────────────────────┐
│ sentiment-classifier (Python · FinBERT · MPS/GPU) │
│ ┌─────────────┐ ┌──────────────────────────────┐ │
│ │ Validation │ │ FinBERT batch inference │ │
│ │ empty / non │ │ sentimentScore = P(pos)-P(neg)│ │
│ │ -English │ │ p50 latency: ~15 ms/headline │ │
│ └──────┬──────┘ └─────────────┬────────────────┘ │
│ │ invalid │ valid │
│ ▼ ▼ │
│ sentiment.dlq spaCy EntityRuler NER │
│ (2 partitions) ticker / company / sector │
└──────────────────────────────────┬────────────────────┘
│ Kafka PRODUCE
▼
┌──────────────────────────┐
│ enriched.sentiment │
│ 12 partitions │
│ key = ticker │
│ retention = 7 days │
└──────────┬────────────────┘
│ Kafka CONSUME
▼
┌──────────────────────────────────────────────────────┐
│ sentiment-persistence (Python · psycopg2 · Redis) │
│ │
│ TimescaleDB Redis │
│ ┌───────────────────┐ ┌─────────────────────┐ │
│ │ sentiment_events │ │ HSET sentiment:{tk} │ │
│ │ hypertable │ │ latest snapshot │ │
│ │ 4 ticker shards │ │ │ │
│ │ compress > 7d │ │ PUBLISH sentiment. │ │
│ │ │ │ updates.{ticker} │ │
│ │ sentiment_hourly │ │ │ │
│ │ (cont. aggregate) │ │ drift z-score │ │
│ └───────────────────┘ │ LIST [720 pts] │ │
└──────────────────────────┬─┴─────────────────────┴───┘
│
┌─────────────────────┴──────────────────────┐
│ │
▼ ▼
┌───────────────────────────┐ ┌─────────────────────────┐
│ sentiment-api (FastAPI) │ │ Redis Pub/Sub │
│ asyncpg pool │ │ sentiment.updates.* │
│ GET /v1/sentiment/ │ └───────────┬─────────────┘
│ {ticker}/latest │ │ WS push
│ {ticker}/history │ ▼
│ sector/{sector} │ ┌────────────────────────────┐
│ drift-alerts │ │ sentiment-dashboard │
│ health │ │ React + Vite + Tailwind │
│ WS /stream/{ticker} ─────┼─────────│ Recharts · ReactQuery │
└───────────────────────────┘ │ localhost:3001 │
└────────────────────────────┘
| Topic | Partitions | Key | Retention | Purpose |
|---|---|---|---|---|
raw.headlines |
4 | source | 24 h | Raw ingestor output |
enriched.sentiment |
12 | ticker | 7 days | FinBERT-classified, NER-enriched events |
sentiment.dlq |
2 | — | 30 days | Empty / non-English / malformed events |
Partitioning enriched.sentiment by ticker ensures all AAPL events land on the same
partition — preserving ordering for drift detection.
| Service | Port | Stack | Scales |
|---|---|---|---|
| sentiment-ingestor | — | Python · APScheduler · kafka-python | Horizontal (multiple sources) |
| sentiment-classifier | 8000 (Prometheus) | Python · HuggingFace · spaCy | Vertical (GPU) |
| sentiment-persistence | — | Python · psycopg2 · redis-py | Horizontal (more partitions) |
| sentiment-api | 8081 | FastAPI · asyncpg · redis.asyncio | Horizontal (stateless) |
| sentiment-dashboard | 3001 | React · Vite · nginx | CDN |
Fine-tuned from ProsusAI/finbert on:
- FinancialPhraseBank (
sentences_allagree) — 2,264 sentences - FiQA-SA (
pauri32/fiqa-2018) — 1,173 sentence-level annotations
Training: 5 epochs, lr=2e-5, batch=16, weighted cross-entropy (inverse class frequency). Hardware: Apple MPS (~4.5 min).