quantpad-data 0.4.2__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. quantpad_data-0.5.1/.gitignore +34 -0
  2. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/PKG-INFO +63 -3
  3. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/README.md +61 -2
  4. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/pyproject.toml +5 -2
  5. quantpad_data-0.5.1/quantpad_data/__init__.py +219 -0
  6. quantpad_data-0.5.1/quantpad_data/cache.py +440 -0
  7. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/quantpad_data/client.py +11 -17
  8. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/quantpad_data/memory.py +59 -3
  9. quantpad_data-0.5.1/quantpad_data/rth.py +178 -0
  10. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/quantpad_data/v2.py +208 -5
  11. quantpad_data-0.5.1/tests/conftest.py +26 -0
  12. quantpad_data-0.5.1/tests/test_packaging.py +184 -0
  13. quantpad_data-0.5.1/tests/test_rth.py +479 -0
  14. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/tests/test_v2.py +124 -17
  15. quantpad_data-0.5.1/tests/test_v2_cache.py +675 -0
  16. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/tests/test_v2_gate.py +85 -33
  17. quantpad_data-0.4.2/.gitignore +0 -21
  18. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/quantpad_data/errors.py +0 -0
  19. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/quantpad_data/lean.py +0 -0
  20. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/quantpad_data/py.typed +0 -0
  21. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/tests/test_client.py +0 -0
  22. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/tests/test_lean.py +0 -0
  23. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/tests/test_message_encoding.py +0 -0
  24. {quantpad_data-0.4.2 → quantpad_data-0.5.1}/tests/test_version.py +0 -0
@@ -0,0 +1,34 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .env
5
+ .env.local
6
+ .venv/
7
+ venv/
8
+ # Any suffixed venv too. .venv141 (fastapi 0.141, the version the image builds --
9
+ # see pinfastapi) was NOT matched by the pattern above and a `git add -A` swallowed
10
+ # all ~4,000 files of it into a commit.
11
+ .venv*/
12
+ *.egg-info/
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ .pytest_cache/
16
+
17
+ # Throwaway investigation probes. Named so they sort together and are
18
+ # obviously not part of the service; there are usually a lot of them
19
+ # mid-investigation and none of them should ever be committed.
20
+ _qp_*
21
+
22
+ # Scratch: commit-message and one-off investigation files.
23
+ _*.txt
24
+ _*.md
25
+ _*.py
26
+
27
+ # A DUNDER FILE IS NOT A SCRATCH FILE. `_*.py` matches `__init__.py`, and while
28
+ # git keeps an already-tracked file regardless, HATCHLING APPLIES THESE PATTERNS
29
+ # WHEN SELECTING WHAT GOES IN THE WHEEL -- so from 2026-09-14 every published
30
+ # quantpad-data release shipped without its `__init__.py`, i.e. without Client,
31
+ # Historical, get_bars and every other documented name. `pip install
32
+ # quantpad-data` gave a namespace package that answered AttributeError to the
33
+ # README's first example. Verified against the artifact on PyPI, not inferred.
34
+ !__*.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quantpad-data
3
- Version: 0.4.2
3
+ Version: 0.5.1
4
4
  Summary: Official Python client for the QuantPad market-data API
5
5
  Project-URL: Documentation, https://api.quantpad.ai/external/docs
6
6
  Author: QuantPad
@@ -16,6 +16,7 @@ Provides-Extra: rth
16
16
  Requires-Dist: exchange-calendars>=4.5; extra == 'rth'
17
17
  Provides-Extra: test
18
18
  Requires-Dist: build>=1.2; extra == 'test'
19
+ Requires-Dist: exchange-calendars>=4.5; extra == 'test'
19
20
  Requires-Dist: pytest>=8; extra == 'test'
20
21
  Description-Content-Type: text/markdown
21
22
 
@@ -75,8 +76,7 @@ client honors `Retry-After` and uses exponential backoff for transient failures.
75
76
  QuantPad notebooks remain compatible through `QUANTPAD_NOTEBOOK_DATA_TOKEN`.
76
77
 
77
78
  Bars preserve the notebook helper's OHLCV aliases and smart futures
78
- back-adjustment default. Tick timestamps are epoch nanoseconds. Install
79
- `quantpad-data[rth]` for XNYS regular-session helpers.
79
+ back-adjustment default. Tick timestamps are epoch nanoseconds.
80
80
 
81
81
  `mbp-10` is true L2 depth with bid/ask price, size, and order-count fields at
82
82
  levels 0-9. It is available for supported CME futures, individual CME
@@ -172,6 +172,66 @@ job = client.batch.submit(
172
172
  client.batch.hydrate(job["job_id"])
173
173
  ```
174
174
 
175
+ ### Regular trading hours
176
+
177
+ `get_range` returns the whole published session, which for US equities includes
178
+ pre- and post-market. That is what the upstream feed publishes, and it is more
179
+ data than QuantPad's older `/v1` tick endpoints returned: those enumerated
180
+ exchange sessions, and the XNYS calendar's open and close *are* regular hours,
181
+ so `/v1` excluded the extended session without saying so — around 7% of the
182
+ rows on a liquid name.
183
+
184
+ If you want that narrower scope, ask for it:
185
+
186
+ ```python
187
+ df = client.timeseries.get_range_rth(
188
+ dataset="XNAS.ITCH", symbols="BA", schema="trades",
189
+ start="2026-09-16", end="2026-09-17",
190
+ )
191
+ ```
192
+
193
+ `iter_range_rth` is the streaming form. Both follow the exchange calendar, so
194
+ half-days close early and holidays are skipped, and both filter on `ts_event`
195
+ — the clock `/v1` presented as its `t` column — so the numbers are comparable
196
+ with a `/v1` result. Requires the calendar extra: `pip install
197
+ 'quantpad-data[rth]'`.
198
+
199
+ The window sent to the service is still the whole range; the narrowing happens
200
+ here. So the size budget applies to the full session either way, and a window
201
+ too large unfiltered is too large for `get_range_rth` too — use
202
+ `iter_range_rth` for those.
203
+
204
+ ### The disk cache
205
+
206
+ Settled `/v2` answers are cached on disk, so asking twice costs the network
207
+ once. Measured against production on a day of `BA` `mbp-10` (237,916 rows):
208
+ 1.30s cold, 0.04s warm for `get_range`, and 1.01s to 0.14s for `iter_range`,
209
+ which caches each day's partition separately.
210
+
211
+ Only *final* answers are stored. A window reaching into the current day is
212
+ never cached, and neither is one the service clamped at the end, because both
213
+ legitimately return more data later. A cached answer carries the same
214
+ `.attrs['quantpad']` as the network one, so a hit and a miss are
215
+ indistinguishable to your code.
216
+
217
+ Defaults, all overridable:
218
+
219
+ | Variable | Default | Meaning |
220
+ | --- | --- | --- |
221
+ | `QPD_CACHE` | `1` | Set `0` to disable entirely |
222
+ | `QPD_CACHE_DIR` | platform user-cache dir | Where entries live |
223
+ | `QPD_CACHE_MAX_BYTES` | 1 GiB | Least-recently-used entries are evicted past this |
224
+
225
+ The cache is best-effort by design: a full disk, an unwritable directory or a
226
+ corrupt entry costs you the speedup and never the request.
227
+
228
+ ```python
229
+ from quantpad_data import cache
230
+
231
+ cache.total_bytes() # how much is on disk
232
+ cache.clear() # throw it all away
233
+ ```
234
+
175
235
  ### Other namespaces
176
236
 
177
237
  `client.symbology.resolve(...)`, the `client.metadata.*` mirrors
@@ -54,8 +54,7 @@ client honors `Retry-After` and uses exponential backoff for transient failures.
54
54
  QuantPad notebooks remain compatible through `QUANTPAD_NOTEBOOK_DATA_TOKEN`.
55
55
 
56
56
  Bars preserve the notebook helper's OHLCV aliases and smart futures
57
- back-adjustment default. Tick timestamps are epoch nanoseconds. Install
58
- `quantpad-data[rth]` for XNYS regular-session helpers.
57
+ back-adjustment default. Tick timestamps are epoch nanoseconds.
59
58
 
60
59
  `mbp-10` is true L2 depth with bid/ask price, size, and order-count fields at
61
60
  levels 0-9. It is available for supported CME futures, individual CME
@@ -151,6 +150,66 @@ job = client.batch.submit(
151
150
  client.batch.hydrate(job["job_id"])
152
151
  ```
153
152
 
153
+ ### Regular trading hours
154
+
155
+ `get_range` returns the whole published session, which for US equities includes
156
+ pre- and post-market. That is what the upstream feed publishes, and it is more
157
+ data than QuantPad's older `/v1` tick endpoints returned: those enumerated
158
+ exchange sessions, and the XNYS calendar's open and close *are* regular hours,
159
+ so `/v1` excluded the extended session without saying so — around 7% of the
160
+ rows on a liquid name.
161
+
162
+ If you want that narrower scope, ask for it:
163
+
164
+ ```python
165
+ df = client.timeseries.get_range_rth(
166
+ dataset="XNAS.ITCH", symbols="BA", schema="trades",
167
+ start="2026-09-16", end="2026-09-17",
168
+ )
169
+ ```
170
+
171
+ `iter_range_rth` is the streaming form. Both follow the exchange calendar, so
172
+ half-days close early and holidays are skipped, and both filter on `ts_event`
173
+ — the clock `/v1` presented as its `t` column — so the numbers are comparable
174
+ with a `/v1` result. Requires the calendar extra: `pip install
175
+ 'quantpad-data[rth]'`.
176
+
177
+ The window sent to the service is still the whole range; the narrowing happens
178
+ here. So the size budget applies to the full session either way, and a window
179
+ too large unfiltered is too large for `get_range_rth` too — use
180
+ `iter_range_rth` for those.
181
+
182
+ ### The disk cache
183
+
184
+ Settled `/v2` answers are cached on disk, so asking twice costs the network
185
+ once. Measured against production on a day of `BA` `mbp-10` (237,916 rows):
186
+ 1.30s cold, 0.04s warm for `get_range`, and 1.01s to 0.14s for `iter_range`,
187
+ which caches each day's partition separately.
188
+
189
+ Only *final* answers are stored. A window reaching into the current day is
190
+ never cached, and neither is one the service clamped at the end, because both
191
+ legitimately return more data later. A cached answer carries the same
192
+ `.attrs['quantpad']` as the network one, so a hit and a miss are
193
+ indistinguishable to your code.
194
+
195
+ Defaults, all overridable:
196
+
197
+ | Variable | Default | Meaning |
198
+ | --- | --- | --- |
199
+ | `QPD_CACHE` | `1` | Set `0` to disable entirely |
200
+ | `QPD_CACHE_DIR` | platform user-cache dir | Where entries live |
201
+ | `QPD_CACHE_MAX_BYTES` | 1 GiB | Least-recently-used entries are evicted past this |
202
+
203
+ The cache is best-effort by design: a full disk, an unwritable directory or a
204
+ corrupt entry costs you the speedup and never the request.
205
+
206
+ ```python
207
+ from quantpad_data import cache
208
+
209
+ cache.total_bytes() # how much is on disk
210
+ cache.clear() # throw it all away
211
+ ```
212
+
154
213
  ### Other namespaces
155
214
 
156
215
  `client.symbology.resolve(...)`, the `client.metadata.*` mirrors
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quantpad-data"
7
- version = "0.4.2"
7
+ version = "0.5.1"
8
8
  description = "Official Python client for the QuantPad market-data API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -23,7 +23,10 @@ classifiers = [
23
23
 
24
24
  [project.optional-dependencies]
25
25
  rth = ["exchange-calendars>=4.5"]
26
- test = ["pytest>=8", "build>=1.2"]
26
+ # The calendar is in `test` as well as `rth` on purpose: tests/test_rth.py
27
+ # importorskips it, so without it here CI would report green having run none of
28
+ # the regular-hours suite.
29
+ test = ["pytest>=8", "build>=1.2", "exchange-calendars>=4.5"]
27
30
 
28
31
  [project.urls]
29
32
  Documentation = "https://api.quantpad.ai/external/docs"
@@ -0,0 +1,219 @@
1
+ """QuantPad market-data SDK and notebook-compatible function API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterator, Optional
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ import pyarrow as pa
10
+
11
+ from .client import Client, ROLL_ADJUST_MODES, TICK_SCHEMAS, USER_AGENT, VERSION
12
+ from .errors import (
13
+ AuthenticationError,
14
+ NotFoundError,
15
+ PermissionDeniedError,
16
+ QuantPadDataError,
17
+ QuantPadDataNotReady,
18
+ QuotaExceededError,
19
+ RateLimitError,
20
+ ResponseTooLargeError,
21
+ UpstreamError,
22
+ V2NotAvailableError,
23
+ ValidationError,
24
+ )
25
+
26
+ __version__ = VERSION
27
+ QuantPadClient = Client
28
+
29
+
30
+ def _client(**kwargs) -> Client:
31
+ return Client(**kwargs)
32
+
33
+
34
+ def get_bars(symbol, timeframe, start_ms, end_ms, *, max_rows=None, as_df=True,
35
+ roll_adjust=None):
36
+ return _client().get_bars(
37
+ symbol, timeframe, start_ms, end_ms, max_rows=max_rows,
38
+ as_df=as_df, roll_adjust=roll_adjust,
39
+ )
40
+
41
+
42
+ def get_bars_with_retry(symbol, timeframe, start_ms, end_ms, *, max_rows=None,
43
+ retries=3, sleep_seconds=10, roll_adjust=None):
44
+ # Client retries use Retry-After and exponential jitter. ``sleep_seconds``
45
+ # remains accepted so existing notebooks do not break.
46
+ return Client(max_retries=max(0, int(retries) - 1)).get_bars(
47
+ symbol, timeframe, start_ms, end_ms, max_rows=max_rows,
48
+ roll_adjust=roll_adjust,
49
+ )
50
+
51
+
52
+ def get_ticks(symbol, schema, start_ms, end_ms, *, columns=None, chunksize=250_000):
53
+ return _client().get_ticks(
54
+ symbol, schema, start_ms, end_ms, columns=columns, chunksize=chunksize
55
+ )
56
+
57
+
58
+ def get_trades(symbol, start_ms, end_ms, *, columns=None, chunk_rows=250_000):
59
+ return get_ticks(
60
+ symbol, "trades", start_ms, end_ms, columns=columns, chunksize=chunk_rows
61
+ )
62
+
63
+
64
+ def get_mbp1(symbol, start_ms, end_ms, *, columns=None, chunk_rows=250_000):
65
+ return get_ticks(
66
+ symbol, "mbp-1", start_ms, end_ms, columns=columns, chunksize=chunk_rows
67
+ )
68
+
69
+
70
+ def get_mbp10(symbol, start_ms, end_ms, *, columns=None, chunk_rows=250_000):
71
+ return get_ticks(
72
+ symbol, "mbp-10", start_ms, end_ms, columns=columns, chunksize=chunk_rows
73
+ )
74
+
75
+
76
+ def symbology_resolve(dataset=None, symbols=None, stype_in=None, stype_out=None,
77
+ start_date=None, end_date=None):
78
+ return _client().symbology_resolve(
79
+ dataset, symbols, stype_in, stype_out, start_date, end_date
80
+ )
81
+
82
+
83
+ def get_universe(query: str, *, limit: int = 20, asset_class: str | None = None):
84
+ return _client().get_universe(query, limit=limit, asset_class=asset_class)
85
+
86
+
87
+ def get_coverage(symbol: str):
88
+ return _client().get_coverage(symbol)
89
+
90
+
91
+ def _iter_rth_windows_ms(start_ms: int, end_ms: int):
92
+ try:
93
+ import exchange_calendars as ec
94
+ except ImportError as exc:
95
+ raise RuntimeError(
96
+ "RTH helpers require: pip install 'quantpad-data[rth]'"
97
+ ) from exc
98
+ cal = ec.get_calendar("XNYS")
99
+ start_day = pd.Timestamp(start_ms, unit="ms", tz="UTC").tz_convert(
100
+ "America/New_York"
101
+ ).date()
102
+ end_day = pd.Timestamp(end_ms - 1, unit="ms", tz="UTC").tz_convert(
103
+ "America/New_York"
104
+ ).date()
105
+ for session in cal.sessions_in_range(pd.Timestamp(start_day), pd.Timestamp(end_day)):
106
+ open_ms = int(cal.session_open(session).timestamp() * 1000)
107
+ close_ms = int(cal.session_close(session).timestamp() * 1000)
108
+ left, right = max(open_ms, int(start_ms)), min(close_ms, int(end_ms))
109
+ if left < right:
110
+ yield left, right
111
+
112
+
113
+ def _stream_ticks_rth(symbol, schema, start_ms, end_ms, *, columns, chunk_rows):
114
+ for open_ms, close_ms in _iter_rth_windows_ms(start_ms, end_ms):
115
+ for frame in get_ticks(
116
+ symbol, schema, open_ms, close_ms,
117
+ columns=columns, chunksize=chunk_rows,
118
+ ):
119
+ if "t" in frame:
120
+ frame = frame.loc[
121
+ (frame["t"] >= open_ms * 1_000_000)
122
+ & (frame["t"] < close_ms * 1_000_000)
123
+ ]
124
+ if not frame.empty:
125
+ yield frame
126
+
127
+
128
+ def get_trades_rth(symbol, start_ms, end_ms, *, columns=None, chunk_rows=250_000):
129
+ return _stream_ticks_rth(
130
+ symbol, "trades", start_ms, end_ms, columns=columns, chunk_rows=chunk_rows
131
+ )
132
+
133
+
134
+ def get_mbp1_rth(symbol, start_ms, end_ms, *, columns=None, chunk_rows=250_000):
135
+ return _stream_ticks_rth(
136
+ symbol, "mbp-1", start_ms, end_ms, columns=columns, chunk_rows=chunk_rows
137
+ )
138
+
139
+
140
+ def get_mbp10_rth(symbol, start_ms, end_ms, *, columns=None, chunk_rows=250_000):
141
+ return _stream_ticks_rth(
142
+ symbol, "mbp-10", start_ms, end_ms, columns=columns, chunk_rows=chunk_rows
143
+ )
144
+
145
+
146
+ def get_spread_1m_rth(symbol, start_ms, end_ms, *, chunk_rows=250_000):
147
+ pieces = []
148
+ for frame in get_mbp1_rth(
149
+ symbol, start_ms, end_ms,
150
+ columns=["t", "bid_px", "ask_px"], chunk_rows=chunk_rows,
151
+ ):
152
+ spread = frame["ask_px"] - frame["bid_px"]
153
+ valid = spread.notna() & (spread >= 0)
154
+ if valid.any():
155
+ index = pd.to_datetime(frame.loc[valid, "t"], unit="ns", utc=True)
156
+ pieces.append(pd.Series(spread.loc[valid].to_numpy(), index=index))
157
+ if not pieces:
158
+ return pd.DataFrame(
159
+ {"spread": pd.Series(dtype="float64")},
160
+ index=pd.DatetimeIndex([], name="t", tz="UTC"),
161
+ )
162
+ result = pd.concat(pieces).resample("1min").mean().to_frame("spread")
163
+ result.index.name = "t"
164
+ return result
165
+
166
+
167
+ def iter_trades(symbol, start_ms, end_ms, *, columns=None, chunksize=250_000):
168
+ return get_trades(
169
+ symbol, start_ms, end_ms, columns=columns, chunk_rows=chunksize
170
+ )
171
+
172
+
173
+ def iter_mbp1(symbol, start_ms, end_ms, *, columns=None, chunksize=250_000):
174
+ return get_mbp1(symbol, start_ms, end_ms, columns=columns, chunk_rows=chunksize)
175
+
176
+
177
+ def iter_l1_df_chunks(symbol, schema="trades", start_ms=0, end_ms=0,
178
+ chunksize=250_000, usecols: Optional[list[str]] = None,
179
+ time_col="ts_event", set_datetime_index=True, channel=None,
180
+ chunk_size=None):
181
+ return get_ticks(
182
+ symbol, channel or schema, start_ms, end_ms, columns=usecols,
183
+ chunksize=chunk_size or chunksize,
184
+ )
185
+
186
+
187
+ def iter_l1_trades_df_chunks(symbol, start_ms, end_ms, chunksize=250_000,
188
+ usecols=None):
189
+ return iter_trades(
190
+ symbol, start_ms, end_ms, columns=usecols, chunksize=chunksize
191
+ )
192
+
193
+
194
+ def iter_l1_mbp1_df_chunks(symbol, start_ms, end_ms, chunksize=250_000,
195
+ usecols=None):
196
+ return iter_mbp1(symbol, start_ms, end_ms, columns=usecols, chunksize=chunksize)
197
+
198
+
199
+ from . import cache # noqa: E402 (submodule: on-disk cache for settled /v2)
200
+ from . import lean # noqa: E402 (submodule: LEAN backtesting data staging)
201
+ from . import v2 # noqa: E402 (submodule: Databento-shaped explicit API)
202
+
203
+ # Exported at the root as well as on ``v2``: the sandbox module reaches it
204
+ # as ``qpd.Historical()``, and one spelling across both surfaces means
205
+ # notebook code moves between them unchanged.
206
+ Historical = v2.Historical
207
+
208
+ __all__ = [
209
+ "Client", "QuantPadClient", "QuantPadDataError", "QuantPadDataNotReady", "AuthenticationError",
210
+ "PermissionDeniedError", "RateLimitError", "QuotaExceededError",
211
+ "NotFoundError", "V2NotAvailableError", "ValidationError", "UpstreamError",
212
+ "ResponseTooLargeError", "TICK_SCHEMAS",
213
+ "ROLL_ADJUST_MODES", "get_bars", "get_bars_with_retry", "get_ticks",
214
+ "get_trades", "get_mbp1", "get_mbp10", "get_trades_rth", "get_mbp1_rth",
215
+ "get_mbp10_rth", "get_spread_1m_rth", "symbology_resolve", "get_universe",
216
+ "get_coverage", "iter_trades", "iter_mbp1", "iter_l1_df_chunks",
217
+ "iter_l1_trades_df_chunks", "iter_l1_mbp1_df_chunks", "cache", "lean",
218
+ "v2", "Historical", "np", "pd", "pa",
219
+ ]