Trading comps from SEC XBRL filings, with one rule enforced by the architecture:
The model does not do the arithmetic.
It picks the peers and writes the commentary. Every number that lands in a cell — enterprise value, EBITDA, the multiples, the medians — is computed in Python from reported XBRL figures, in code you can read and that has tests.
compsheet build ACME --offline --no-commentary # no API key needed
compsheet build AAPL --xlsx out/aapl.xlsxLanguage models are good at valuation reasoning and unreliable at valuation arithmetic. The dangerous part is that the failure is invisible: a plausible EV/EBITDA that happens to be wrong looks exactly like a correct one, and it looks correct all the way into the deck.
So the split is structural, not a matter of prompting discipline:
| Job | Who does it | Why |
|---|---|---|
| Which companies are comparable | Model | Business model and end market, not a SIC code |
| Do those companies exist | Code | Every proposal is resolved against the SEC ticker index |
| EV, EBITDA, multiples, medians | Code | Auditable, tested, reproducible |
| What the table means | Model | Reads the finished table; never recomputes it |
The commentary model receives the rendered table as text. It never sees raw XBRL, so any figure it quotes is already in a row above it and a reader can check it.
Most of the work in a comps tool is refusing to make something up. Three states, kept distinct everywhere:
| Cell | Means | Statistics |
|---|---|---|
n/a |
The filer never tagged the input | Excluded, and counted in the coverage column |
nm |
Arithmetically real, analytically useless — negative EBITDA, a 900x from a near-zero denominator | Excluded from median and mean |
| a number | Computed from reported figures | Included |
Never a zero. A defaulted zero flows into a median, drags the whole peer set,
and nobody looking at the output can tell it was invented. Every derived field in
the model layer is float | None for exactly this reason.
Coverage is printed under the table, so a median drawn from two of nine peers looks like a median drawn from two of nine peers.
Filers don't agree on tags. One reports Revenues; another reports
RevenueFromContractWithCustomerExcludingAssessedTax; a third reports both with
different values because one is a segment total. Total debt is usually split
across current and non-current, so LongTermDebt alone understates anyone with a
drawn revolver.
So each concept has an ordered list of candidate tags, and which tag actually
supplied each figure is recorded and exported to a Sources sheet in the
workbook. When two rows disagree, the first question is whether they were even
measuring the same thing.
Only 10-K/20-F/40-F observations are used, so a comps table never mixes an annual figure for one company with a trailing quarter for another.
The statistics rows are =MEDIAN(...) formulas over the peer range, not pasted
values.
An analyst's first move on receiving a comp set is to delete a peer they disagree
with. A workbook whose median doesn't move when they do is worse than useless —
it's confidently wrong in a way that looks fine. The formulas are IFERROR-guarded
because MEDIAN over a range of blanks returns #NUM!, and a peer row is blank
whenever the filer didn't tag the input.
git clone https://github.com/K611-dot/compsheet
cd compsheet
python -m venv .venv && .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"# No credentials: fixture companies, explicit peers, no commentary.
compsheet build ACME --offline --no-commentary
# Live, with model-selected peers and a write-up.
export ANTHROPIC_API_KEY=sk-ant-...
export COMPSHEET_SEC_USER_AGENT="compsheet you@example.com" # SEC requires this
compsheet build AAPL --out out/aapl.md --xlsx out/aapl.xlsx
# Live, with your own peer set — skips model peer selection entirely.
compsheet build AAPL --peers MSFT,GOOGL,AMZN,META --no-commentary| Flag | Effect |
|---|---|
--peers A,B,C |
Use these peers; skips model-driven selection |
--no-commentary |
Skip the write-up (the table is fully deterministic without it) |
--offline |
Bundled fixture companies, no network |
--xlsx PATH |
Write the workbook with live formulas |
--out PATH |
Write Markdown to a file |
--peers plus --no-commentary means no model calls at all — the whole
deterministic half runs on its own.
Exit codes: 2 config, 3 data source, 4 peer selection, 5 commentary.
--offline runs against a fictional peer set built to break things, because a
tidy one would test none of the handling that matters:
- a peer with negative EBITDA →
EV/EBITDAmust rendernmand leave the median - a peer that never tagged D&A → EBITDA is
None, not EBIT relabelled - a peer with no price series → its entire multiples column is
n/a - a peer with no prior year → growth is
n/awhile everything else computes - a barely-profitable peer producing a real but useless P/E above the bound
pytest # hermetic: no network, no API calls
ruff check . && ruff format --check .
mypytests/test_metrics.py is the file that justifies the project's central claim. If
it were thin, "the model doesn't do the maths" would be decoration.
Figures are computed from SEC XBRL company facts and last closing prices. This is a comps table, which shows where something trades — not what it is worth. Not investment advice, not a valuation, and no substitute for reading the filings.
MIT