Skip to content

Repository files navigation

Market Narrator

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).


Tech stack

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

Architecture

                           ┌─────────────────────────────────────────────┐
                           │              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     │
                     └───────────────────────────────────────────────┘

Design decisions

  • LangGraph over CrewAI: the three research agents (news, correlations, fundamentals) must run in parallel and their results merged before synthesis. LangGraph's Send API + conditional_edges + operator.add reducer 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.

Key components

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

Dashboard

Event feed + narrative detail

┌────────────────────────────────────────────────────────────────────┐
│  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)                                         │
└────────────────────────────────────────────────────────────────────┘

Jaeger distributed trace

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

Observability

OTel spans

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

Prometheus metrics

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

Narrative quality evals

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]

Project structure

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

Running locally

# 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:8501

Without 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-missing

Cloud deployment (GCP)

Deployed 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

Environment variables

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

License

MIT

About

AI system that monitors financial markets in real time, detects statistical anomalies and autonomously generates journalist-style narratives explaining why they happen. Built with LangGraph multi-agent system, RAG, persistent memory, MCP server and LLMOps observability.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages