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.
- edgar_ratios-2.0.0/.github/workflows/ci.yml +20 -0
- edgar_ratios-2.0.0/.github/workflows/publish.yml +30 -0
- edgar_ratios-2.0.0/.gitignore +11 -0
- edgar_ratios-2.0.0/LICENSE +21 -0
- edgar_ratios-2.0.0/PKG-INFO +181 -0
- edgar_ratios-2.0.0/README.md +161 -0
- edgar_ratios-2.0.0/docs/demo.tape +35 -0
- edgar_ratios-2.0.0/edgar_ratios/__init__.py +3 -0
- edgar_ratios-2.0.0/edgar_ratios/__main__.py +143 -0
- edgar_ratios-2.0.0/edgar_ratios/commentary.py +109 -0
- edgar_ratios-2.0.0/edgar_ratios/config.py +80 -0
- edgar_ratios-2.0.0/edgar_ratios/export.py +66 -0
- edgar_ratios-2.0.0/edgar_ratios/facts.py +349 -0
- edgar_ratios-2.0.0/edgar_ratios/ratios.py +212 -0
- edgar_ratios-2.0.0/edgar_ratios/render.py +160 -0
- edgar_ratios-2.0.0/edgar_ratios/report.py +111 -0
- edgar_ratios-2.0.0/edgar_ratios/sec.py +111 -0
- edgar_ratios-2.0.0/pyproject.toml +39 -0
- edgar_ratios-2.0.0/tests/conftest.py +98 -0
- edgar_ratios-2.0.0/tests/fixtures/aapl.json +1 -0
- edgar_ratios-2.0.0/tests/fixtures/company_tickers.json +37 -0
- edgar_ratios-2.0.0/tests/fixtures/googl.json +1 -0
- edgar_ratios-2.0.0/tests/fixtures/jpm.json +1 -0
- edgar_ratios-2.0.0/tests/fixtures/msft.json +1 -0
- edgar_ratios-2.0.0/tests/fixtures/nvda.json +1 -0
- edgar_ratios-2.0.0/tests/fixtures/trim.py +41 -0
- edgar_ratios-2.0.0/tests/test_cli.py +144 -0
- edgar_ratios-2.0.0/tests/test_commentary.py +95 -0
- edgar_ratios-2.0.0/tests/test_config.py +61 -0
- edgar_ratios-2.0.0/tests/test_facts.py +360 -0
- edgar_ratios-2.0.0/tests/test_output.py +158 -0
- edgar_ratios-2.0.0/tests/test_ratios.py +214 -0
- 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,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
|
+
[](https://github.com/owen-alderson/edgar-ratios/actions/workflows/ci.yml)
|
|
36
|
+
[](https://pypi.org/project/edgar-ratios/)
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+

|
|
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
|
+
[](https://github.com/owen-alderson/edgar-ratios/actions/workflows/ci.yml)
|
|
16
|
+
[](https://pypi.org/project/edgar-ratios/)
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+

|
|
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,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()
|