Turns SEC filings into a citation-first investment memo — with a two-stage claim-audit gate that separates fact from inference from unknown and refuses to silently pass hallucinated quotes.
Investment committees, PE deal teams, and credit analysts spend enormous hours turning primary documents into a structured memo: thesis, financials, risks, catalysts, sources. Most finance-AI demos try to automate this and die in compliance review, because they hallucinate numbers, invent citations, and blur the line between what a document says and what the model guessed.
This copilot is built around the failure mode, not around the demo. Every claim is traceable to a source span. Every quote is mechanically verified to appear in the source. The output is a memo a human can defend in a room.
-
Loughran & McDonald (2011), Journal of Finance — financial text needs finance-specific sentiment; generic NLP lexicons mislabel 10-K language. (Their dictionaries are free for academic use; commercial use needs a licence — flagged in Data.)
-
Mayew & Venkatachalam (2012), Journal of Finance — managerial tone on earnings calls carries price-relevant information.
-
anthropics/financial-services(Apache-2.0) — reference patterns for agentic IB / equity-research / PE workflows (Pitch Agent, Earnings Reviewer). -
Master's thesis to reverse-engineer (surfaced — read & vet next):
- S. Yurkevich, Dictionaries for Financial Texts (master thesis) — yurkevi.ch
- UTS honours thesis, Predicting Stock Prices: ML-Driven Sentiment Analysis of earnings calls — uts.edu.au
Math we're stealing: finance-specific term weighting and how to turn dictionary tone into a signal — without mistaking fluent output for correct output.
-
Frontier to push beyond (2025–2026): AlphaAgents — LLM multi-agents for equity portfolios (arXiv 2508.11152); The New Quant: LLMs in Financial Prediction & Trading survey (arXiv 2510.05533); FinRobot equity-research agent (arXiv 2411.08804). Target: beat a single-prompt baseline with an audited two-stage memo pipeline.
Hypothesis: the bottleneck in memo production is not analysis, it's faithful synthesis under a citation constraint — exactly what a constrained, audited LLM pipeline can compress from days to minutes.
| Source | Provides | Cost | License caveat |
|---|---|---|---|
| SEC EDGAR (submissions + Archives) | Filings | Free, no key | Public domain. User-Agent required by SEC fair-access policy. |
| SEC EDGAR XBRL companyfacts | Structured financial facts | Free, no key | Public domain. (Roadmap — see "What I'd build next".) |
| Earnings-call transcripts | Management tone | Free where available | Source-by-source. (Roadmap.) |
| FRED / Riksbanken / SCB | Macro context | Free | Open. (Roadmap.) |
| Loughran–McDonald dictionaries | Finance sentiment | Free (academic) | Commercial use needs licence. (Roadmap.) |
flowchart LR
A[SEC EDGAR<br/>ticker → CIK → 10-K HTML] --> B[Chunker<br/>Item-aware splitter<br/>+ TOC dedupe]
B --> C[Retriever<br/>BM25 + Item-scoped]
C --> D[Drafter<br/>Sonnet 4.6 or DeepSeek-chat<br/>+ prompt-cached chunks]
D --> E[Mechanical Audit<br/>fuzzy substring +<br/>compact-whitespace fallback]
E --> F[Semantic Audit<br/>Haiku 4.5 or DeepSeek-chat<br/>fact / inference / unknown]
F --> G[Markdown memo<br/>+ audit table]
E -. cite_excerpt not in source .-> X[flagged unknown<br/>no LLM call]
Two ideas do most of the work:
-
Item-aware chunking. Every 10-K has SEC-mandated section structure (Item 1 Business, 1A Risk Factors, 7 MD&A, ...). The chunker splits by those boundaries, dedupes the TOC entries (which match the same regex but are tiny page-number stubs), and tags each chunk with its Item id so citations read like an analyst's — "see Item 1A" — not "lines 4,283–4,425".
-
Two-stage audit gate. Most "RAG + judge" pipelines burn LLM calls on every claim. We don't: a fuzzy-substring check verifies the
cite_excerptexists in the cited Item first, in microseconds. Only claims that survive the mechanical check are sent to the semantic auditor, which classifies each intofact/inference/unknownwith a one-line reason. Hallucinated quotes get flagged at zero LLM cost.
Live run against Eli Lilly's FY2025 10-K (filed 2026-02-12,
accession 0000059478-26-000013). The drafter and auditor both run on
DeepSeek-chat over a 6-Item context (~12K input tokens).
$ ic-memo --ticker LLY --offline --provider deepseek -o lly_memo.md
provider=deepseek drafter=deepseek-chat auditor=deepseek-chat
drafter: input=12,204 cache_read=0 output=2,970 (~25s wall)
auditor: input=13,163 cache_read=11,392 output=1,378 (~10s wall)
Audit summary: total=23 fact=22 inference=1 unknown=0 (unknown_rate=0.0%)
wrote lly_memo.md (23 claims, 0 flagged unknown)
The iteration evidence (data/sample/lly_memo_v*.md): four real
runs against the LLY 10-K, each exposing a different failure mode and
each driving a concrete code or prompt fix.
| Version | Change | Claims | Fact | Inference | Unknown |
|---|---|---|---|---|---|
| v1 | First live run, cache cold | 23 | 22 | 0 | 1 (4%) |
| v2 | Matcher: compact-whitespace fallback for SEC table rows | 23 | 20 | 0 | 3 (13%) |
| v3 | Drafter prompt: forbid synthetic prose-quotes of tables | 22 | 15 | 5 | 2 (9%) |
| v4 | Drafter prompt: currency-aware example (preserve "$") | 23 | 22 | 1 | 0 (0%) |
The intermediate versions are committed in data/sample/ as evidence — the
story isn't "look, 0% unknown" but "here are three classes of hallucination
the audit gate caught that a naive system would have shipped as fact."
Cumulative API cost across all 4 live runs: ~$0.04.
Sample memo excerpt (from data/sample/lly_memo_v4.md):
**Audit summary** — 23 claims · ✓ 22 fact · ~ 1 inference · ✗ 0 unknown
## Financial Highlights
...
- ✓ Revenue grew 45% to $65,179 million in 2025 from $45,043 million in 2024.
- *Source: Item 7 — "Revenue $ 65,179 $ 45,043 45"*
- ✓ Net income increased 95% to $20,640 million in 2025 from $10,590 million in 2024.
- *Source: Item 7 — "Net income 20,640 10,590 95"*Every numerical claim is tied to the exact cell sequence in the cleaned-text form of the filing. The audit gate verifies the quote appears in the source before it ships.
- The drafter is non-deterministic. Two back-to-back runs produce different claim sets. v1's 4% unknown rate and v4's 0% are both reflections of one draw; honest expected rate sits in the low single digits with the current prompts.
- No XBRL yet. Numerical claims cite by prose excerpt from the rendered
filing text, not by XBRL fact ID. If the model paraphrases a number, the
mechanical check catches it — but a stricter "every number cites
us-gaap:Revenueswith period tag" pipeline would eliminate the whole class. (See "What I'd build next".) - Item 8 (Financial Statements) excluded from drafter context to keep tokens manageable. Headline numbers reach the drafter via Item 7 MD&A; deep segment / geographic detail does not.
- No retry loop in MVP. If unknown rate spikes, the operator inspects flagged claims manually rather than auto-redrafting.
- English-language US filings only. No 20-F / 6-K / non-SEC equivalents.
- License caveats — Loughran–McDonald and other academic-only resources are flagged at the Data layer; the MVP itself uses only public-domain SEC data.
- XBRL companyfacts loader. Numbers cite by fact ID
(
us-gaap:Revenues, period=2025-FY) so the citation IS the number. Eliminates the table-reformatting class of hallucinations entirely. - Connector to the firm's DMS / data room with entitlement-aware retrieval. The chunker stays; the source layer becomes pluggable.
- Versioned memos with diff-tracking across quarters. "What changed in management's risk language vs. last 10-K?" as a first-class output.
- Reviewer feedback loop that tightens the claim-audit thresholds and the drafter's quoting discipline based on accept/reject signals on shipped memos.
- Multi-agent specialists (AlphaAgents-style: separate Thesis, Financials, Risks, Catalysts agents) once the v1 single-drafter baseline has accumulated enough labelled examples to attribute failures.
# install
pip install -r requirements.txt
# configure: set ONE of these provider keys, plus an EDGAR User-Agent
cp .env.example .env
# Edit .env to add:
# SEC_EDGAR_USER_AGENT="Your Name your.email@example.com"
# DEEPSEEK_API_KEY=... (preferred — cheaper)
# OR ANTHROPIC_API_KEY=... (default if both set: DeepSeek)
# run against the bundled cached LLY 10-K (no SEC roundtrip beyond ticker lookup)
ic-memo --ticker LLY --offline -o my_memo.md --verbose
# run live against an arbitrary ticker
ic-memo --ticker MSFT --filing 10-K -o msft_memo.md --verbose
# select Items explicitly (default: 1,1A,1C,3,7,7A)
ic-memo --ticker LLY --offline --items 1A,7 -o risks_and_mdna.md
# tests
pytest # 84 hermetic tests, no network or API calls
pytest -m slow # +4 integration tests on the cached LLY 10-KCost characteristics (DeepSeek, ~12K-token context):
- Cache cold (first run): ~$0.012 / memo / ~35s wall
- Cache warm (repeat with same context): ~$0.004 / memo
Engineering standards: permissive code (MIT), public data first (SEC EDGAR, no key), every claim cited and mechanically verified, human-in-the-loop integrity gate.