Give it a ticker. It reads the company's SEC filings, checks the tape and the headlines, and writes an investment memo where every claim carries a citation you can click.
analyst demo # see the output shape — no API key, no network# Acme Robotics Inc. (ACME)
**Stance:** Cautious | **Confidence:** 55% | **Generated:** 2026-03-01 09:30 UTC
## Thesis
Acme has compounded revenue at 18% but the growth is renting three customers'
capex budgets, and the January guidance withdrawal suggests management does not
currently know what 2026 looks like.
## Findings
**1. Revenue is concentrated in three customers, and getting more so.**
Top three customers were 61% of FY2025 revenue, up from 54% in FY2024. Contracts
are cancellable on 90 days notice.
> Source: [FY2025 10-K, Item 1 & 1A](https://example.invalid/acme/10-K-2025.htm)
...
Most "AI research assistant" demos summarise a press release. The hard part of equity research isn't summarising — it's sourcing. An analyst who says "margins compressed" and can't tell you which line of which filing says so is not doing research, and neither is a model that does the same thing more fluently.
So the design constraint here was: the agent must not be able to make a claim it cannot cite. Everything below follows from that.
ticker
│
├─ resolve ──────────────► SEC company index → CIK
│
├─ PHASE 1: research loop (agentic, tools, unconstrained)
│ model chooses what to read ──► list_filings
│ read_filing ──► EDGAR
│ recent_news ──► Google News RSS
│ price_history ──► Stooq
│ every tool result is appended to a dossier
│
└─ PHASE 2: synthesis (single call, JSON-schema constrained)
dossier ──► Memo{stance, thesis, findings[], risks[], confidence}
└─ each finding: claim + evidence + citation
Phase 1 needs freedom — the model should decide whether the 8-K matters more than the 10-Q. Phase 2 needs a guaranteed shape — a deliverable that sometimes comes back as prose is not a deliverable.
Doing both in one call means either constraining the exploration or trusting the final turn to hold its format after a dozen tool results. Splitting them also means phase 2 starts from a clean context containing only what was actually read, so a citation can be checked against the dossier rather than taken on faith. If a source isn't in the dossier, the model has nowhere to have got it from.
| Mechanism | What it actually buys you |
|---|---|
| JSON schema on the synthesis call | Shape. Every memo has findings, each with a citation field. |
| Pydantic validation on the way in | Range. The schema can't express 0 ≤ confidence ≤ 1; a model returning 1.4 fails here rather than rendering a memo claiming 140% confidence. |
| Read-only tool surface | Blast radius. There is nothing the agent can call that mutates state or reaches a host we didn't choose, so a prompt injection buried in a filing has nothing to reach for. |
| Dossier-only context in phase 2 | Auditability. Citations are checkable against the run, not just plausible-looking. |
What it does not buy you: truth. A correctly-cited finding can still misread the filing. This is a research aid, not an analyst.
git clone https://github.com/K611-dot/quant-analyst-agent
cd quant-analyst-agent
python -m venv .venv && .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"See the output shape with no credentials at all. demo renders a stored
example memo — it is an artefact, not a run, and says so on stderr:
analyst demoA real run:
export ANTHROPIC_API_KEY=sk-ant-...
export ANALYST_SEC_USER_AGENT="your-project you@example.com" # SEC requires this
analyst memo AAPL --out memos/aapl.md --json memos/aapl.jsonA real run against fixed inputs — --offline swaps the data providers for
bundled fixtures, so the pipeline is deterministic and touches no network. The
model is still called, so this needs an API key:
analyst memo ACME --offline| Flag | Effect |
|---|---|
--offline |
Fixture data instead of network sources. Still calls the model. |
--out PATH |
Write Markdown to a file instead of stdout. |
--json PATH |
Also emit the structured memo for downstream tooling. |
--no-cache |
Bypass the disk cache (default TTL 12h). |
-q |
Suppress the token/tool run summary. |
Exit codes are distinct per failure class (2 config, 3 data source,
4 model refusal, 5 agent) so this composes in a pipeline.
Deliberately keyless, so anyone can clone this and get a working run without a signup step:
| Source | Used for | Notes |
|---|---|---|
| SEC EDGAR | Filings, company identity | Requires a descriptive User-Agent; rate-limited, so responses are cached. |
| Stooq | Daily closes and volume | Answers unknown symbols with a 200 and a junk body — handled. |
| Google News RSS | Headlines | Treated as leads to verify, never as evidence. |
Everything sits behind a ResearchProvider protocol, which is what makes the
offline fixture a drop-in and keeps the test suite hermetic.
pytest # no network, no API calls
ruff check . && ruff format --check .
mypyThe test suite scripts exact model turn sequences through a fake transport, so
loop behaviour is asserted rather than observed — parallel tool results going
back in a single message, pause_turn being resumed rather than mistaken for
completion, an empty dossier refusing to produce a memo, tool errors being
handed to the model instead of aborting the run.
Output is generated from public filings by an automated system. It is not investment advice, carries no price target, and has not been reviewed by a licensed analyst. Verify every citation before relying on anything it says.
MIT