balanceproof 0.1.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.
- balanceproof-0.1.0/PKG-INFO +55 -0
- balanceproof-0.1.0/README.md +39 -0
- balanceproof-0.1.0/balanceproof/__init__.py +193 -0
- balanceproof-0.1.0/balanceproof.egg-info/PKG-INFO +55 -0
- balanceproof-0.1.0/balanceproof.egg-info/SOURCES.txt +8 -0
- balanceproof-0.1.0/balanceproof.egg-info/dependency_links.txt +1 -0
- balanceproof-0.1.0/balanceproof.egg-info/requires.txt +3 -0
- balanceproof-0.1.0/balanceproof.egg-info/top_level.txt +1 -0
- balanceproof-0.1.0/pyproject.toml +28 -0
- balanceproof-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: balanceproof
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SEC fundamentals that prove they add up: checked balance sheets, income statements and cash flow, point-in-time.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://balanceproof.dev
|
|
7
|
+
Project-URL: Documentation, https://balanceproof.dev/api
|
|
8
|
+
Keywords: sec,edgar,xbrl,fundamentals,balance sheet,backtesting,point-in-time
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
11
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Provides-Extra: pandas
|
|
15
|
+
Requires-Dist: pandas>=1.5; extra == "pandas"
|
|
16
|
+
|
|
17
|
+
# balanceproof
|
|
18
|
+
|
|
19
|
+
Python client for [BalanceProof](https://balanceproof.dev): US public-company
|
|
20
|
+
fundamentals from SEC EDGAR, where every balance sheet, income statement and
|
|
21
|
+
cash flow is checked against its own totals, and a filing that does not add up
|
|
22
|
+
is flagged with the reason instead of passed through.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install "balanceproof[pandas]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import balanceproof as bp
|
|
30
|
+
|
|
31
|
+
client = bp.Client("YOUR_KEY") # free key: https://balanceproof.dev/dashboard
|
|
32
|
+
# or set BALANCEPROOF_API_KEY
|
|
33
|
+
|
|
34
|
+
client.balance_sheet("AAPL")
|
|
35
|
+
client.statements("AAPL", period="quarterly")
|
|
36
|
+
|
|
37
|
+
# Point-in-time (Pro and above): only what had been filed by that date,
|
|
38
|
+
# so a backtest never sees a figure before it was public.
|
|
39
|
+
client.balance_sheet("AAPL", as_of="2025-03-01")
|
|
40
|
+
|
|
41
|
+
# A pandas panel, one row per company and period.
|
|
42
|
+
df = client.panel(["AAPL", "MSFT", "KO"], ["revenue", "net_income", "operating_cash_flow"],
|
|
43
|
+
period="quarterly")
|
|
44
|
+
clean = df[df.checks_failed.str.len() == 0] # drop periods that failed a check
|
|
45
|
+
|
|
46
|
+
# Failed checks and restatements across every company (Pro: 90 days, Business: all).
|
|
47
|
+
feed = client.exceptions(type="restatement")
|
|
48
|
+
client.to_frame(feed)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Derived figures (Q4 as the year minus nine months, Q2/Q3 cash flow from
|
|
52
|
+
year-to-date filings) are listed in each period's `derived`.
|
|
53
|
+
|
|
54
|
+
A call that needs a higher plan raises `balanceproof.PlanRequired`, whose
|
|
55
|
+
`required_plan` names it.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# balanceproof
|
|
2
|
+
|
|
3
|
+
Python client for [BalanceProof](https://balanceproof.dev): US public-company
|
|
4
|
+
fundamentals from SEC EDGAR, where every balance sheet, income statement and
|
|
5
|
+
cash flow is checked against its own totals, and a filing that does not add up
|
|
6
|
+
is flagged with the reason instead of passed through.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install "balanceproof[pandas]"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import balanceproof as bp
|
|
14
|
+
|
|
15
|
+
client = bp.Client("YOUR_KEY") # free key: https://balanceproof.dev/dashboard
|
|
16
|
+
# or set BALANCEPROOF_API_KEY
|
|
17
|
+
|
|
18
|
+
client.balance_sheet("AAPL")
|
|
19
|
+
client.statements("AAPL", period="quarterly")
|
|
20
|
+
|
|
21
|
+
# Point-in-time (Pro and above): only what had been filed by that date,
|
|
22
|
+
# so a backtest never sees a figure before it was public.
|
|
23
|
+
client.balance_sheet("AAPL", as_of="2025-03-01")
|
|
24
|
+
|
|
25
|
+
# A pandas panel, one row per company and period.
|
|
26
|
+
df = client.panel(["AAPL", "MSFT", "KO"], ["revenue", "net_income", "operating_cash_flow"],
|
|
27
|
+
period="quarterly")
|
|
28
|
+
clean = df[df.checks_failed.str.len() == 0] # drop periods that failed a check
|
|
29
|
+
|
|
30
|
+
# Failed checks and restatements across every company (Pro: 90 days, Business: all).
|
|
31
|
+
feed = client.exceptions(type="restatement")
|
|
32
|
+
client.to_frame(feed)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Derived figures (Q4 as the year minus nine months, Q2/Q3 cash flow from
|
|
36
|
+
year-to-date filings) are listed in each period's `derived`.
|
|
37
|
+
|
|
38
|
+
A call that needs a higher plan raises `balanceproof.PlanRequired`, whose
|
|
39
|
+
`required_plan` names it.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""BalanceProof: SEC fundamentals that prove they add up.
|
|
2
|
+
|
|
3
|
+
>>> import balanceproof as bp
|
|
4
|
+
>>> client = bp.Client("YOUR_KEY") # or set BALANCEPROOF_API_KEY
|
|
5
|
+
>>> client.balance_sheet("AAPL")
|
|
6
|
+
>>> client.statements("AAPL", period="quarterly")
|
|
7
|
+
>>> client.balance_sheet("AAPL", as_of="2025-03-01") # point-in-time (Pro+)
|
|
8
|
+
>>> client.panel(["AAPL", "MSFT"], ["revenue", "net_income"]) # pandas
|
|
9
|
+
|
|
10
|
+
Standard library only; pandas is needed just for `panel()` and `to_frame()`.
|
|
11
|
+
A free key is at https://balanceproof.dev/dashboard.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import datetime as _dt
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import urllib.error
|
|
20
|
+
import urllib.parse
|
|
21
|
+
import urllib.request
|
|
22
|
+
from typing import Any, Iterable
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
__all__ = ["Client", "BalanceProofError", "PlanRequired"]
|
|
26
|
+
|
|
27
|
+
DEFAULT_BASE_URL = "https://balanceproof.dev"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BalanceProofError(Exception):
|
|
31
|
+
"""The API answered with an error. `status` is the HTTP code."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, status: int, detail: Any):
|
|
34
|
+
self.status = status
|
|
35
|
+
self.detail = detail
|
|
36
|
+
super().__init__(f"{status}: {detail}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PlanRequired(BalanceProofError):
|
|
40
|
+
"""The feature needs a higher plan; `required_plan` names it."""
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def required_plan(self) -> str | None:
|
|
44
|
+
return self.detail.get("required_plan") if isinstance(self.detail, dict) else None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _date(v: Any) -> str | None:
|
|
48
|
+
if v is None:
|
|
49
|
+
return None
|
|
50
|
+
if isinstance(v, (_dt.date, _dt.datetime)):
|
|
51
|
+
return v.isoformat()[:10]
|
|
52
|
+
return str(v)[:10]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Client:
|
|
56
|
+
"""One API key against one base URL. Every method is one metered call."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, api_key: str | None = None, *, base_url: str | None = None,
|
|
59
|
+
timeout: float = 60.0):
|
|
60
|
+
self.api_key = api_key or os.environ.get("BALANCEPROOF_API_KEY", "")
|
|
61
|
+
self.base_url = (base_url or os.environ.get("BALANCEPROOF_BASE_URL")
|
|
62
|
+
or DEFAULT_BASE_URL).rstrip("/")
|
|
63
|
+
self.timeout = timeout
|
|
64
|
+
|
|
65
|
+
# -- transport ------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def _request(self, method: str, path: str, params: dict[str, Any] | None = None,
|
|
68
|
+
body: Any = None, raw: bool = False) -> Any:
|
|
69
|
+
query = {k: v for k, v in (params or {}).items() if v is not None}
|
|
70
|
+
url = self.base_url + path + ("?" + urllib.parse.urlencode(query) if query else "")
|
|
71
|
+
headers = {"Accept": "application/json",
|
|
72
|
+
"User-Agent": f"balanceproof-python/{__version__}"}
|
|
73
|
+
if self.api_key:
|
|
74
|
+
headers["X-API-Key"] = self.api_key
|
|
75
|
+
data = None
|
|
76
|
+
if body is not None:
|
|
77
|
+
data = json.dumps(body).encode()
|
|
78
|
+
headers["Content-Type"] = "application/json"
|
|
79
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
80
|
+
try:
|
|
81
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as res:
|
|
82
|
+
payload = res.read()
|
|
83
|
+
except urllib.error.HTTPError as exc:
|
|
84
|
+
text = exc.read().decode("utf-8", "replace")
|
|
85
|
+
try:
|
|
86
|
+
detail = json.loads(text).get("detail", text)
|
|
87
|
+
except ValueError:
|
|
88
|
+
detail = text
|
|
89
|
+
if exc.code == 403 and isinstance(detail, dict) and detail.get("error") == "plan_required":
|
|
90
|
+
raise PlanRequired(exc.code, detail) from None
|
|
91
|
+
raise BalanceProofError(exc.code, detail) from None
|
|
92
|
+
if raw:
|
|
93
|
+
return payload.decode("utf-8")
|
|
94
|
+
return json.loads(payload)
|
|
95
|
+
|
|
96
|
+
# -- endpoints ------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def balance_sheet(self, ticker: str, as_of: Any = None) -> dict[str, Any]:
|
|
99
|
+
"""The latest filed balance sheet, or as public on `as_of` (Pro+)."""
|
|
100
|
+
return self._request("GET", f"/api/company/{urllib.parse.quote(ticker)}",
|
|
101
|
+
{"as_of": _date(as_of)})
|
|
102
|
+
|
|
103
|
+
def history(self, ticker: str, years: int | None = None, as_of: Any = None) -> dict[str, Any]:
|
|
104
|
+
"""Every filed balance sheet, newest first. Depth is capped by plan."""
|
|
105
|
+
return self._request("GET", f"/api/company/{urllib.parse.quote(ticker)}/history",
|
|
106
|
+
{"years": years, "as_of": _date(as_of)})
|
|
107
|
+
|
|
108
|
+
def statements(self, ticker: str, period: str = "annual", years: int | None = None,
|
|
109
|
+
as_of: Any = None) -> dict[str, Any]:
|
|
110
|
+
"""Income statement and cash flow per period, each with its checks."""
|
|
111
|
+
return self._request("GET", f"/api/company/{urllib.parse.quote(ticker)}/statements",
|
|
112
|
+
{"period": period, "years": years, "as_of": _date(as_of)})
|
|
113
|
+
|
|
114
|
+
def changes(self, ticker: str) -> dict[str, Any]:
|
|
115
|
+
"""What moved since the previous period, and what was restated (Starter+)."""
|
|
116
|
+
return self._request("GET", f"/api/company/{urllib.parse.quote(ticker)}/changes")
|
|
117
|
+
|
|
118
|
+
def verify(self, tickers: Iterable[str]) -> dict[str, Any]:
|
|
119
|
+
"""Check many balance sheets at once (Pro: 50, Business: 500)."""
|
|
120
|
+
return self._request("POST", "/api/verify", body={"tickers": list(tickers)})
|
|
121
|
+
|
|
122
|
+
def exceptions(self, since: Any = None, until: Any = None, type: str | None = None, # noqa: A002
|
|
123
|
+
ticker: str | None = None, filer_only: bool = False,
|
|
124
|
+
limit: int = 500, offset: int = 0) -> dict[str, Any]:
|
|
125
|
+
"""Failed checks and restatements across all companies (Pro: 90 days)."""
|
|
126
|
+
params = {"since": _date(since), "until": _date(until), "type": type,
|
|
127
|
+
"ticker": ticker, "limit": limit, "offset": offset}
|
|
128
|
+
if filer_only:
|
|
129
|
+
params.update(type="failed_check", attribution="filer")
|
|
130
|
+
return self._request("GET", "/api/exceptions", params)
|
|
131
|
+
|
|
132
|
+
def exceptions_csv(self, **kwargs: Any) -> str:
|
|
133
|
+
"""The same feed as CSV text."""
|
|
134
|
+
params = {"since": _date(kwargs.get("since")), "until": _date(kwargs.get("until")),
|
|
135
|
+
"type": kwargs.get("type"), "ticker": kwargs.get("ticker"),
|
|
136
|
+
"limit": kwargs.get("limit", 5000), "format": "csv"}
|
|
137
|
+
return self._request("GET", "/api/exceptions", params, raw=True)
|
|
138
|
+
|
|
139
|
+
def status(self) -> dict[str, Any]:
|
|
140
|
+
"""Your plan and how many calls are left this month. Free of charge."""
|
|
141
|
+
return self._request("GET", "/api/user/status")
|
|
142
|
+
|
|
143
|
+
# -- pandas ---------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def panel(self, tickers: Iterable[str], fields: Iterable[str] | None = None,
|
|
146
|
+
period: str = "annual", years: int | None = None, as_of: Any = None):
|
|
147
|
+
"""A long pandas DataFrame: one row per (ticker, period), one column per field.
|
|
148
|
+
|
|
149
|
+
`fields` are statement metrics ("revenue", "net_income",
|
|
150
|
+
"operating_cash_flow", ...); None keeps them all. Each row also carries
|
|
151
|
+
`filing_date`, `fiscal_period`, `checks_failed` and `derived`, so a
|
|
152
|
+
backtest can drop derived or failed rows. One call per ticker.
|
|
153
|
+
"""
|
|
154
|
+
pd = _pandas()
|
|
155
|
+
wanted = list(fields) if fields is not None else None
|
|
156
|
+
rows: list[dict[str, Any]] = []
|
|
157
|
+
for t in tickers:
|
|
158
|
+
try:
|
|
159
|
+
body = self.statements(t, period=period, years=years, as_of=as_of)
|
|
160
|
+
except BalanceProofError as exc:
|
|
161
|
+
if exc.status == 404:
|
|
162
|
+
continue
|
|
163
|
+
raise
|
|
164
|
+
for p in body["periods"]:
|
|
165
|
+
values = {**p["income_statement"], **p["cash_flow"]}
|
|
166
|
+
row = {"ticker": body["ticker"], "period_end": p["period_end"],
|
|
167
|
+
"fiscal_period": p["fiscal_period"], "filing_date": p["filing_date"]}
|
|
168
|
+
row.update({k: v for k, v in values.items() if wanted is None or k in wanted})
|
|
169
|
+
row["checks_failed"] = [c["check"] for c in p["checks"] if c["status"] == "failed"]
|
|
170
|
+
row["derived"] = p["derived"]
|
|
171
|
+
rows.append(row)
|
|
172
|
+
df = pd.DataFrame(rows)
|
|
173
|
+
for col in ("period_end", "filing_date"):
|
|
174
|
+
if col in df:
|
|
175
|
+
df[col] = pd.to_datetime(df[col])
|
|
176
|
+
return df
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def to_frame(payload: dict[str, Any]):
|
|
180
|
+
"""Any list-bearing response (history, statements, exceptions) as a DataFrame."""
|
|
181
|
+
pd = _pandas()
|
|
182
|
+
for key in ("events", "periods", "balance_sheets", "results"):
|
|
183
|
+
if key in payload:
|
|
184
|
+
return pd.json_normalize(payload[key])
|
|
185
|
+
return pd.json_normalize(payload)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _pandas():
|
|
189
|
+
try:
|
|
190
|
+
import pandas as pd
|
|
191
|
+
except ImportError as exc: # pragma: no cover - depends on the environment
|
|
192
|
+
raise ImportError("panel() and to_frame() need pandas: pip install pandas") from exc
|
|
193
|
+
return pd
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: balanceproof
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SEC fundamentals that prove they add up: checked balance sheets, income statements and cash flow, point-in-time.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://balanceproof.dev
|
|
7
|
+
Project-URL: Documentation, https://balanceproof.dev/api
|
|
8
|
+
Keywords: sec,edgar,xbrl,fundamentals,balance sheet,backtesting,point-in-time
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
11
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Provides-Extra: pandas
|
|
15
|
+
Requires-Dist: pandas>=1.5; extra == "pandas"
|
|
16
|
+
|
|
17
|
+
# balanceproof
|
|
18
|
+
|
|
19
|
+
Python client for [BalanceProof](https://balanceproof.dev): US public-company
|
|
20
|
+
fundamentals from SEC EDGAR, where every balance sheet, income statement and
|
|
21
|
+
cash flow is checked against its own totals, and a filing that does not add up
|
|
22
|
+
is flagged with the reason instead of passed through.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install "balanceproof[pandas]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import balanceproof as bp
|
|
30
|
+
|
|
31
|
+
client = bp.Client("YOUR_KEY") # free key: https://balanceproof.dev/dashboard
|
|
32
|
+
# or set BALANCEPROOF_API_KEY
|
|
33
|
+
|
|
34
|
+
client.balance_sheet("AAPL")
|
|
35
|
+
client.statements("AAPL", period="quarterly")
|
|
36
|
+
|
|
37
|
+
# Point-in-time (Pro and above): only what had been filed by that date,
|
|
38
|
+
# so a backtest never sees a figure before it was public.
|
|
39
|
+
client.balance_sheet("AAPL", as_of="2025-03-01")
|
|
40
|
+
|
|
41
|
+
# A pandas panel, one row per company and period.
|
|
42
|
+
df = client.panel(["AAPL", "MSFT", "KO"], ["revenue", "net_income", "operating_cash_flow"],
|
|
43
|
+
period="quarterly")
|
|
44
|
+
clean = df[df.checks_failed.str.len() == 0] # drop periods that failed a check
|
|
45
|
+
|
|
46
|
+
# Failed checks and restatements across every company (Pro: 90 days, Business: all).
|
|
47
|
+
feed = client.exceptions(type="restatement")
|
|
48
|
+
client.to_frame(feed)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Derived figures (Q4 as the year minus nine months, Q2/Q3 cash flow from
|
|
52
|
+
year-to-date filings) are listed in each period's `derived`.
|
|
53
|
+
|
|
54
|
+
A call that needs a higher plan raises `balanceproof.PlanRequired`, whose
|
|
55
|
+
`required_plan` names it.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
balanceproof
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "balanceproof"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SEC fundamentals that prove they add up: checked balance sheets, income statements and cash flow, point-in-time."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = []
|
|
13
|
+
keywords = ["sec", "edgar", "xbrl", "fundamentals", "balance sheet", "backtesting", "point-in-time"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
17
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
pandas = ["pandas>=1.5"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://balanceproof.dev"
|
|
25
|
+
Documentation = "https://balanceproof.dev/api"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
packages = ["balanceproof"]
|