hfdatalibrary 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.
- hfdatalibrary-0.1.0/PKG-INFO +76 -0
- hfdatalibrary-0.1.0/README.md +44 -0
- hfdatalibrary-0.1.0/hfdatalibrary/__init__.py +41 -0
- hfdatalibrary-0.1.0/hfdatalibrary/client.py +172 -0
- hfdatalibrary-0.1.0/hfdatalibrary.egg-info/PKG-INFO +76 -0
- hfdatalibrary-0.1.0/hfdatalibrary.egg-info/SOURCES.txt +13 -0
- hfdatalibrary-0.1.0/hfdatalibrary.egg-info/dependency_links.txt +1 -0
- hfdatalibrary-0.1.0/hfdatalibrary.egg-info/requires.txt +5 -0
- hfdatalibrary-0.1.0/hfdatalibrary.egg-info/top_level.txt +1 -0
- hfdatalibrary-0.1.0/pyproject.toml +45 -0
- hfdatalibrary-0.1.0/setup.cfg +4 -0
- hfdatalibrary-0.1.0/tests/test_backtest.py +60 -0
- hfdatalibrary-0.1.0/tests/test_coverage.py +79 -0
- hfdatalibrary-0.1.0/tests/test_panel.py +53 -0
- hfdatalibrary-0.1.0/tests/test_universe.py +49 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hfdatalibrary
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the HF Data Library — free 1-minute OHLCV data for U.S. equities and ETFs
|
|
5
|
+
Author: Ahmed Elkassabgi
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://hfdatalibrary.com
|
|
8
|
+
Project-URL: Documentation, https://hfdatalibrary.com/pages/docs
|
|
9
|
+
Project-URL: Source, https://github.com/elkassabgi/hfdatalibrary
|
|
10
|
+
Project-URL: Issues, https://github.com/elkassabgi/hfdatalibrary/issues
|
|
11
|
+
Keywords: finance,stock-market,ohlcv,intraday,high-frequency,backtesting,research-data
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
Requires-Dist: requests>=2.20
|
|
29
|
+
Requires-Dist: pandas>=1.0
|
|
30
|
+
Provides-Extra: parquet
|
|
31
|
+
Requires-Dist: pyarrow>=6.0; extra == "parquet"
|
|
32
|
+
|
|
33
|
+
# hfdatalibrary (Python)
|
|
34
|
+
|
|
35
|
+
Python client for the [HF Data Library](https://hfdatalibrary.com) — free,
|
|
36
|
+
research-grade 1-minute OHLCV data for ~1,391 U.S. equities and ETFs, in
|
|
37
|
+
both `raw` and `clean` versions across eight timeframes.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install hfdatalibrary # add pyarrow for parquet: pip install hfdatalibrary[parquet]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Authenticate
|
|
46
|
+
|
|
47
|
+
Get a free API key at <https://hfdatalibrary.com/pages/account>, then:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import hfdatalibrary as hfdl
|
|
51
|
+
hfdl.set_key("YOUR_API_KEY") # or set the HFDL_API_KEY environment variable
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Use
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
hfdl.symbols() # -> ['A', 'AA', 'AAPL', ...]
|
|
58
|
+
df = hfdl.get("AAPL") # clean 1-minute bars (pandas DataFrame)
|
|
59
|
+
df = hfdl.get("AAPL", version="raw", timeframe="daily")
|
|
60
|
+
panel = hfdl.get(["AAPL", "MSFT", "SPY"]) # -> {ticker: DataFrame}
|
|
61
|
+
df = hfdl.get("AAPL", fmt="csv") # CSV instead of parquet (no pyarrow needed)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Parameters: `version` ∈ {`clean`, `raw`}; `timeframe` ∈ {`1min`,`5min`,`15min`,`30min`,`hourly`,`daily`,`weekly`,`monthly`}.
|
|
65
|
+
|
|
66
|
+
## Important: survivorship bias
|
|
67
|
+
|
|
68
|
+
The universe is a fixed snapshot (~2023) carried back to 2002, so pre-2022
|
|
69
|
+
history is **survivor-conditioned** — companies that delisted before ~2021 are
|
|
70
|
+
absent. Not suitable for survivorship-sensitive backtests over 2002–2021
|
|
71
|
+
without adjustment. See the [methodology docs](https://hfdatalibrary.com/pages/docs)
|
|
72
|
+
for full limitations (including the post-March-2022 IEX-only volume caveat).
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT (client code). Data is CC BY 4.0 — cite per <https://hfdatalibrary.com/pages/cite>.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# hfdatalibrary (Python)
|
|
2
|
+
|
|
3
|
+
Python client for the [HF Data Library](https://hfdatalibrary.com) — free,
|
|
4
|
+
research-grade 1-minute OHLCV data for ~1,391 U.S. equities and ETFs, in
|
|
5
|
+
both `raw` and `clean` versions across eight timeframes.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install hfdatalibrary # add pyarrow for parquet: pip install hfdatalibrary[parquet]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Authenticate
|
|
14
|
+
|
|
15
|
+
Get a free API key at <https://hfdatalibrary.com/pages/account>, then:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import hfdatalibrary as hfdl
|
|
19
|
+
hfdl.set_key("YOUR_API_KEY") # or set the HFDL_API_KEY environment variable
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Use
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
hfdl.symbols() # -> ['A', 'AA', 'AAPL', ...]
|
|
26
|
+
df = hfdl.get("AAPL") # clean 1-minute bars (pandas DataFrame)
|
|
27
|
+
df = hfdl.get("AAPL", version="raw", timeframe="daily")
|
|
28
|
+
panel = hfdl.get(["AAPL", "MSFT", "SPY"]) # -> {ticker: DataFrame}
|
|
29
|
+
df = hfdl.get("AAPL", fmt="csv") # CSV instead of parquet (no pyarrow needed)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Parameters: `version` ∈ {`clean`, `raw`}; `timeframe` ∈ {`1min`,`5min`,`15min`,`30min`,`hourly`,`daily`,`weekly`,`monthly`}.
|
|
33
|
+
|
|
34
|
+
## Important: survivorship bias
|
|
35
|
+
|
|
36
|
+
The universe is a fixed snapshot (~2023) carried back to 2002, so pre-2022
|
|
37
|
+
history is **survivor-conditioned** — companies that delisted before ~2021 are
|
|
38
|
+
absent. Not suitable for survivorship-sensitive backtests over 2002–2021
|
|
39
|
+
without adjustment. See the [methodology docs](https://hfdatalibrary.com/pages/docs)
|
|
40
|
+
for full limitations (including the post-March-2022 IEX-only volume caveat).
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT (client code). Data is CC BY 4.0 — cite per <https://hfdatalibrary.com/pages/cite>.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""hfdatalibrary — Python client for the HF Data Library.
|
|
2
|
+
|
|
3
|
+
Free, research-grade 1-minute OHLCV data for U.S. equities and ETFs.
|
|
4
|
+
|
|
5
|
+
Quick start:
|
|
6
|
+
import hfdatalibrary as hfdl
|
|
7
|
+
hfdl.set_key("YOUR_API_KEY") # or set HFDL_API_KEY in the environment
|
|
8
|
+
|
|
9
|
+
universe = hfdl.symbols() # list available tickers
|
|
10
|
+
df = hfdl.get("AAPL") # clean 1-minute bars -> pandas DataFrame
|
|
11
|
+
df = hfdl.get("AAPL", version="raw", timeframe="daily")
|
|
12
|
+
panel = hfdl.get(["AAPL", "MSFT"]) # dict {ticker: DataFrame}
|
|
13
|
+
|
|
14
|
+
Data is survivorship-biased (constituents fixed ~2023; pre-2022 history is
|
|
15
|
+
survivor-conditioned). See https://hfdatalibrary.com/pages/docs for limitations.
|
|
16
|
+
"""
|
|
17
|
+
from .client import (
|
|
18
|
+
Client,
|
|
19
|
+
set_key,
|
|
20
|
+
symbols,
|
|
21
|
+
get,
|
|
22
|
+
VERSIONS,
|
|
23
|
+
TIMEFRAMES,
|
|
24
|
+
HFDLError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
__all__ = [
|
|
29
|
+
"Client", "set_key", "symbols", "get",
|
|
30
|
+
"VERSIONS", "TIMEFRAMES", "HFDLError", "lab", "__version__",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def __getattr__(name):
|
|
35
|
+
# Lazy access to the recipe layer so `import hfdatalibrary` stays light and
|
|
36
|
+
# never pulls optional analysis deps at package load. `hfdl.lab` / `from
|
|
37
|
+
# hfdatalibrary import lab` both work.
|
|
38
|
+
if name == "lab":
|
|
39
|
+
import importlib
|
|
40
|
+
return importlib.import_module("hfdatalibrary.lab")
|
|
41
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Core client for the HF Data Library API.
|
|
2
|
+
|
|
3
|
+
Wraps https://api.hfdatalibrary.com. Auth is via an API key sent in the
|
|
4
|
+
X-API-Key header (get one at https://hfdatalibrary.com/pages/account).
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import io
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
from typing import Dict, List, Optional, Union
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
BASE_URL = os.environ.get("HFDL_BASE_URL", "https://api.hfdatalibrary.com")
|
|
16
|
+
VERSIONS = ("clean", "raw")
|
|
17
|
+
TIMEFRAMES = ("1min", "5min", "15min", "30min", "hourly", "daily", "weekly", "monthly")
|
|
18
|
+
|
|
19
|
+
_DEFAULT_KEY = os.environ.get("HFDL_API_KEY")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HFDLError(Exception):
|
|
23
|
+
"""Raised on API or client errors."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_key(api_key: str) -> None:
|
|
27
|
+
"""Set the API key for the module-level default client."""
|
|
28
|
+
global _DEFAULT_KEY
|
|
29
|
+
_DEFAULT_KEY = api_key
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Client:
|
|
33
|
+
"""A configured HF Data Library client.
|
|
34
|
+
|
|
35
|
+
Prefer the module-level functions (hfdl.get, hfdl.symbols) for simple use;
|
|
36
|
+
instantiate Client directly to hold a key explicitly or tune the session.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
api_key: Optional[str] = None,
|
|
42
|
+
base_url: str = BASE_URL,
|
|
43
|
+
timeout: int = 120,
|
|
44
|
+
max_retries: int = 3,
|
|
45
|
+
session: Optional[requests.Session] = None,
|
|
46
|
+
):
|
|
47
|
+
self.api_key = api_key or _DEFAULT_KEY
|
|
48
|
+
self.base_url = base_url.rstrip("/")
|
|
49
|
+
self.timeout = timeout
|
|
50
|
+
self.max_retries = max_retries
|
|
51
|
+
self.session = session or requests.Session()
|
|
52
|
+
self.session.headers.update({"User-Agent": "hfdatalibrary-python/0.1.0"})
|
|
53
|
+
|
|
54
|
+
# ---- low-level request with retry/backoff -----------------------------
|
|
55
|
+
def _request(self, path: str, params: Optional[dict] = None, auth: bool = True) -> requests.Response:
|
|
56
|
+
url = f"{self.base_url}{path}"
|
|
57
|
+
headers = {}
|
|
58
|
+
if auth:
|
|
59
|
+
if not self.api_key:
|
|
60
|
+
raise HFDLError(
|
|
61
|
+
"No API key set. Call hfdl.set_key('...'), pass api_key=..., "
|
|
62
|
+
"or set the HFDL_API_KEY environment variable. "
|
|
63
|
+
"Get a key at https://hfdatalibrary.com/pages/account"
|
|
64
|
+
)
|
|
65
|
+
headers["X-API-Key"] = self.api_key
|
|
66
|
+
|
|
67
|
+
last_exc = None
|
|
68
|
+
for attempt in range(self.max_retries):
|
|
69
|
+
try:
|
|
70
|
+
r = self.session.get(url, params=params, headers=headers, timeout=self.timeout)
|
|
71
|
+
except requests.RequestException as e:
|
|
72
|
+
last_exc = e
|
|
73
|
+
time.sleep(1.5 * (attempt + 1))
|
|
74
|
+
continue
|
|
75
|
+
if r.status_code == 200:
|
|
76
|
+
return r
|
|
77
|
+
if r.status_code in (401, 403):
|
|
78
|
+
raise HFDLError(f"Authentication failed ({r.status_code}). Check your API key.")
|
|
79
|
+
if r.status_code == 404:
|
|
80
|
+
raise HFDLError(f"Not found: {path} (params={params}). Check the ticker/timeframe/version.")
|
|
81
|
+
if r.status_code == 429:
|
|
82
|
+
# rate limited — honor Retry-After if present
|
|
83
|
+
wait = int(r.headers.get("Retry-After", 5 * (attempt + 1)))
|
|
84
|
+
time.sleep(wait)
|
|
85
|
+
continue
|
|
86
|
+
if 500 <= r.status_code < 600:
|
|
87
|
+
last_exc = HFDLError(f"Server error {r.status_code}")
|
|
88
|
+
time.sleep(2.0 * (attempt + 1))
|
|
89
|
+
continue
|
|
90
|
+
raise HFDLError(f"HTTP {r.status_code}: {r.text[:200]}")
|
|
91
|
+
raise HFDLError(f"Request to {path} failed after {self.max_retries} attempts: {last_exc}")
|
|
92
|
+
|
|
93
|
+
# ---- public API -------------------------------------------------------
|
|
94
|
+
def symbols(self) -> List[str]:
|
|
95
|
+
"""Return the list of available ticker symbols (no auth required)."""
|
|
96
|
+
r = self._request("/v1/symbols", auth=False)
|
|
97
|
+
data = r.json()
|
|
98
|
+
syms = data.get("symbols", data) if isinstance(data, dict) else data
|
|
99
|
+
# symbols may be list[str] or list[dict]; normalize to list[str]
|
|
100
|
+
out = []
|
|
101
|
+
for s in syms:
|
|
102
|
+
out.append(s["ticker"] if isinstance(s, dict) else s)
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
def get(
|
|
106
|
+
self,
|
|
107
|
+
ticker: Union[str, List[str]],
|
|
108
|
+
version: str = "clean",
|
|
109
|
+
timeframe: str = "1min",
|
|
110
|
+
fmt: str = "parquet",
|
|
111
|
+
):
|
|
112
|
+
"""Fetch bars for one or many tickers.
|
|
113
|
+
|
|
114
|
+
Returns a pandas DataFrame for a single ticker, or a dict
|
|
115
|
+
{ticker: DataFrame} when `ticker` is a list.
|
|
116
|
+
|
|
117
|
+
version: 'clean' (default) or 'raw'
|
|
118
|
+
timeframe: one of TIMEFRAMES
|
|
119
|
+
fmt: 'parquet' (default, needs pyarrow) or 'csv'
|
|
120
|
+
"""
|
|
121
|
+
if version not in VERSIONS:
|
|
122
|
+
raise HFDLError(f"version must be one of {VERSIONS}")
|
|
123
|
+
if timeframe not in TIMEFRAMES:
|
|
124
|
+
raise HFDLError(f"timeframe must be one of {TIMEFRAMES}")
|
|
125
|
+
if fmt not in ("parquet", "csv"):
|
|
126
|
+
raise HFDLError("fmt must be 'parquet' or 'csv'")
|
|
127
|
+
|
|
128
|
+
if isinstance(ticker, (list, tuple, set)):
|
|
129
|
+
return {t: self._get_one(t, version, timeframe, fmt) for t in ticker}
|
|
130
|
+
return self._get_one(ticker, version, timeframe, fmt)
|
|
131
|
+
|
|
132
|
+
def _get_one(self, ticker: str, version: str, timeframe: str, fmt: str):
|
|
133
|
+
params = {"version": version, "timeframe": timeframe, "format": fmt}
|
|
134
|
+
r = self._request(f"/v1/download/{ticker.upper()}", params=params)
|
|
135
|
+
content = r.content
|
|
136
|
+
try:
|
|
137
|
+
import pandas as pd
|
|
138
|
+
except ImportError as e:
|
|
139
|
+
raise HFDLError("pandas is required to return DataFrames: pip install pandas") from e
|
|
140
|
+
|
|
141
|
+
if fmt == "csv":
|
|
142
|
+
return pd.read_csv(io.BytesIO(content))
|
|
143
|
+
# parquet
|
|
144
|
+
try:
|
|
145
|
+
return pd.read_parquet(io.BytesIO(content))
|
|
146
|
+
except ImportError as e:
|
|
147
|
+
raise HFDLError(
|
|
148
|
+
"pyarrow is required to read parquet: pip install pyarrow "
|
|
149
|
+
"(or call get(..., fmt='csv'))"
|
|
150
|
+
) from e
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---- module-level convenience (uses a lazily-built default client) --------
|
|
154
|
+
_default_client: Optional[Client] = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _client() -> Client:
|
|
158
|
+
global _default_client
|
|
159
|
+
# rebuild if key changed via set_key after first use
|
|
160
|
+
if _default_client is None or _default_client.api_key != _DEFAULT_KEY:
|
|
161
|
+
_default_client = Client(api_key=_DEFAULT_KEY)
|
|
162
|
+
return _default_client
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def symbols() -> List[str]:
|
|
166
|
+
"""Module-level: list available tickers."""
|
|
167
|
+
return _client().symbols()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get(ticker, version: str = "clean", timeframe: str = "1min", fmt: str = "parquet"):
|
|
171
|
+
"""Module-level: fetch bars. See Client.get."""
|
|
172
|
+
return _client().get(ticker, version=version, timeframe=timeframe, fmt=fmt)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hfdatalibrary
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the HF Data Library — free 1-minute OHLCV data for U.S. equities and ETFs
|
|
5
|
+
Author: Ahmed Elkassabgi
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://hfdatalibrary.com
|
|
8
|
+
Project-URL: Documentation, https://hfdatalibrary.com/pages/docs
|
|
9
|
+
Project-URL: Source, https://github.com/elkassabgi/hfdatalibrary
|
|
10
|
+
Project-URL: Issues, https://github.com/elkassabgi/hfdatalibrary/issues
|
|
11
|
+
Keywords: finance,stock-market,ohlcv,intraday,high-frequency,backtesting,research-data
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
Requires-Dist: requests>=2.20
|
|
29
|
+
Requires-Dist: pandas>=1.0
|
|
30
|
+
Provides-Extra: parquet
|
|
31
|
+
Requires-Dist: pyarrow>=6.0; extra == "parquet"
|
|
32
|
+
|
|
33
|
+
# hfdatalibrary (Python)
|
|
34
|
+
|
|
35
|
+
Python client for the [HF Data Library](https://hfdatalibrary.com) — free,
|
|
36
|
+
research-grade 1-minute OHLCV data for ~1,391 U.S. equities and ETFs, in
|
|
37
|
+
both `raw` and `clean` versions across eight timeframes.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install hfdatalibrary # add pyarrow for parquet: pip install hfdatalibrary[parquet]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Authenticate
|
|
46
|
+
|
|
47
|
+
Get a free API key at <https://hfdatalibrary.com/pages/account>, then:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import hfdatalibrary as hfdl
|
|
51
|
+
hfdl.set_key("YOUR_API_KEY") # or set the HFDL_API_KEY environment variable
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Use
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
hfdl.symbols() # -> ['A', 'AA', 'AAPL', ...]
|
|
58
|
+
df = hfdl.get("AAPL") # clean 1-minute bars (pandas DataFrame)
|
|
59
|
+
df = hfdl.get("AAPL", version="raw", timeframe="daily")
|
|
60
|
+
panel = hfdl.get(["AAPL", "MSFT", "SPY"]) # -> {ticker: DataFrame}
|
|
61
|
+
df = hfdl.get("AAPL", fmt="csv") # CSV instead of parquet (no pyarrow needed)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Parameters: `version` ∈ {`clean`, `raw`}; `timeframe` ∈ {`1min`,`5min`,`15min`,`30min`,`hourly`,`daily`,`weekly`,`monthly`}.
|
|
65
|
+
|
|
66
|
+
## Important: survivorship bias
|
|
67
|
+
|
|
68
|
+
The universe is a fixed snapshot (~2023) carried back to 2002, so pre-2022
|
|
69
|
+
history is **survivor-conditioned** — companies that delisted before ~2021 are
|
|
70
|
+
absent. Not suitable for survivorship-sensitive backtests over 2002–2021
|
|
71
|
+
without adjustment. See the [methodology docs](https://hfdatalibrary.com/pages/docs)
|
|
72
|
+
for full limitations (including the post-March-2022 IEX-only volume caveat).
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT (client code). Data is CC BY 4.0 — cite per <https://hfdatalibrary.com/pages/cite>.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
hfdatalibrary/__init__.py
|
|
4
|
+
hfdatalibrary/client.py
|
|
5
|
+
hfdatalibrary.egg-info/PKG-INFO
|
|
6
|
+
hfdatalibrary.egg-info/SOURCES.txt
|
|
7
|
+
hfdatalibrary.egg-info/dependency_links.txt
|
|
8
|
+
hfdatalibrary.egg-info/requires.txt
|
|
9
|
+
hfdatalibrary.egg-info/top_level.txt
|
|
10
|
+
tests/test_backtest.py
|
|
11
|
+
tests/test_coverage.py
|
|
12
|
+
tests/test_panel.py
|
|
13
|
+
tests/test_universe.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hfdatalibrary
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hfdatalibrary"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python client for the HF Data Library — free 1-minute OHLCV data for U.S. equities and ETFs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Ahmed Elkassabgi" }]
|
|
13
|
+
keywords = ["finance", "stock-market", "ohlcv", "intraday", "high-frequency", "backtesting", "research-data"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"requests>=2.20",
|
|
32
|
+
"pandas>=1.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
parquet = ["pyarrow>=6.0"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://hfdatalibrary.com"
|
|
40
|
+
Documentation = "https://hfdatalibrary.com/pages/docs"
|
|
41
|
+
Source = "https://github.com/elkassabgi/hfdatalibrary"
|
|
42
|
+
Issues = "https://github.com/elkassabgi/hfdatalibrary/issues"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools]
|
|
45
|
+
packages = ["hfdatalibrary"]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""backtest_momentum: runs no-look-ahead, enforces the survivorship hard stop,
|
|
2
|
+
and reports the pre/post-2022 split. Uses synthetic random-walk prices (no key)."""
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from hfdatalibrary.lab import backtest_momentum, BacktestResult, SurvivorshipBiasError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _panel(start, periods, n=20, seed=0):
|
|
13
|
+
rng = np.random.default_rng(seed)
|
|
14
|
+
idx = pd.date_range(start, periods=periods, freq="B")
|
|
15
|
+
rets = rng.normal(0.0003, 0.01, size=(periods, n))
|
|
16
|
+
prices = 100 * np.exp(np.cumsum(rets, axis=0))
|
|
17
|
+
cols = [f"T{i:02d}" for i in range(n - 1)] + ["SPY"]
|
|
18
|
+
return pd.DataFrame(prices, index=idx, columns=cols)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_post2022_runs_with_split_and_benchmark():
|
|
22
|
+
p = _panel("2022-02-01", 400)
|
|
23
|
+
res = backtest_momentum(p, lookback=60, skip=5, top_n=5, optimize="equal",
|
|
24
|
+
benchmark="SPY")
|
|
25
|
+
assert isinstance(res, BacktestResult)
|
|
26
|
+
assert len(res.equity) > 0
|
|
27
|
+
for k in ("total_return", "sharpe", "max_drawdown", "ann_vol"):
|
|
28
|
+
assert k in res.stats
|
|
29
|
+
assert "pre2022" in res.stats and "post2022" in res.stats
|
|
30
|
+
assert "benchmark_total_return" in res.stats
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_pre2022_hard_stops_without_ack():
|
|
34
|
+
p = _panel("2018-01-01", 800)
|
|
35
|
+
with pytest.raises(SurvivorshipBiasError):
|
|
36
|
+
backtest_momentum(p, lookback=60, skip=5, top_n=5, optimize="equal")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_pre2022_runs_with_ack_and_records():
|
|
40
|
+
p = _panel("2018-01-01", 800)
|
|
41
|
+
with warnings.catch_warnings():
|
|
42
|
+
warnings.simplefilter("ignore")
|
|
43
|
+
res = backtest_momentum(p, lookback=60, skip=5, top_n=5, optimize="equal",
|
|
44
|
+
acknowledge_survivorship=True)
|
|
45
|
+
assert res.coverage.acknowledged
|
|
46
|
+
assert len(res.equity) > 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_no_lookahead_equity_is_clean():
|
|
50
|
+
p = _panel("2022-02-01", 300)
|
|
51
|
+
res = backtest_momentum(p, lookback=40, skip=3, top_n=4, optimize="equal")
|
|
52
|
+
assert res.equity.notna().all()
|
|
53
|
+
# momentum is price-only -> the IEX *volume* caveat must NOT be raised
|
|
54
|
+
assert not any("IEX VOLUME" in w for w in res.warnings)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_inverse_vol_weighting_runs():
|
|
58
|
+
p = _panel("2022-02-01", 300, seed=3)
|
|
59
|
+
res = backtest_momentum(p, lookback=40, skip=3, top_n=6, optimize="inverse_vol")
|
|
60
|
+
assert len(res.weights) > 0
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""The guardrails must be loud and never silent: survivorship is a HARD STOP for
|
|
2
|
+
cumulative pre-2022 backtests unless acknowledged; the IEX-volume caveat fires
|
|
3
|
+
only when a strategy actually uses volume."""
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from hfdatalibrary.lab import (
|
|
10
|
+
audit_panel, enforce_survivorship,
|
|
11
|
+
CaveatWarning, SurvivorshipBiasError, CoverageReport,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _panel(start, end, n=3):
|
|
16
|
+
idx = pd.date_range(start, end, freq="B")
|
|
17
|
+
return pd.DataFrame({f"T{i}": [100.0] * len(idx) for i in range(n)}, index=idx)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_pre2022_is_severe_and_hard_stops():
|
|
21
|
+
rep = audit_panel(_panel("2008-01-01", "2020-12-31"))
|
|
22
|
+
assert rep.survivorship == "severe"
|
|
23
|
+
assert rep.pre_2022 and not rep.point_in_time_safe
|
|
24
|
+
with pytest.raises(SurvivorshipBiasError):
|
|
25
|
+
enforce_survivorship(rep, acknowledge=False, cumulative=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_acknowledge_allows_and_records():
|
|
29
|
+
rep = audit_panel(_panel("2008-01-01", "2020-12-31"))
|
|
30
|
+
with warnings.catch_warnings():
|
|
31
|
+
warnings.simplefilter("ignore")
|
|
32
|
+
out = enforce_survivorship(rep, acknowledge=True, cumulative=True)
|
|
33
|
+
assert out.acknowledged
|
|
34
|
+
assert any("SURV_ACK" in n for n in out.notes)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_non_cumulative_does_not_hard_stop():
|
|
38
|
+
rep = audit_panel(_panel("2008-01-01", "2020-12-31"))
|
|
39
|
+
with warnings.catch_warnings():
|
|
40
|
+
warnings.simplefilter("ignore")
|
|
41
|
+
enforce_survivorship(rep, acknowledge=False, cumulative=False) # must not raise
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_post2022_is_clean():
|
|
45
|
+
rep = audit_panel(_panel("2022-06-01", "2024-01-01"))
|
|
46
|
+
assert rep.survivorship == "none"
|
|
47
|
+
assert rep.point_in_time_safe
|
|
48
|
+
enforce_survivorship(rep) # no raise
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_iex_volume_warns_only_when_volume_used():
|
|
52
|
+
with_vol = audit_panel(_panel("2022-06-01", "2023-06-01"), uses_volume=True)
|
|
53
|
+
assert any("IEX VOLUME" in w for w in with_vol.warnings)
|
|
54
|
+
|
|
55
|
+
no_vol = audit_panel(_panel("2022-06-01", "2023-06-01"), uses_volume=False)
|
|
56
|
+
assert not any("IEX VOLUME" in w for w in no_vol.warnings)
|
|
57
|
+
assert any("IEX SEGMENT" in n for n in no_vol.notes)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_source_column_overrides_date_inference():
|
|
61
|
+
# window is entirely pre-break by date, but the source column says IEX
|
|
62
|
+
rep = audit_panel(start="2021-01-01", end="2021-06-01", n_tickers=2,
|
|
63
|
+
sources={"iex"})
|
|
64
|
+
assert rep.iex_segment is True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_missing_tickers_recorded():
|
|
68
|
+
rep = audit_panel(_panel("2023-01-01", "2023-06-01"),
|
|
69
|
+
missing_tickers=["XYZ", "ABC"])
|
|
70
|
+
assert "XYZ" in rep.missing_tickers
|
|
71
|
+
assert any("skipped" in n for n in rep.notes)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_caveat_warning_can_escalate_to_error():
|
|
75
|
+
rep = audit_panel(_panel("2022-06-01", "2023-06-01"), uses_volume=True)
|
|
76
|
+
with warnings.catch_warnings():
|
|
77
|
+
warnings.simplefilter("error", CaveatWarning)
|
|
78
|
+
with pytest.raises(CaveatWarning):
|
|
79
|
+
enforce_survivorship(rep, acknowledge=False, cumulative=False)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""load_panel: pivots many tickers, skips 404s honestly (client RAISES, not None),
|
|
2
|
+
filters dates, and attaches a CoverageReport using the real source column."""
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from hfdatalibrary.lab import load_panel
|
|
7
|
+
from hfdatalibrary.client import HFDLError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _ticker_df(start="2022-01-03", periods=200, source="iex", price=100.0):
|
|
11
|
+
idx = pd.date_range(start, periods=periods, freq="B")
|
|
12
|
+
return pd.DataFrame({
|
|
13
|
+
"Date": idx.astype(str),
|
|
14
|
+
"Open": price, "High": price + 1, "Low": price - 1, "Close": price,
|
|
15
|
+
"Volume": 1000, "source": source,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_pivots_and_skips_missing():
|
|
20
|
+
data = {"AAA": _ticker_df(price=100), "BBB": _ticker_df(price=50)}
|
|
21
|
+
|
|
22
|
+
def getter(t, version="clean", timeframe="daily"):
|
|
23
|
+
if t == "CCC":
|
|
24
|
+
raise HFDLError("Not found: CCC") # the client raises on 404
|
|
25
|
+
return data[t]
|
|
26
|
+
|
|
27
|
+
wide = load_panel(["AAA", "BBB", "CCC"], field="Close", timeframe="daily",
|
|
28
|
+
cache=False, throttle=0, getter=getter)
|
|
29
|
+
assert list(wide.columns) == ["AAA", "BBB"]
|
|
30
|
+
assert isinstance(wide.index, pd.DatetimeIndex)
|
|
31
|
+
cov = wide.attrs["coverage"]
|
|
32
|
+
assert "CCC" in cov.missing_tickers
|
|
33
|
+
assert cov.iex_segment is True # source column == 'iex'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_date_filter():
|
|
37
|
+
df = _ticker_df(start="2020-01-01", periods=800, source="pitrading")
|
|
38
|
+
|
|
39
|
+
def getter(t, **k):
|
|
40
|
+
return df
|
|
41
|
+
|
|
42
|
+
wide = load_panel(["AAA"], cache=False, throttle=0, getter=getter,
|
|
43
|
+
start="2021-01-01", end="2021-12-31")
|
|
44
|
+
assert wide.index.min() >= pd.Timestamp("2021-01-01")
|
|
45
|
+
assert wide.index.max() <= pd.Timestamp("2021-12-31")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_all_missing_raises():
|
|
49
|
+
def getter(t, **k):
|
|
50
|
+
raise HFDLError("Not found")
|
|
51
|
+
|
|
52
|
+
with pytest.raises(HFDLError):
|
|
53
|
+
load_panel(["X", "Y"], cache=False, throttle=0, getter=getter)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Universe counts must match the REAL survivor-snapshot, never the nominal index
|
|
2
|
+
size (reviewer fix #1). Verified from data/ticker_meta.json: sp500=550,
|
|
3
|
+
nasdaq100=81, dow30=30, etf=569, stock=822, total=1391."""
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from hfdatalibrary.lab import universe, index_coverage, CaveatWarning
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_sp500_count():
|
|
12
|
+
assert len(universe("sp500")) == 550
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_dow30_count():
|
|
16
|
+
assert len(universe("dow30")) == 30
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_nasdaq100_count_and_warns():
|
|
20
|
+
with warnings.catch_warnings(record=True) as rec:
|
|
21
|
+
warnings.simplefilter("always")
|
|
22
|
+
u = universe("nasdaq100")
|
|
23
|
+
assert len(u) == 81
|
|
24
|
+
assert any(issubclass(w.category, CaveatWarning) and "81 of 100" in str(w.message)
|
|
25
|
+
for w in rec), "nasdaq100 must warn about the 81-of-100 survivorship gap"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_type_counts():
|
|
29
|
+
assert len(universe("etf")) == 569
|
|
30
|
+
assert len(universe("stock")) == 822
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_all_is_full_snapshot():
|
|
34
|
+
assert len(universe("all")) == 1391
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_sector():
|
|
38
|
+
assert len(universe("sector:Healthcare")) > 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_unknown_raises():
|
|
42
|
+
with pytest.raises(ValueError):
|
|
43
|
+
universe("bogus_index")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_index_coverage_diagnostic():
|
|
47
|
+
cov = index_coverage()
|
|
48
|
+
assert cov["nasdaq100"] == {"present": 81, "nominal": 100}
|
|
49
|
+
assert cov["dow30"] == {"present": 30, "nominal": 30}
|