edgar-ratios 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. edgar_ratios-2.0.0/.github/workflows/ci.yml +20 -0
  2. edgar_ratios-2.0.0/.github/workflows/publish.yml +30 -0
  3. edgar_ratios-2.0.0/.gitignore +11 -0
  4. edgar_ratios-2.0.0/LICENSE +21 -0
  5. edgar_ratios-2.0.0/PKG-INFO +181 -0
  6. edgar_ratios-2.0.0/README.md +161 -0
  7. edgar_ratios-2.0.0/docs/demo.tape +35 -0
  8. edgar_ratios-2.0.0/edgar_ratios/__init__.py +3 -0
  9. edgar_ratios-2.0.0/edgar_ratios/__main__.py +143 -0
  10. edgar_ratios-2.0.0/edgar_ratios/commentary.py +109 -0
  11. edgar_ratios-2.0.0/edgar_ratios/config.py +80 -0
  12. edgar_ratios-2.0.0/edgar_ratios/export.py +66 -0
  13. edgar_ratios-2.0.0/edgar_ratios/facts.py +349 -0
  14. edgar_ratios-2.0.0/edgar_ratios/ratios.py +212 -0
  15. edgar_ratios-2.0.0/edgar_ratios/render.py +160 -0
  16. edgar_ratios-2.0.0/edgar_ratios/report.py +111 -0
  17. edgar_ratios-2.0.0/edgar_ratios/sec.py +111 -0
  18. edgar_ratios-2.0.0/pyproject.toml +39 -0
  19. edgar_ratios-2.0.0/tests/conftest.py +98 -0
  20. edgar_ratios-2.0.0/tests/fixtures/aapl.json +1 -0
  21. edgar_ratios-2.0.0/tests/fixtures/company_tickers.json +37 -0
  22. edgar_ratios-2.0.0/tests/fixtures/googl.json +1 -0
  23. edgar_ratios-2.0.0/tests/fixtures/jpm.json +1 -0
  24. edgar_ratios-2.0.0/tests/fixtures/msft.json +1 -0
  25. edgar_ratios-2.0.0/tests/fixtures/nvda.json +1 -0
  26. edgar_ratios-2.0.0/tests/fixtures/trim.py +41 -0
  27. edgar_ratios-2.0.0/tests/test_cli.py +144 -0
  28. edgar_ratios-2.0.0/tests/test_commentary.py +95 -0
  29. edgar_ratios-2.0.0/tests/test_config.py +61 -0
  30. edgar_ratios-2.0.0/tests/test_facts.py +360 -0
  31. edgar_ratios-2.0.0/tests/test_output.py +158 -0
  32. edgar_ratios-2.0.0/tests/test_ratios.py +214 -0
  33. edgar_ratios-2.0.0/tests/test_sec.py +147 -0
@@ -0,0 +1,20 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+ - uses: actions/setup-python@v6
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - run: pip install -e '.[dev]'
20
+ - run: pytest -q
@@ -0,0 +1,30 @@
1
+ # Push a version tag (e.g. v2.0.0) to test, build and publish to PyPI via Trusted Publishing (no tokens).
2
+ # The run finishes green only once pip can install the new version.
3
+ name: Publish to PyPI
4
+
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ environment: pypi
13
+ permissions:
14
+ id-token: write # lets PyPI verify this workflow instead of a stored token
15
+ steps:
16
+ - uses: actions/checkout@v5
17
+ - uses: actions/setup-python@v6
18
+ with:
19
+ python-version: "3.12"
20
+ - run: pip install -e '.[dev]' build
21
+ - run: pytest -q
22
+ - run: python -m build
23
+ - uses: pypa/gh-action-pypi-publish@release/v1
24
+ - name: Wait until pip can install it # PyPI's CDN can serve the old index for a few minutes after upload
25
+ run: |
26
+ for i in $(seq 30); do
27
+ pip download --no-deps --no-cache-dir -q -d /tmp/check "edgar-ratios==${GITHUB_REF_NAME#v}" && exit 0
28
+ sleep 10
29
+ done
30
+ exit 1
@@ -0,0 +1,11 @@
1
+ .env
2
+ __pycache__/
3
+ *.pyc
4
+ .DS_Store
5
+ .venv/
6
+ venv/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ .pytest_cache/
11
+ .claude/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Owen Alderson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.5
2
+ Name: edgar-ratios
3
+ Version: 2.0.0
4
+ Summary: Financial ratios straight from SEC filings: type a ticker, get 23 ratios over five years, every number traced to the 10-K it came from.
5
+ Project-URL: Homepage, https://github.com/owen-alderson/edgar-ratios
6
+ Author: Owen Alderson
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: 10-k,claude,edgar,financial-ratios,fundamentals,sec,valuation,xbrl
10
+ Classifier: Environment :: Console
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Office/Business :: Financial :: Investment
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: anthropic>=1.11
15
+ Requires-Dist: certifi
16
+ Requires-Dist: rich>=13
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=8; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # edgar-ratios
22
+
23
+ **Financial ratios straight from SEC filings: type a ticker, get 23 ratios over five years, every number
24
+ traced to the 10-K it came from.**
25
+
26
+ ```bash
27
+ edgar-ratios AAPL
28
+ ```
29
+
30
+ It reads the numbers companies file with the SEC (the US markets regulator) in machine-readable form,
31
+ works out margins, returns, liquidity, leverage and efficiency for each fiscal year, and tells you
32
+ exactly where each input came from. When a number can't be found, the ratio says **n/a and why**. It
33
+ is never guessed, estimated or filled with zero.
34
+
35
+ [![CI](https://github.com/owen-alderson/edgar-ratios/actions/workflows/ci.yml/badge.svg)](https://github.com/owen-alderson/edgar-ratios/actions/workflows/ci.yml)
36
+ [![PyPI](https://img.shields.io/pypi/v/edgar-ratios)](https://pypi.org/project/edgar-ratios/)
37
+ ![License: MIT](https://img.shields.io/badge/license-MIT-blue)
38
+
39
+ ![edgar-ratios demo: Apple's ratios over five fiscal years, then a peer comparison](docs/demo.gif)
40
+
41
+ ## Why
42
+
43
+ Most free ratio sites don't show their working. You can't tell whether "debt" includes leases, which
44
+ year a figure belongs to, or whether a number was restated. edgar-ratios is built the other way round:
45
+
46
+ - **Every input is traceable.** `--sources` lists, for each figure, the XBRL concept (the official tag
47
+ the company used, e.g. `RevenueFromContractWithCustomerExcludingAssessedTax`), the filing it came
48
+ from, when it was filed, and a link to it.
49
+ - **Missing means missing.** Apple stopped reporting interest expense as its own line after fiscal
50
+ 2023, so its interest coverage for 2024 and 2025 shows as n/a with that reason, not as a made-up
51
+ number.
52
+ - **Restatements are visible.** Companies often revise last year's numbers in this year's report.
53
+ The most recently filed value is used and the change is noted, including per-share figures that
54
+ changed because of a stock split ("consistent with a 10-for-1 stock split").
55
+ - **No invented prices.** SEC filings don't contain share prices, so valuation ratios (P/E,
56
+ EV/EBITDA...) only appear when you pass one with `--price`. They are labelled as using your price.
57
+
58
+ ## Quickstart
59
+
60
+ ```bash
61
+ pipx install edgar-ratios
62
+ edgar-ratios AAPL
63
+ ```
64
+
65
+ The first run asks for your name and email. The SEC asks every tool that reads its data to identify
66
+ itself ([its rules](https://www.sec.gov/os/accessing-edgar-data)); it's only sent to sec.gov and is
67
+ saved in `~/.edgar-ratios/config.json`. You can also set it up front:
68
+
69
+ ```bash
70
+ edgar-ratios --set-user-agent "Jane Doe jane@example.com"
71
+ ```
72
+
73
+ More:
74
+
75
+ ```bash
76
+ edgar-ratios AAPL --years 8 # longer history
77
+ edgar-ratios AAPL --price 227.50 # add valuation ratios at a price you supply
78
+ edgar-ratios AAPL MSFT NVDA # compare each company's latest fiscal year
79
+ edgar-ratios AAPL MSFT --price AAPL=227.50 --price MSFT=510
80
+ edgar-ratios AAPL --sources # where every input came from
81
+ edgar-ratios AAPL --html aapl.html # self-contained report, every figure linked to its filing
82
+ edgar-ratios AAPL --format json # everything, with provenance, for scripts
83
+ edgar-ratios AAPL MSFT --format csv # one row per ratio per year
84
+ edgar-ratios AAPL --commentary # a short read-out written by Claude
85
+ ```
86
+
87
+ `--commentary` needs an Anthropic API key in `ANTHROPIC_API_KEY` or in `~/.edgar-ratios/.env`.
88
+ Claude (`claude-opus-5-5`) only sees the computed ratios, is told to use them exactly as written, and
89
+ every number in its reply is checked against the table: anything that doesn't match is listed under
90
+ the commentary so you can check it.
91
+
92
+ ## The ratios
93
+
94
+ All balance-sheet figures are year-end values (not averages). "Debt" is long-term debt including the
95
+ part due within a year, plus short-term borrowings or commercial paper; leases are not included.
96
+
97
+ | Category | Ratio | What it tells you |
98
+ |---|---|---|
99
+ | **Valuation** (needs `--price`) | P/E | Share price ÷ diluted earnings per share: what you pay for $1 of yearly profit |
100
+ | | EV/EBITDA | Enterprise value (market cap + debt − cash, i.e. the price of the whole business) ÷ EBITDA |
101
+ | | EV/Revenue | Enterprise value ÷ revenue |
102
+ | | P/B | Market cap ÷ book value (shareholders' equity on the balance sheet) |
103
+ | | P/S | Market cap ÷ revenue |
104
+ | **Profitability** | Gross margin | What's left of each $1 of sales after the direct cost of what was sold |
105
+ | | EBITDA margin | EBITDA ÷ revenue. EBITDA = operating income + depreciation & amortization, a rough measure of cash profit from operations |
106
+ | | Operating margin | Profit from the core business ÷ revenue |
107
+ | | Net margin | The bottom line ÷ revenue |
108
+ | | ROE | Net income ÷ shareholders' equity |
109
+ | | ROA | Net income ÷ total assets |
110
+ | | ROIC | Net income ÷ (equity + debt), a simple version of return on invested capital |
111
+ | **Liquidity** | Current ratio | Current assets ÷ current liabilities: can it pay what's due within a year? |
112
+ | | Quick ratio | The same without inventory |
113
+ | | Cash ratio | Cash ÷ current liabilities, the strictest version |
114
+ | **Leverage** | Debt/Equity | Debt ÷ equity |
115
+ | | Net debt/EBITDA | (Debt − cash) ÷ EBITDA: years of operating profit to pay off net debt. Negative = more cash than debt |
116
+ | | Debt/Assets | Debt ÷ total assets |
117
+ | | Interest coverage | Operating income ÷ interest expense. Under 1.5x is a warning sign |
118
+ | **Efficiency** | Asset turnover | Revenue ÷ total assets |
119
+ | | Inventory turnover | Cost of revenue ÷ inventory: how many times stock is sold through a year |
120
+ | | Days inventory | Days of stock on hand |
121
+ | | Days sales outstanding | Days customers take to pay |
122
+
123
+ A ratio is shown as n/a, with the reason, when an input isn't reported, or when its denominator is
124
+ zero or negative (a P/E on negative earnings or an ROE on negative equity isn't meaningful).
125
+
126
+ ## How it works
127
+
128
+ ```mermaid
129
+ flowchart LR
130
+ T[ticker] --> SEC[sec.py<br/>SEC ticker list + XBRL company facts<br/>cached, ≤10 requests/s]
131
+ SEC --> F[facts.py<br/>fiscal years, one fact per input per year,<br/>fallbacks, restatements, derivations]
132
+ F --> R[ratios.py<br/>23 ratios: a number or a reason]
133
+ P[--price] --> R
134
+ R --> Out[table · JSON · CSV · HTML]
135
+ R -.-> C[commentary.py<br/>Claude + number check]
136
+ ```
137
+
138
+ The data is the SEC's free [XBRL company facts API](https://www.sec.gov/search-filings/edgar-application-programming-interfaces):
139
+ every number each company has tagged in its filings. [`facts.py`](edgar_ratios/facts.py) holds every rule:
140
+
141
+ - **Fiscal years** come from the company's 10-K annual reports, labelled the way the company labels
142
+ them (NVIDIA's "fiscal 2026" ended on 25 January 2026).
143
+ - **One number per input per year.** A flow figure such as revenue must cover one full year
144
+ (350-380 days, so 52/53-week years count) ending on the fiscal year end; a balance-sheet figure
145
+ must be dated on it. Only 10-K and 10-K/A filings count, so quarterly numbers never leak in.
146
+ - **Latest filing wins.** Each figure is repeated in later reports as a comparison. The most recently
147
+ filed value is used, and any difference from the first-filed one is noted.
148
+ - **Fallbacks are explicit.** Each input has an ordered list of concepts, because companies change
149
+ tags over time (Apple's revenue moved from `SalesRevenueNet` to
150
+ `RevenueFromContractWithCustomerExcludingAssessedTax`). The concept used is recorded per year.
151
+ - **Derived figures say so.** Gross profit computed as revenue minus cost of revenue, debt built from
152
+ its parts, D&A built from depreciation plus amortization: each is labelled.
153
+ - **P/E is protected against splits.** If the share count today is far from the one implied by the
154
+ 10-K's EPS (a stock split since the report), P/E is n/a rather than off by the split factor.
155
+
156
+ Downloads are cached in `~/.edgar-ratios/cache` for a day (`--refresh` to refetch).
157
+
158
+ **Limits:** US companies that file 10-Ks with US GAAP tags. Foreign companies filing 20-F or 40-F
159
+ aren't supported. Banks and insurers don't report current assets, gross profit or operating income,
160
+ so those ratios show as n/a for them.
161
+
162
+ ## Development
163
+
164
+ ```bash
165
+ git clone https://github.com/owen-alderson/edgar-ratios && cd edgar-ratios
166
+ python -m venv .venv && . .venv/bin/activate
167
+ pip install -e '.[dev]'
168
+ pytest # 200 tests, no network
169
+ ```
170
+
171
+ The tests run on real EDGAR data for Apple, Microsoft, NVIDIA, JPMorgan and Alphabet, trimmed to the
172
+ concepts the tool reads ([`tests/fixtures/trim.py`](tests/fixtures/trim.py) rebuilds them). The Claude
173
+ tests run the real SDK against a mocked HTTP transport, so the exact request is checked. Releases:
174
+ push a `vX.Y.Z` tag and GitHub Actions publishes to PyPI.
175
+
176
+ The original Streamlit app, where you typed the figures in by hand, is kept at tag
177
+ [`v1-streamlit`](https://github.com/owen-alderson/edgar-ratios/tree/v1-streamlit).
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,161 @@
1
+ # edgar-ratios
2
+
3
+ **Financial ratios straight from SEC filings: type a ticker, get 23 ratios over five years, every number
4
+ traced to the 10-K it came from.**
5
+
6
+ ```bash
7
+ edgar-ratios AAPL
8
+ ```
9
+
10
+ It reads the numbers companies file with the SEC (the US markets regulator) in machine-readable form,
11
+ works out margins, returns, liquidity, leverage and efficiency for each fiscal year, and tells you
12
+ exactly where each input came from. When a number can't be found, the ratio says **n/a and why**. It
13
+ is never guessed, estimated or filled with zero.
14
+
15
+ [![CI](https://github.com/owen-alderson/edgar-ratios/actions/workflows/ci.yml/badge.svg)](https://github.com/owen-alderson/edgar-ratios/actions/workflows/ci.yml)
16
+ [![PyPI](https://img.shields.io/pypi/v/edgar-ratios)](https://pypi.org/project/edgar-ratios/)
17
+ ![License: MIT](https://img.shields.io/badge/license-MIT-blue)
18
+
19
+ ![edgar-ratios demo: Apple's ratios over five fiscal years, then a peer comparison](docs/demo.gif)
20
+
21
+ ## Why
22
+
23
+ Most free ratio sites don't show their working. You can't tell whether "debt" includes leases, which
24
+ year a figure belongs to, or whether a number was restated. edgar-ratios is built the other way round:
25
+
26
+ - **Every input is traceable.** `--sources` lists, for each figure, the XBRL concept (the official tag
27
+ the company used, e.g. `RevenueFromContractWithCustomerExcludingAssessedTax`), the filing it came
28
+ from, when it was filed, and a link to it.
29
+ - **Missing means missing.** Apple stopped reporting interest expense as its own line after fiscal
30
+ 2023, so its interest coverage for 2024 and 2025 shows as n/a with that reason, not as a made-up
31
+ number.
32
+ - **Restatements are visible.** Companies often revise last year's numbers in this year's report.
33
+ The most recently filed value is used and the change is noted, including per-share figures that
34
+ changed because of a stock split ("consistent with a 10-for-1 stock split").
35
+ - **No invented prices.** SEC filings don't contain share prices, so valuation ratios (P/E,
36
+ EV/EBITDA...) only appear when you pass one with `--price`. They are labelled as using your price.
37
+
38
+ ## Quickstart
39
+
40
+ ```bash
41
+ pipx install edgar-ratios
42
+ edgar-ratios AAPL
43
+ ```
44
+
45
+ The first run asks for your name and email. The SEC asks every tool that reads its data to identify
46
+ itself ([its rules](https://www.sec.gov/os/accessing-edgar-data)); it's only sent to sec.gov and is
47
+ saved in `~/.edgar-ratios/config.json`. You can also set it up front:
48
+
49
+ ```bash
50
+ edgar-ratios --set-user-agent "Jane Doe jane@example.com"
51
+ ```
52
+
53
+ More:
54
+
55
+ ```bash
56
+ edgar-ratios AAPL --years 8 # longer history
57
+ edgar-ratios AAPL --price 227.50 # add valuation ratios at a price you supply
58
+ edgar-ratios AAPL MSFT NVDA # compare each company's latest fiscal year
59
+ edgar-ratios AAPL MSFT --price AAPL=227.50 --price MSFT=510
60
+ edgar-ratios AAPL --sources # where every input came from
61
+ edgar-ratios AAPL --html aapl.html # self-contained report, every figure linked to its filing
62
+ edgar-ratios AAPL --format json # everything, with provenance, for scripts
63
+ edgar-ratios AAPL MSFT --format csv # one row per ratio per year
64
+ edgar-ratios AAPL --commentary # a short read-out written by Claude
65
+ ```
66
+
67
+ `--commentary` needs an Anthropic API key in `ANTHROPIC_API_KEY` or in `~/.edgar-ratios/.env`.
68
+ Claude (`claude-opus-5-5`) only sees the computed ratios, is told to use them exactly as written, and
69
+ every number in its reply is checked against the table: anything that doesn't match is listed under
70
+ the commentary so you can check it.
71
+
72
+ ## The ratios
73
+
74
+ All balance-sheet figures are year-end values (not averages). "Debt" is long-term debt including the
75
+ part due within a year, plus short-term borrowings or commercial paper; leases are not included.
76
+
77
+ | Category | Ratio | What it tells you |
78
+ |---|---|---|
79
+ | **Valuation** (needs `--price`) | P/E | Share price ÷ diluted earnings per share: what you pay for $1 of yearly profit |
80
+ | | EV/EBITDA | Enterprise value (market cap + debt − cash, i.e. the price of the whole business) ÷ EBITDA |
81
+ | | EV/Revenue | Enterprise value ÷ revenue |
82
+ | | P/B | Market cap ÷ book value (shareholders' equity on the balance sheet) |
83
+ | | P/S | Market cap ÷ revenue |
84
+ | **Profitability** | Gross margin | What's left of each $1 of sales after the direct cost of what was sold |
85
+ | | EBITDA margin | EBITDA ÷ revenue. EBITDA = operating income + depreciation & amortization, a rough measure of cash profit from operations |
86
+ | | Operating margin | Profit from the core business ÷ revenue |
87
+ | | Net margin | The bottom line ÷ revenue |
88
+ | | ROE | Net income ÷ shareholders' equity |
89
+ | | ROA | Net income ÷ total assets |
90
+ | | ROIC | Net income ÷ (equity + debt), a simple version of return on invested capital |
91
+ | **Liquidity** | Current ratio | Current assets ÷ current liabilities: can it pay what's due within a year? |
92
+ | | Quick ratio | The same without inventory |
93
+ | | Cash ratio | Cash ÷ current liabilities, the strictest version |
94
+ | **Leverage** | Debt/Equity | Debt ÷ equity |
95
+ | | Net debt/EBITDA | (Debt − cash) ÷ EBITDA: years of operating profit to pay off net debt. Negative = more cash than debt |
96
+ | | Debt/Assets | Debt ÷ total assets |
97
+ | | Interest coverage | Operating income ÷ interest expense. Under 1.5x is a warning sign |
98
+ | **Efficiency** | Asset turnover | Revenue ÷ total assets |
99
+ | | Inventory turnover | Cost of revenue ÷ inventory: how many times stock is sold through a year |
100
+ | | Days inventory | Days of stock on hand |
101
+ | | Days sales outstanding | Days customers take to pay |
102
+
103
+ A ratio is shown as n/a, with the reason, when an input isn't reported, or when its denominator is
104
+ zero or negative (a P/E on negative earnings or an ROE on negative equity isn't meaningful).
105
+
106
+ ## How it works
107
+
108
+ ```mermaid
109
+ flowchart LR
110
+ T[ticker] --> SEC[sec.py<br/>SEC ticker list + XBRL company facts<br/>cached, ≤10 requests/s]
111
+ SEC --> F[facts.py<br/>fiscal years, one fact per input per year,<br/>fallbacks, restatements, derivations]
112
+ F --> R[ratios.py<br/>23 ratios: a number or a reason]
113
+ P[--price] --> R
114
+ R --> Out[table · JSON · CSV · HTML]
115
+ R -.-> C[commentary.py<br/>Claude + number check]
116
+ ```
117
+
118
+ The data is the SEC's free [XBRL company facts API](https://www.sec.gov/search-filings/edgar-application-programming-interfaces):
119
+ every number each company has tagged in its filings. [`facts.py`](edgar_ratios/facts.py) holds every rule:
120
+
121
+ - **Fiscal years** come from the company's 10-K annual reports, labelled the way the company labels
122
+ them (NVIDIA's "fiscal 2026" ended on 25 January 2026).
123
+ - **One number per input per year.** A flow figure such as revenue must cover one full year
124
+ (350-380 days, so 52/53-week years count) ending on the fiscal year end; a balance-sheet figure
125
+ must be dated on it. Only 10-K and 10-K/A filings count, so quarterly numbers never leak in.
126
+ - **Latest filing wins.** Each figure is repeated in later reports as a comparison. The most recently
127
+ filed value is used, and any difference from the first-filed one is noted.
128
+ - **Fallbacks are explicit.** Each input has an ordered list of concepts, because companies change
129
+ tags over time (Apple's revenue moved from `SalesRevenueNet` to
130
+ `RevenueFromContractWithCustomerExcludingAssessedTax`). The concept used is recorded per year.
131
+ - **Derived figures say so.** Gross profit computed as revenue minus cost of revenue, debt built from
132
+ its parts, D&A built from depreciation plus amortization: each is labelled.
133
+ - **P/E is protected against splits.** If the share count today is far from the one implied by the
134
+ 10-K's EPS (a stock split since the report), P/E is n/a rather than off by the split factor.
135
+
136
+ Downloads are cached in `~/.edgar-ratios/cache` for a day (`--refresh` to refetch).
137
+
138
+ **Limits:** US companies that file 10-Ks with US GAAP tags. Foreign companies filing 20-F or 40-F
139
+ aren't supported. Banks and insurers don't report current assets, gross profit or operating income,
140
+ so those ratios show as n/a for them.
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ git clone https://github.com/owen-alderson/edgar-ratios && cd edgar-ratios
146
+ python -m venv .venv && . .venv/bin/activate
147
+ pip install -e '.[dev]'
148
+ pytest # 200 tests, no network
149
+ ```
150
+
151
+ The tests run on real EDGAR data for Apple, Microsoft, NVIDIA, JPMorgan and Alphabet, trimmed to the
152
+ concepts the tool reads ([`tests/fixtures/trim.py`](tests/fixtures/trim.py) rebuilds them). The Claude
153
+ tests run the real SDK against a mocked HTTP transport, so the exact request is checked. Releases:
154
+ push a `vX.Y.Z` tag and GitHub Actions publishes to PyPI.
155
+
156
+ The original Streamlit app, where you typed the figures in by hand, is kept at tag
157
+ [`v1-streamlit`](https://github.com/owen-alderson/edgar-ratios/tree/v1-streamlit).
158
+
159
+ ## License
160
+
161
+ MIT
@@ -0,0 +1,35 @@
1
+ # Re-record the README demo: `vhs docs/demo.tape` from the repo root with EDGAR_RATIOS_USER_AGENT set
2
+ # (live SEC data, a throwaway settings dir).
3
+ Output docs/demo.gif
4
+
5
+ Set Shell zsh
6
+ Set FontSize 12
7
+ Set Width 1180
8
+ Set Height 640
9
+ Set Padding 10
10
+ Set Framerate 15
11
+ Set TypingSpeed 50ms
12
+ Set Theme { "background": "#000000", "foreground": "#e0e0e0" }
13
+
14
+ Hide
15
+ Type "export EDGAR_RATIOS_HOME=$(mktemp -d) PATH=$PWD/.venv/bin:$PATH COLUMNS=150 && clear"
16
+ Enter
17
+ Sleep 1s
18
+ Show
19
+
20
+ # five fiscal years of ratios, straight from Apple's 10-Ks
21
+ Type "edgar-ratios AAPL"
22
+ Sleep 400ms
23
+ Enter
24
+ Sleep 9s
25
+
26
+ Hide
27
+ Type "clear"
28
+ Enter
29
+ Show
30
+
31
+ # compare each company's latest fiscal year
32
+ Type "edgar-ratios MSFT NVDA GOOGL"
33
+ Sleep 400ms
34
+ Enter
35
+ Sleep 10s
@@ -0,0 +1,3 @@
1
+ """Financial ratios straight from SEC filings, every number traceable to the filing it came from."""
2
+
3
+ __version__ = "2.0.0"
@@ -0,0 +1,143 @@
1
+ """edgar-ratios AAPL [MSFT ...]: financial ratios from SEC filings, every input traceable to its filing."""
2
+
3
+ import argparse
4
+ import sys
5
+ from datetime import date
6
+ from pathlib import Path
7
+
8
+ from rich.console import Console
9
+
10
+ from . import __version__, config
11
+ from .ratios import Price, analyze
12
+ from .sec import Sec, SecError
13
+
14
+ EPILOG = """examples:
15
+ edgar-ratios AAPL five fiscal years of ratios
16
+ edgar-ratios AAPL --price 227.50 add valuation ratios at your price
17
+ edgar-ratios AAPL MSFT NVDA compare the latest fiscal year side by side
18
+ edgar-ratios AAPL MSFT --price AAPL=227.50 --price MSFT=510
19
+ edgar-ratios AAPL --sources show the XBRL concept and filing behind every input
20
+ edgar-ratios AAPL --html aapl.html self-contained report with links to each filing
21
+ edgar-ratios AAPL --format json everything, with provenance, for scripts
22
+ """
23
+
24
+
25
+ class UsageError(Exception):
26
+ pass
27
+
28
+
29
+ def parse_prices(raw: list[str], tickers: list[str], today: date) -> dict[str, Price]:
30
+ """`--price 227.5` for a single ticker, or `--price AAPL=227.5` per ticker."""
31
+ prices: dict[str, Price] = {}
32
+ for item in raw:
33
+ ticker, sep, value = item.rpartition("=")
34
+ if not sep:
35
+ if len(tickers) != 1:
36
+ raise UsageError("With several tickers, give each price as TICKER=PRICE (e.g. --price AAPL=227.50).")
37
+ ticker = tickers[0]
38
+ ticker = ticker.strip().upper()
39
+ if ticker not in tickers:
40
+ raise UsageError(f"--price names {ticker}, which isn't one of the tickers asked for.")
41
+ try:
42
+ number = float(value.replace(",", "").lstrip("$"))
43
+ except ValueError:
44
+ raise UsageError(f"Not a price: {value!r}.") from None
45
+ if number <= 0:
46
+ raise UsageError(f"Price must be positive (got {value}).")
47
+ prices[ticker] = Price(number, today)
48
+ return prices
49
+
50
+
51
+ def build_parser() -> argparse.ArgumentParser:
52
+ p = argparse.ArgumentParser(
53
+ prog="edgar-ratios",
54
+ description="Financial ratios from SEC EDGAR filings. Every input is traced to the 10-K it came from; "
55
+ "anything that can't be found is shown as n/a with the reason, never guessed.",
56
+ epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter,
57
+ )
58
+ p.add_argument("tickers", nargs="*", metavar="TICKER", help="one ticker for its history, several to compare")
59
+ p.add_argument("--years", type=int, default=5, help="fiscal years to show for one ticker (default 5)")
60
+ p.add_argument("--price", action="append", default=[], metavar="[TICKER=]PRICE",
61
+ help="share price for the valuation ratios (SEC filings contain no prices)")
62
+ p.add_argument("--format", choices=["table", "json", "csv"], default="table", help="output format (default table)")
63
+ p.add_argument("--html", type=Path, metavar="FILE", help="also write a self-contained HTML report")
64
+ p.add_argument("--sources", action="store_true", help="show where every input figure came from")
65
+ p.add_argument("--commentary", action="store_true", help="add a short read-out written by Claude (needs ANTHROPIC_API_KEY)")
66
+ p.add_argument("--refresh", action="store_true", help="ignore the local cache and refetch from the SEC")
67
+ p.add_argument("--set-user-agent", metavar='"NAME EMAIL"', help="save the name and email sent to the SEC, then exit")
68
+ p.add_argument("--version", action="version", version=f"edgar-ratios {__version__}")
69
+ return p
70
+
71
+
72
+ def main(argv: list[str] | None = None, sec: Sec | None = None, console: Console | None = None,
73
+ commentator=None, today: date | None = None) -> int:
74
+ args = build_parser().parse_args(argv)
75
+ console = console or Console()
76
+ err = Console(stderr=True)
77
+ try:
78
+ if args.set_user_agent:
79
+ config.save_user_agent(args.set_user_agent)
80
+ console.print(f"Saved. Requests to the SEC will identify as: {args.set_user_agent.strip()}")
81
+ return 0
82
+ if not args.tickers:
83
+ raise UsageError("Give at least one ticker, e.g. edgar-ratios AAPL")
84
+ if args.years < 1:
85
+ raise UsageError("--years must be at least 1.")
86
+ tickers = list(dict.fromkeys(t.strip().upper() for t in args.tickers))
87
+ prices = parse_prices(args.price, tickers, today or date.today())
88
+ if sec is None:
89
+ sec = Sec(config.user_agent(), config.cache_dir(), refresh=args.refresh)
90
+ years = args.years if len(tickers) == 1 else 1
91
+ analyses = [analyze(sec, t, years=years, price=prices.get(t)) for t in tickers]
92
+
93
+ commentary = None
94
+ if args.commentary:
95
+ config.load_env()
96
+ from .commentary import Commentator, CommentaryError
97
+
98
+ try:
99
+ commentary = (commentator or Commentator()).write(analyses)
100
+ except CommentaryError as e:
101
+ err.print(f"[red]error:[/red] {e}")
102
+ return 1
103
+
104
+ if args.format == "json":
105
+ from .export import to_json
106
+
107
+ sys.stdout.write(to_json(analyses) + "\n")
108
+ elif args.format == "csv":
109
+ from .export import to_csv
110
+
111
+ sys.stdout.write(to_csv(analyses))
112
+ else:
113
+ from .render import print_all
114
+
115
+ print_all(analyses, args.sources, console)
116
+ if commentary:
117
+ _print_commentary(console, commentary)
118
+ if args.html:
119
+ from .report import to_html
120
+
121
+ args.html.write_text(to_html(analyses, commentary.text if commentary else None))
122
+ err.print(f"Wrote {args.html}")
123
+ return 0
124
+ except (UsageError, config.ConfigError, SecError) as e:
125
+ err.print(f"[red]error:[/red] {e}")
126
+ return 2 if isinstance(e, UsageError) else 1
127
+
128
+
129
+ def _print_commentary(console: Console, commentary) -> None:
130
+ console.print("[bold]Commentary[/bold] [dim](Claude, from the numbers above)[/dim]")
131
+ console.print(commentary.text)
132
+ if commentary.unverified:
133
+ console.print(f"[yellow]Not in the table above, check before relying on them:[/yellow] "
134
+ f"{', '.join(commentary.unverified)}")
135
+ console.print()
136
+
137
+
138
+ def run() -> None:
139
+ sys.exit(main())
140
+
141
+
142
+ if __name__ == "__main__":
143
+ run()