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.
@@ -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,5 @@
1
+ requests>=2.20
2
+ pandas>=1.0
3
+
4
+ [parquet]
5
+ pyarrow>=6.0
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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}