portfolio-analytics-mcp
# portfolio-analytics-mcp
An [MCP](https://modelcontextprotocol.io) server that gives an AI agent three portfolio
analytics tools: beta to a benchmark, correlation between sectors, and FIFO trade
matching with realised and unrealised P&L.
Point an agent at it and ask *"what's the beta of this book to the S&P"*, *"are my
sectors actually diversified"*, or *"what did I make on these fills"* — in natural
language, against a portfolio you supply.
No API key. Prices come from Yahoo Finance, so a bare checkout works.
---
## Tools
| Tool | Answers | You supply |
|---|---|---|
| `portfolio_beta` | How sensitive is this portfolio to the market? | Holdings (+ optional weights), benchmark |
| `sector_correlation` | Is this book actually diversified, or is everything one bet? | Holdings with sector labels |
| `revalue_positions` | What did I make, and what's still open? | A list of fills, optionally current marks |
### What these tools do not do
They have **no brokerage connection and no account access**. Nothing here can look up
what you own — you pass the portfolio in. That is a deliberate boundary, not a missing
feature: the analytics are useful without ever touching a broker, and the server has no
business holding credentials.
---
## Install
```bash
git clone https://github.com/quanttrucker/portfolio-analytics-mcp
cd portfolio-analytics-mcp
python3 -m venv .venv && .venv/bin/pip install -e .
```
Register it with an MCP client — for Claude Desktop, in `claude_desktop_config.json`:
```json
{
"mcpServers": {
"portfolio-analytics": {
"command": "/absolute/path/to/portfolio-analytics-mcp/.venv/bin/portfolio-analytics-mcp"
}
}
}
```
Restart the client and the three tools appear.
---
## Demo
A recorded session of a real agent (`claude-opus-5`) driving this server over stdio —
MCP handshake, live Yahoo prices, plain-English questions, verbatim tool calls and
answers. It comes in two forms:
- **[`demo/transcript.html`](demo/transcript.html)** — the session rendered as a
self-contained page: tool calls as cards, results as charts (beta bars, sector
correlation heatmap, P&L tiles), raw JSON collapsed underneath. Open it in a
browser; no build step, no external assets.
- **[`demo/TRANSCRIPT.md`](demo/TRANSCRIPT.md)** — the same session as plain
markdown, the source of record.
What the session shows, one exchange per tool:
1. *"I hold 60% AAPL and 40% MSFT — what's my beta to SPY?"* → one `portfolio_beta`
call with the weights and benchmark filled in correctly; answer **0.83** with a
per-holding decomposition.
2. *"Is my portfolio actually diversified?"* → `sector_correlation` and
`portfolio_beta` called **in parallel**; the agent states its equal-weight
assumption, reads the near-zero cross-sector correlations, and flags that XOM's
−0.48 beta is regime-specific rather than presenting it as a stable hedge.
3. *"I bought 100 AAPL at 180, sold 40 at 195, it's at 210 now — what did I make?"*
→ `revalue_positions` matches the fills FIFO: **$600 realised, $1,800 unrealised**
on the 60-share remainder.
Re-record against the current market with:
```bash
python demo/transcript.py # needs the same .env credentials as the evals
```
---
## Example
Matching two fills and marking what's left open:
```jsonc
// revalue_positions
{
"executions": [
{"symbol": "AAA", "side": "BUY", "quantity": 100, "price": 10.0, "timestamp": "2025-01-02T10:00:00"},
{"symbol": "AAA", "side": "SELL", "quantity": 40, "price": 12.5, "timestamp": "2025-01-09T15:30:00"}
],
"marks": {"AAA": 13.0}
}
```
```jsonc
{
"realised_pnl_base": 100.0, // 40 units closed at +2.50
"open_lots": [
{"symbol": "AAA", "direction": "Long", "quantity": 60,
"entry_price": 10.0, "unrealised_pnl_currency": 180.0}
]
}
```
---
## Details worth knowing
**FIFO matching is symmetric.** A sell consumes the oldest open lots first; any excess
opens a position the other way, so a sell of 150 against a long of 100 closes the 100
and leaves a short of 50. Shorts work identically in reverse. Realised P&L converts at
the *closing* fill's FX rate, which is where the gain is crystallised.
**London prices are handled.** Yahoo quotes LSE listings in pence and reports their
currency as `GBp`, not `GBP`. Left alone that inflates a UK holding 100× against
everything else in the portfolio; here it is normalised to major units at ingestion.
Non-US listings take an `exchange` code (`LSE`, `IBIS`, `SEHK`) to resolve the venue.
**Cross-venue portfolios don't share a trading calendar.** A UK line and a US line
disagree on holidays, so on some dates one is missing. Summing across such a row drops
the absent member's *weight* rather than its *return*, understating the portfolio on
exactly the days two markets diverge. Returns are complete-case by default.
**Undefined statistics come back as `null`, never as a number.** A beta estimated on too
few overlapping observations is null with a note saying so, rather than a figure that
looks authoritative. A sector whose members offset each other exactly has no variance,
so its correlation is genuinely undefined — also null, not zero.
**Prices are cached to disk.** Yahoo is unofficial and occasionally flaky. Fetches are
cached (12h TTL) under `~/.cache/portfolio-analytics-mcp`, overridable with
`PORTFOLIO_ANALYTICS_CACHE`. A corrupt cache entry refetches rather than failing.
---
## Development
```bash
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest
```
The test suite never touches the network — the price downloader sits behind a protocol
and is faked. Symbology and the pence conversion are additionally checked against the
live feed by hand, since those are the two claims a fake cannot validate.
---
## Licence
MIT
TDQS
Scored across 3 tools
Each tool performs a distinctly different analytic: beta vs benchmark, sector correlation matrix, and FIFO P&L. There is no ambiguity or overlap between their purposes, so an agent can easily select the right tool.
All tool names use snake_case and are descriptive, but the pattern is slightly mixed: 'portfolio_beta' and 'sector_correlation' are noun phrases, while 'revalue_positions' is a verb phrase. This is a minor deviation that does not harm readability.
With three tools, the server is well-scoped. Each tool addresses a major portfolio analytics need (risk, diversification, and performance) and earns its place within the typical 3-15 tool range.
The server covers three important portfolio analytics functions, but it lacks additional common analytics like portfolio return or volatility. However, within its stated scope, there are no dead ends—each tool produces meaningful output from user-supplied data.