finco
by HamzaOuadid
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.

*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).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues