Skip to main content
Glama
README.md
# Financial Research Copilot (FINCO)

A retrieval-augmented research assistant over SEC EDGAR filings, exposed as
an [MCP](https://modelcontextprotocol.io) server so it can be plugged directly
into Claude Desktop or any other MCP-compatible client, plus a standalone
Streamlit terminal for demoing it without an LLM client.

Ask an LLM things like *"What are Apple's biggest risk factors this
quarter?"* or *"Give me a snapshot of NVDA — price, fundamentals, and the
relevant 10-K excerpt"* and it answers by pulling live market data and
searching a locally-indexed corpus of filings, instead of hallucinating.

![FINCO terminal — live IBM quote, fundamentals, and a reranked 10-K excerpt](assets/finco_terminal.png)

*The Streamlit terminal answering a live query: quote + fundamentals from
Alpha Vantage's demo key, and the excerpt retrieved from IBM's latest 10-K
by the hybrid + reranked pipeline. Macro panel needs a (free) FRED key.*

## Why this exists

Most RAG demos stop at "chunk a PDF, embed it, ask an LLM." This project is
closer to something you'd actually run in production for multiple
customers:

- **Hybrid retrieval, not just vector search** — BM25 (lexical) and dense
  embeddings run in parallel and are combined with Reciprocal Rank Fusion,
  then re-ranked with a cross-encoder. Pure semantic search misses exact
  ticker/term matches; pure keyword search misses paraphrases. On the
  committed eval set the full pipeline beats dense-only by **+75% recall@5**
  and **+39% MRR** (numbers below).
- **A retrieval eval harness**, not vibes — `src/retrieval/eval` runs a
  fixed query set against any retrieval version and logs
  recall@k/precision@k/MRR/latency to Postgres, so changes to the pipeline
  (chunking, fusion weights, reranker) can be compared against a baseline
  instead of eyeballed. It earns its keep: it caught that `plainto_tsquery`
  ANDs every term, which silently zeroed out BM25 for natural-language
  questions — fusion looked useless until the eval showed why.
- **Multi-tenant data isolation via Postgres Row-Level Security**, not an
  app-layer `WHERE tenant_id = ...` that's one missed clause away from a
  data leak (`db/rls_policies.sql`). The database enforces isolation even if
  application code forgets.
- **Delta-aware ingestion** — filings are hashed at the section level so a
  10-K that hasn't materially changed since the last crawl isn't
  re-chunked and re-embedded (`src/ingestion/delta.py`), which matters once
  you're tracking a real watchlist daily.

## Architecture

```mermaid
flowchart TD
    EDGAR[SEC EDGAR] --> Crawler[crawler] --> Delta[delta detection] --> Chunker[chunker] --> Embedder[embedder] --> PG[(Postgres + pgvector)]
    PG --> BM25[BM25 search]
    PG --> Dense[dense vector search]
    BM25 --> RRF[Reciprocal Rank Fusion]
    Dense --> RRF
    RRF --> Rerank[cross-encoder rerank]
    Rerank --> MCP[MCP server tools<br/>Claude Desktop, etc.]
    Rerank --> UI[Streamlit terminal<br/>public demo UI]
```

Live market data (quote, fundamentals, macro indicators) is fetched from
Alpha Vantage / FRED alongside the filing search, cached per-tool with a
configurable TTL, and merged into a single "company snapshot" response.

A scheduled poller (`src/streaming/poller.py`) also watches the filing feed
and can dispatch webhook/email alerts when a followed ticker files a new
10-K/10-Q/8-K.

## Retrieval quality

Measured on a corpus of 24 real filings (10-K/10-Q/8-K for AAPL, MSFT,
NVDA, IBM; ~730 chunks) with 12 labeled queries. Ground truth per query is
defined by ranker-independent SQL predicates (ticker + form type + section
+ keyword conjunctions), so no retriever is favored by construction.

| Version | recall@5 | precision@5 | recall@10 | MRR | latency* |
|---|---|---|---|---|---|
| v1 dense-only | 0.164 | 0.217 | 0.239 | 0.475 | 297 ms |
| v2 hybrid (BM25 + dense + RRF) | 0.184 | 0.200 | 0.259 | 0.488 | 97 ms |
| v3 hybrid + cross-encoder rerank | **0.288** | **0.333** | **0.288** | **0.663** | 313 ms |

\* Cold-start skew: v1 runs first and its average includes one-time
embedding-model load. All models run locally on CPU.

Reproduce with `python -m src.retrieval.eval.run_all`, which rewrites
`eval_baseline.json` — the committed baseline that
`tests/test_eval_regression.py` gates against in CI (fails if recall@5
drops more than 2 points).

Delta-aware ingestion, measured on a second daily run over the same 24
filings: `sections_skipped=28 sections_reembedded=0 skip_ratio=100%` —
nothing is re-chunked or re-embedded unless a section's content hash
actually changed.

## Stack

| Layer | Choice |
|---|---|
| Language | Python 3.10+ |
| Database | PostgreSQL 16 + [pgvector](https://github.com/pgvector/pgvector) (HNSW index) |
| Dense embeddings | `sentence-transformers` (`all-MiniLM-L6-v2`, local — no API key) |
| Lexical search | Postgres `tsvector`/GIN, fused via `rank-bm25` |
| Reranking | local cross-encoder (`ms-marco-MiniLM-L-6-v2`) |
| Serving | [MCP](https://modelcontextprotocol.io) server (`mcp[cli]`, stdio transport) |
| Demo UI | Streamlit |
| Ingestion | `httpx` + `beautifulsoup4`/`lxml` against SEC EDGAR |
| CI | GitHub Actions (ruff + pytest), plus a scheduled daily-ingest workflow |

## MCP tools

| Tool | Description |
|---|---|
| `get_quote` | Live stock price (Alpha Vantage, cached) |
| `get_fundamentals` | Key financial metrics (Alpha Vantage, cached) |
| `get_macro_indicator` | Macro series from FRED — treasury yields, CPI, Fed funds rate, unemployment |
| `get_filing_excerpt` | Hybrid RAG search over ingested SEC filings for a ticker + topic |
| `get_company_snapshot` | Merges the above into one response: quote + fundamentals + macro context + relevant filing excerpt |

## Running it

**Prerequisites:** Docker (for Postgres/pgvector), Python 3.10+.

```bash
# 1. Start the database
docker compose up -d

# 2. Install the project (editable, with dev deps)
pip install -e ".[dev]"

# 3. Configure environment
cp .env.example .env
# fill in SEC_EDGAR_USER_AGENT (required by SEC's fair-use policy),
# and optionally ALPHA_VANTAGE_API_KEY / FRED_API_KEY for live market data

# 4. Load the schema
psql "$DATABASE_URL" -f db/schema.sql
psql "$DATABASE_URL" -f db/rls_policies.sql

# 5. Ingest some filings for a watchlist ticker
python -m src.ingestion.run_daily

# (optional) score all three retrieval versions against the eval set
python -m src.retrieval.eval.run_all

# 6a. Run the MCP server (point Claude Desktop / an MCP client at this)
python -m src.mcp_server.server

# 6b. ...or run the standalone demo UI instead
streamlit run public_app/streamlit_app.py
```

### Tests

```bash
pytest tests/ -m "not integration"   # unit tests, no external calls
pytest tests/                        # includes tests that hit real APIs
```

## Repo layout

```
src/
  ingestion/     SEC EDGAR crawler, section-level delta detection, chunking, embedding
  retrieval/     BM25 + dense search, RRF fusion, cross-encoder reranking, eval harness
  mcp_server/    MCP tool definitions + server entrypoint
  streaming/     Filing poller + alert dispatch
db/              Schema + row-level security policies
public_app/      Streamlit demo UI
tests/           Unit + integration tests
```

## Status

This is a personal/portfolio project, not a production service. It's not
deployed anywhere persistent; the daily-ingest GitHub Action and the
Streamlit app are meant to be run against your own Postgres instance. API
keys for Alpha Vantage and FRED are free-tier and not included.

## License

MIT — see [LICENSE](LICENSE).

Maintenance

ActivitySlowing
ResponsivenessNo issues