Real-time financial market anomaly detection + AI-powered narrative generation
Live demo → — Demo mode available online (no API keys required). Full pipeline functional with live Polygon.io data — demonstrable on request.
Market Narrator monitors a watchlist of US equities, detects statistically significant price/volume/volatility events, and automatically generates cited, confidence-scored narratives using a LangGraph multi-agent pipeline backed by GPT-4o (configurable via the MODEL_NAME environment variable).
| Layer | Technology |
|---|---|
| Agents & orchestration | LangGraph 1.x, LiteLLM (model-agnostic) |
| Data sources | Polygon.io (OHLCV), NewsAPI, SEC EDGAR |
| RAG | Qdrant Cloud — hybrid dense (MiniLM) + sparse (BM25) with RRF re-ranking |
| Memory | Redis (episodic short-term) + Qdrant (long-term narrative recall) |
| Tool protocol | MCP-compatible HTTP server (market data, news search, SEC filings) |
| Observability | OpenTelemetry → Jaeger, Prometheus → Grafana, LLM-as-judge evals |
| Infrastructure | GCP Cloud Run, Artifact Registry, Secret Manager, Cloud Build CI/CD |
| Testing | pytest — 230+ unit + integration tests |
┌─────────────────────────────────────────────┐
│ Cloud Run (public) │
│ Streamlit Dashboard :8501 │
└──────────────────┬──────────────────────────┘
│ asyncio.Queue
┌──────────────────▼──────────────────────────┐
│ Anomaly Detector │
│ Polygon.io → OHLCV → detect_events() │
│ PRICE_SPIKE · VOLUME_SURGE · VOL_BREAK │
└──────────────────┬──────────────────────────┘
│ MarketEvent
┌──────────────────▼──────────────────────────┐
│ LangGraph Orchestrator │
│ TRIAGE → fan-out (Send API) │
│ ├─ NEWS_AGENT (Polygon + RAG) │
│ ├─ CORR_AGENT (Pearson / peers) │
│ └─ FUND_AGENT (SEC EDGAR + RAG) │
│ fan-in → NARRATOR (gpt-4o via LiteLLM) │
└──────┬─────────────────────┬────────────────┘
│ │
┌────────────▼──────┐ ┌──────────▼──────────┐
│ Qdrant (RAG + │ │ Redis (episodic │
│ narrative memory)│ │ short-term memory) │
└───────────────────┘ └─────────────────────┘
│
┌────────────▼──────────────────────────────────┐
│ Observability │
│ OpenTelemetry → Jaeger (OTLP gRPC) │
│ Prometheus metrics → Grafana dashboards │
│ LLM-as-judge evals → SQLite → CLI report │
└───────────────────────────────────────────────┘
-
LangGraph over CrewAI: the three research agents (news, correlations, fundamentals) must run in parallel and their results merged before synthesis. LangGraph's
SendAPI +conditional_edges+operator.addreducer enables typed parallel fan-out/fan-in natively. CrewAI's sequential role model would have serialised ~15 s of I/O-bound work. -
Hybrid RRF (dense + BM25) for RAG, dense-only for narrative recall: ticker symbols and SEC form types are exact-match signals that cosine similarity handles poorly — BM25 closes that gap. Qdrant's
FusionQuery(RRF)fuses both branches without a tunable weight. The narrative memory collection intentionally skips BM25: past narrative lookup is a pure semantic similarity problem with no keyword-matching benefit. -
Redis for episodic memory, Qdrant for long-term recall: the access patterns are structurally different. Recent episodes are retrieved by ticker key (
mnr:ep:{TICKER}) with a 24-hour TTL and a bounded list (LPUSH + LTRIM) — O(1), no semantics. Past narratives are queried by semantic similarity ("find events for NVDA similar to this one"), which requires vector search. Forcing either pattern into the wrong store would either bloat the vector index or lose semantic recall. -
LiteLLM for model abstraction: all three LLM calls route through a single
traced_completion()wrapper. Swapping GPT-4o for Claude Sonnet or Gemini is a one-line env var change (MODEL_NAME) with zero code modifications. Token cost accounting works identically across models via tiktoken. -
MCP-compatible tool server over internal utilities: market data, news search, and SEC filings are exposed as an MCP-compatible HTTP server, not just internal Python functions. The same tools can be consumed by external Claude Desktop sessions or other agents without re-implementing rate limiting and retry logic.
| Module | Purpose |
|---|---|
agents/anomaly_detector.py |
Polls Polygon.io; emits typed MarketEvent |
agents/orchestrator.py |
LangGraph graph: TRIAGE→[NEWS|CORR|FUND]→NARRATOR |
agents/narrator.py |
GPT-4o generates JSON narrative with citations |
rag/ |
Qdrant hybrid RRF search for news + SEC filings |
memory/ |
Redis episodic store + Qdrant long-term narrative recall |
mcp_server/ |
MCP-compatible tools: market data, news search, SEC filings |
observability/tracer.py |
OTel spans for every node, LLM call, RAG query, MCP tool |
observability/evals.py |
LLM judge scores narrative quality (0–10 × 4 dimensions) |
dashboard/app.py |
Streamlit dashboard: live feed + candlesticks + eval scores |
┌────────────────────────────────────────────────────────────────────┐
│ Market Narrator ● LIVE │
├────────────────────────┬───────────────────────────────────────────┤
│ Event Feed │ [Candlestick chart — NVDA 5d + marker] │
│ │ │
│ NVDA ◀ selected │ ## NVIDIA surges 8% as Blackwell GPU │
│ PRICE_SPIKE │ demand exceeds supply guidance by 40% │
│ EXTREME z=4.21 │ │
│ Narrative ready │ NVIDIA (NVDA) recorded a price z-score │
│ │ of +4.21 on April 11... │
│ AAPL │ │
│ VOLUME_SURGE │ Sources │
│ SIGNIFICANT z=3.84 │ - NVIDIA Supply Update (Reuters, ...) │
│ Narrative ready │ - Microsoft Azure 8-K (SEC, ...) │
│ │ │
│ SPY │ Confidence Duration Est. Cost Corr. │
│ VOLATILITY_BREAK │ 88% 11.4s $0.0187 AMD… │
│ SIGNIFICANT │ │
│ Generating… │ Quality scores (LLM judge) │
│ │ Factual:9 Coherence:9 Spec:10 Risk:1 │
├────────────────────────┴───────────────────────────────────────────┤
│ System Metrics │
│ Events today: 6 (+2) Narratives: 5 (+2) Cost: $0.083 (+$0.03) │
│ Avg quality: 8.6/10 (+0.3) │
└────────────────────────────────────────────────────────────────────┘
market_narrator.run ──────────────────────────────── 11.4s
node/triage ─── 0.3s
node/subagent_dispatcher [news] ─────── 4.1s
mcp/news_search ─ 0.9s
news_agent/rag ─ 0.4s
news_agent/llm model=gpt-4o ─── 2.6s
node/subagent_dispatcher [correlations] ──── 3.2s
mcp/market_data ×6 ── 1.8s
correlations_agent/llm ─── 1.3s
node/narrator ──────────────────── 5.8s
narrator/llm ─────────────────── 5.6s
Every pipeline run generates a trace with:
- Root span
market_narrator.run— ticker, event_type, severity, total cost - Node spans
node/{triage|subagent_dispatcher|narrator}— per-node latency - LLM spans
{agent}/llm— model, input/output tokens, USD cost (tiktoken) - RAG spans
{agent}/rag— query, chunks retrieved, score range - MCP spans
mcp/{tool}— tool name, success/error
market_narrator_events_total{ticker,event_type} counter
market_narrator_run_duration_seconds{ticker,event_type} histogram
market_narrator_run_cost_usd{ticker,event_type} histogram
market_narrator_narrative_confidence{ticker,event_type} histogram
The NARRATOR node automatically calls a judge LLM after each generation:
python -m market_narrator.evals.report --last 7d
# ============================================================
# Market Narrator — Narrative Quality Report
# Window : last 7d Rows: 42 narratives
# ============================================================
# Factual Grounding 8.4/10 ████████████████░░░░
# Coherence 9.1/10 ██████████████████░░
# Specificity 7.9/10 ████████████████░░░░
# Hallucination Risk 1.8/10 ██░░░░░░░░░░░░░░░░░░ [lower is better]market_narrator/
├── agents/
│ ├── anomaly_detector.py # Polygon.io polling + event detection
│ ├── orchestrator.py # LangGraph graph + run_event()
│ ├── graph_state.py # TypedDict state + Pydantic output models
│ ├── triage.py # Routing logic + Send fan-out
│ ├── news_agent.py # Live news + RAG + LLM
│ ├── correlations_agent.py # Peer Pearson correlations + LLM
│ ├── fundamentals_agent.py # SEC filings + RAG + LLM
│ └── narrator.py # Final narrative synthesis
├── rag/
│ ├── embedder.py # Dense (MiniLM) + sparse (BM25) embeddings
│ ├── indexer.py # NewsIndexer + FilingsIndexer
│ └── retriever.py # Hybrid RRF search
├── memory/
│ ├── episode_store.py # Redis episodic short-term memory
│ └── recall.py # Qdrant long-term narrative memory
├── mcp_server/
│ ├── server.py # MCP-compatible HTTP server
│ └── tools/ # market_data, news_search, sec_filing
├── observability/
│ ├── tracer.py # OTel setup + span helpers + Prometheus metrics
│ └── evals.py # LLM judge + SQLite persistence
├── evals/
│ └── report.py # CLI: python -m market_narrator.evals.report
└── dashboard/
├── app.py # Streamlit dashboard
└── demo_events.json # Pre-recorded events for DEMO_MODE
config/
└── watchlist.yaml # Tickers + detection thresholds
deploy/
├── prometheus.yml # Prometheus scrape config
└── grafana/ # Grafana datasource + dashboard provisioning
scripts/
├── ingest_news.py # One-off: seed news_index from NewsAPI
└── ingest_filings.py # One-off: seed filings_index from SEC EDGAR
Dockerfile # Multi-stage image (< 500 MB)
docker-compose.yml # Full local dev stack
cloudbuild.yaml # GCP CI/CD: pytest → build → deploy
# 1. Clone and configure
git clone https://github.com/mikelballay/market-narrator.git && cd market-narrator
cp .env.example .env # add POLYGON_API_KEY + OPENAI_API_KEY
# 2. Start the full stack (Qdrant, Redis, Jaeger, Prometheus, Grafana)
docker compose up -d
# 3. Open the dashboard
open http://localhost:8501Without Docker:
pip install -e ".[dev]"
# Demo mode — no API keys required
DEMO_MODE=true streamlit run market_narrator/dashboard/app.py
# Run tests
pytest # 230+ tests
pytest --cov=market_narrator --cov-report=term-missingDeployed on Cloud Run (pay-per-use, scales to zero) with:
- Qdrant Cloud — vector DB for RAG + narrative memory
- Upstash Redis — episodic memory store (HTTPS REST, serverless-compatible)
- Secret Manager — all API keys stored as versioned secrets
- Cloud Build — CI/CD trigger on
git push origin main:pytest → build + push → deploy
Fixed infrastructure cost: $0/month. OpenAI cost ~$0.019 per full pipeline run.
| Service | Tier | Cost/month |
|---|---|---|
| Qdrant Cloud | Free — 1 GB | $0 |
| Upstash Redis | Free — 10,000 req/day | $0 |
| Cloud Run | Pay per use — scales to 0 | ~$0–2 |
| Cloud Build | 120 min/day free tier | $0 |
| Secret Manager | < 10,000 ops/month | $0 |
| OpenAI (gpt-4o) | ~$0.019 per pipeline run | variable |
| Variable | Required | Default | Description |
|---|---|---|---|
POLYGON_API_KEY |
Yes (live) | — | Polygon.io API key |
OPENAI_API_KEY |
Yes (live) | — | OpenAI API key |
NEWS_API_KEY |
Yes (live) | — | NewsAPI.org key |
MODEL_NAME |
No | gpt-4o |
LiteLLM model string — swap to any supported model without code changes |
DEMO_MODE |
No | false |
Replay pre-recorded events without API calls |
QDRANT_URL |
No | http://localhost:6333 |
Qdrant endpoint |
REDIS_URL |
No | redis://localhost:6379 |
Redis endpoint |
OTEL_EXPORTER_OTLP_ENDPOINT |
No | http://localhost:4317 |
Jaeger/OTLP |
MN_EVALS_DB |
No | ~/.market_narrator/evals.db |
Eval SQLite path |
MIT