desk
Provides multi-agent equity research for US markets with provenance-backed financial data from SEC EDGAR, technicals, macro, and Alpaca paper trading, enforcing risk limits and journaling theses.
README
Trading Desk
Multi-agent equity research for US markets, running on Claude Code subagents over MCP. Analysis-first: real money is never touched, and order flow goes to an Alpaca paper account.
Why this exists
Financial data is easy to fetch and easy to get wrong. US-GAAP XBRL concepts drift per company and over time, so reading a single hardcoded concept returns figures that are years stale with no error raised:
| Ticker | Live concept | Correct FY revenue | Naive Revenues lookup |
|---|---|---|---|
| AAPL | RevenueFromContractWithCustomerExcludingAssessedTax |
$416.2B (FY25) | $62.9B — a 2018 figure |
| NVDA | Revenues |
$215.9B (FY26) | correct, coincidentally |
| MSFT | RevenueFromContractWithCustomerExcludingAssessedTax |
$331.8B (FY26) | $16.0B — a 2010 figure |
An agent given $62.9B for Apple writes a confident, completely wrong thesis. The whole design follows from preventing that.
Operating rules
- Agents never compute and never recall. Every number comes from a tool call; ratios and indicators are calculated in Python.
- Every value carries provenance — XBRL concept, period, form, accession, filing date — so any figure can be traced back to the filing.
- Stale data raises. A number too old to answer the question is an error, not a footnote.
- Concept resolution is code with tests, not a prompt instruction.
Status
Phase 4 complete: research, sizing, a scoreable record, the context around a name, and paper execution that cannot be reached except through an approved thesis.
- [x] EDGAR client — rate limited, disk cached, no API key required
- [x] Concept resolver with provenance and staleness enforcement
- [x] Golden tests (23) pinned to filed 10-Ks
- [x]
deskMCP server, verified over stdio - [x] Phase 1 —
fundamentals+chartistagents,/analyze - [x] Phase 2 —
risk(veto) + journal,/journal - [x] Phase 3 —
filings,macro,screener - [x] Phase 4 —
pilot, Alpaca paper execution behind risk approval - [ ] Phase 5 —
/postmortemcalibration loop
Setup
uv sync
uv run pytest tests/golden -q # must be green before any agent work
Credentials
Keys live in the environment, never in the repo — .mcp.json reads them via
${VAR} expansion. Put them in ~/.zshrc so they cannot be committed by
accident.
| Variable | Where to get it | Needed by |
|---|---|---|
ALPACA_API_KEY / ALPACA_SECRET_KEY |
app.alpaca.markets — switch the dashboard to Paper, then API Keys → Generate. Secret shows once. | Phase 1 (chartist) |
FRED_API_KEY |
fredaccount.stlouisfed.org/apikeys — instant, free | macro agent. Everything else runs without it |
SEC_USER_AGENT |
Your own name email |
optional; defaults in .mcp.json |
Paper trading needs no funding, identity check or approval — that applies only to live accounts.
Verify without exposing anything (the script prints presence and a masked prefix only, never a secret):
uv run python scripts/check_keys.py
Data sources
| Source | Provides | Auth |
|---|---|---|
| SEC EDGAR | XBRL financials, filings, Form 4 insider | none (User-Agent only) |
| Alpaca | Bars, quotes, news, paper orders/positions | free API key |
| FRED | Rates, curve, inflation | free API key |
Deliberately not used: yfinance (unofficial endpoints, silent empty
responses) and Stooq (now behind a JS proof-of-work wall). EDGAR is preferred
for fundamentals because it is the source of record rather than a scrape of it.
MCP tools
| Tool | Returns |
|---|---|
get_financials |
Full statement, 17 line items, each with provenance |
get_financial_history |
Historical series for one line item |
get_metrics |
Margins, returns, growth, leverage, cash conversion |
get_technicals |
Trend, momentum, volatility, support and resistance |
list_line_items |
Supported line item keys |
get_filings |
Recent filings, optionally filtered by form |
get_filing_text |
A filing's text as filed, markup stripped, in windows |
search_filing_text |
Verbatim passages around a term in a filing |
get_insider_activity |
Form 4 insider transactions |
get_macro_snapshot |
Rates, curve, inflation, jobs, vol, dollar — with changes |
get_macro_series |
One macro series in detail |
get_market_movers / get_most_active |
Discovery lists from the tape |
rank_candidates |
Orders a symbol list by relative strength |
size_position |
Share count, capital at risk, every limit checked, verdict |
get_risk_policy |
The standing limits, each with its rationale |
journal_thesis |
Records a call with its evidence and falsifiers |
list_theses / get_thesis |
The book, open or closed |
close_thesis |
Resolves a call and computes realised R |
get_account |
Paper account equity, cash, buying power |
place_order |
Sends an approved thesis to the paper broker |
get_broker_positions / get_broker_orders |
Broker state |
cancel_order / close_broker_position |
Unwind |
reconcile_positions |
Broker positions against journalled theses |
Risk limits
The risk agent holds a veto and does not exercise judgement about it — the
limits are policy, checked in Python, and quoted back with the reason they
exist. get_risk_policy returns them at runtime.
| Limit | Value | Why |
|---|---|---|
| Risk per trade | 1% of equity | Ten consecutive losses cost a tenth of the account, which is survivable |
| Position size | 20% of equity | A tight stop makes a huge position look cheap; gaps do not respect stops |
| Portfolio heat | 6% of equity | The total loss accepted if every open thesis fails at once |
| Reward:risk | 2:1 minimum | Below it the strategy needs a win rate nobody here has demonstrated |
| Stop distance | 1.5x ATR(14) | A stop inside daily noise is an exit schedule, not protection |
Correlation is the one call the checks cannot make: three 1% positions on the same driver are one 3% position in disguise, and the agent is instructed to veto that even when every numeric limit passes.
The journal
Theses are JSON files under theses/, one per call, written at the moment the
call is made — greppable, diffable, and reviewable alongside the code that
produced them. A thesis will not record without at least one falsifier, and
direction="watch" calls are recorded too: a watchlist name that ran away
without you is as informative as a trade that failed, and only one of the two
tends to get remembered. Realised R is computed on close from the recorded
entry and stop, so outcomes compare across positions of different sizes.
Set DESK_THESES_DIR to keep the book somewhere other than the repository.
Execution
Paper only. The base URL is the paper endpoint, hardcoded with no environment
variable that redirects it, and the account number is checked for Alpaca's
PA prefix before any order is sent — live keys against the paper URL fail
closed rather than trading.
The gate is the shape of the function rather than an instruction to an agent:
place_order(thesis_id) # and nothing else
There is no symbol parameter, no quantity, no price. All of it is read back out of the journal entry, which exists only because the risk officer approved it and only carries a share count the risk officer computed. An agent asked to buy 100 shares of something has no way to express that.
An order is refused when the thesis is closed, is a watch call, carries a vetoed verdict, was sized at zero shares, lacks an entry or stop, or already has an order attached — the last of which is what stops a retried call from opening a second position in the same name.
Orders go out as brackets, so the stop the risk officer sized against is
submitted with the entry rather than left to a later call that might never
happen. reconcile_positions compares what the broker holds against what the
journal knows about, which is the only way to see the untracked exposure that
portfolio heat is blind to.
Known limitations
- Q4 quarterly gaps. Q4 gets no standalone 10-Q; it must be derived as FY minus Q1–Q3. Not yet implemented — quarterly series skip Q4.
- Alpaca free tier is IEX-only (~2% of consolidated volume), so daily-bar volume is unreliable for swing setups. The price layer sits behind an adapter so Tiingo or another EOD source can be swapped in without touching agent code. Same seam later carries a non-US broker.
- Non-US-GAAP filers (foreign issuers on IFRS) are not covered by the current concept registry.
- The screener has no universe. It ranks a list you give it and reads the
venue's movers and most-active tables. It cannot screen "all US software
above $2B" — there is no fundamental universe behind it, and a themed list
assembled by an agent is a recollection, not a screen. The
screeneragent is instructed to say which it is. - Filing text is text.
get_filing_textstrips markup and returns what was filed. Tables survive as readable rows, but nothing is parsed into figures — a number read out of filing prose has no XBRL concept behind it, so preferget_financialswhenever the figure exists there. - Macro needs its own key. Without
FRED_API_KEYthe macro tools fail with a message saying where to get one. Nothing else on the desk depends on them. - A submitted order is not a filled one. A limit entry may never fill,
and the position does not exist until it does. The
pilotagent is instructed never to describe one as the other. - Portfolio heat only sees the journal. A position taken without recording
it is invisible to the risk checks, so the heat number is exposure as
recorded rather than exposure as held.
size_positionsays so in itslimitationson every call.
Not financial advice
A research tool, not a prediction engine. The /postmortem loop exists
because calls need to be scored honestly rather than remembered selectively.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.