quantpad-data 0.8.0__tar.gz → 0.8.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.
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/PKG-INFO +3 -4
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/README.md +2 -3
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/pyproject.toml +1 -1
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/quantpad_data/__init__.py +1 -1
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/quantpad_data/client.py +1 -1
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/quantpad_data/memory.py +1 -1
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/quantpad_data/v2.py +14 -25
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/tests/test_packaging.py +38 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/tests/test_v2.py +3 -3
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/.gitignore +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/quantpad_data/cache.py +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/quantpad_data/errors.py +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/quantpad_data/lean.py +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/quantpad_data/py.typed +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/quantpad_data/rth.py +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/tests/conftest.py +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/tests/test_client.py +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/tests/test_lean.py +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/tests/test_message_encoding.py +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/tests/test_refusal_hint.py +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/tests/test_rth.py +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/tests/test_v2_cache.py +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/tests/test_v2_gate.py +0 -0
- {quantpad_data-0.8.0 → quantpad_data-0.8.1}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: quantpad-data
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -106,9 +106,8 @@ symbol it is. That is convenient until it is wrong, and when it is wrong it
|
|
|
106
106
|
fails quietly — asking for a specific futures contract returns an empty frame
|
|
107
107
|
that looks exactly like a coverage gap.
|
|
108
108
|
|
|
109
|
-
`qpd.v2.Historical` removes the inference.
|
|
110
|
-
|
|
111
|
-
swapping the client:
|
|
109
|
+
`qpd.v2.Historical` removes the inference. Every call names its dataset and
|
|
110
|
+
symbology type explicitly, and every response reports the address it used:
|
|
112
111
|
|
|
113
112
|
```python
|
|
114
113
|
import quantpad_data as qpd
|
|
@@ -84,9 +84,8 @@ symbol it is. That is convenient until it is wrong, and when it is wrong it
|
|
|
84
84
|
fails quietly — asking for a specific futures contract returns an empty frame
|
|
85
85
|
that looks exactly like a coverage gap.
|
|
86
86
|
|
|
87
|
-
`qpd.v2.Historical` removes the inference.
|
|
88
|
-
|
|
89
|
-
swapping the client:
|
|
87
|
+
`qpd.v2.Historical` removes the inference. Every call names its dataset and
|
|
88
|
+
symbology type explicitly, and every response reports the address it used:
|
|
90
89
|
|
|
91
90
|
```python
|
|
92
91
|
import quantpad_data as qpd
|
|
@@ -198,7 +198,7 @@ def iter_l1_mbp1_df_chunks(symbol, start_ms, end_ms, chunksize=250_000,
|
|
|
198
198
|
|
|
199
199
|
from . import cache # noqa: E402 (submodule: on-disk cache for settled /v2)
|
|
200
200
|
from . import lean # noqa: E402 (submodule: LEAN backtesting data staging)
|
|
201
|
-
from . import v2 # noqa: E402 (submodule:
|
|
201
|
+
from . import v2 # noqa: E402 (submodule: explicit-address API)
|
|
202
202
|
|
|
203
203
|
# Exported at the root as well as on ``v2``: the sandbox module reaches it
|
|
204
204
|
# as ``qpd.Historical()``, and one spelling across both surfaces means
|
|
@@ -15,7 +15,7 @@ before fetching it, which is why it takes both.
|
|
|
15
15
|
|
|
16
16
|
THE UNITS ARE NOT THE SAME, AND THAT IS THE WHOLE DIFFICULTY
|
|
17
17
|
The service prices in DBN bytes: record count times record width, which is
|
|
18
|
-
|
|
18
|
+
upstream's own billing arithmetic. This process pays for the compressed wire
|
|
19
19
|
bytes, an Arrow table decoded from them, and a pandas frame copied out of that
|
|
20
20
|
table -- partly all three at once. Measured with
|
|
21
21
|
scripts/measure_client_memory.py against production, peak RSS per priced byte
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Explicit-address access to QuantPad's cache.
|
|
2
2
|
|
|
3
3
|
The v1 helpers take QuantPad's own arguments and infer the rest: which
|
|
4
4
|
dataset a symbol lives in, what kind of symbol it is, how far back to
|
|
@@ -7,18 +7,7 @@ it is wrong it fails quietly — a specific futures contract returns an
|
|
|
7
7
|
empty frame that looks exactly like a coverage gap.
|
|
8
8
|
|
|
9
9
|
This module removes the inference. Every call names its dataset and its
|
|
10
|
-
symbology type,
|
|
11
|
-
response reports the address it actually used. Code written against the
|
|
12
|
-
Databento client transfers over with the client swapped:
|
|
13
|
-
|
|
14
|
-
import databento as db
|
|
15
|
-
client = db.Historical(key=...)
|
|
16
|
-
df = client.timeseries.get_range(
|
|
17
|
-
dataset="GLBX.MDP3", symbols="ESZ6", schema="ohlcv-1m",
|
|
18
|
-
start="2026-01-05", end="2026-01-06",
|
|
19
|
-
).to_df()
|
|
20
|
-
|
|
21
|
-
becomes
|
|
10
|
+
symbology type, and every response reports the address it actually used:
|
|
22
11
|
|
|
23
12
|
import quantpad_data as qpd
|
|
24
13
|
client = qpd.v2.Historical()
|
|
@@ -27,9 +16,9 @@ becomes
|
|
|
27
16
|
start="2026-01-05", end="2026-01-06",
|
|
28
17
|
)
|
|
29
18
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
A DataFrame comes back directly, and there is no cost or billing surface,
|
|
20
|
+
because data is included in the QuantPad plan rather than metered per
|
|
21
|
+
request.
|
|
33
22
|
"""
|
|
34
23
|
|
|
35
24
|
from __future__ import annotations
|
|
@@ -255,7 +244,7 @@ class _Namespace:
|
|
|
255
244
|
|
|
256
245
|
|
|
257
246
|
class Timeseries(_Namespace):
|
|
258
|
-
"""
|
|
247
|
+
"""Ranges of bars, ticks and book depth for an explicit address."""
|
|
259
248
|
|
|
260
249
|
def get_range(
|
|
261
250
|
self,
|
|
@@ -763,12 +752,12 @@ class Timeseries(_Namespace):
|
|
|
763
752
|
|
|
764
753
|
|
|
765
754
|
class Symbology(_Namespace):
|
|
766
|
-
"""
|
|
755
|
+
"""Symbol resolution, plus a way in.
|
|
767
756
|
|
|
768
757
|
Every other call here demands an explicit ``dataset``, which is the
|
|
769
758
|
point and also a dead end for someone holding the string "AAPL".
|
|
770
|
-
``search`` is the one endpoint that answers that
|
|
771
|
-
|
|
759
|
+
``search`` is the one endpoint that answers that; the rest of this
|
|
760
|
+
surface assumes you already know.
|
|
772
761
|
"""
|
|
773
762
|
|
|
774
763
|
def search(
|
|
@@ -830,7 +819,7 @@ class Symbology(_Namespace):
|
|
|
830
819
|
|
|
831
820
|
|
|
832
821
|
class Metadata(_Namespace):
|
|
833
|
-
"""
|
|
822
|
+
"""Datasets, schemas, fields, coverage and request sizing.
|
|
834
823
|
|
|
835
824
|
The cost and unit-price endpoints are deliberately absent: QuantPad
|
|
836
825
|
data is included in the plan rather than metered, so a per-request
|
|
@@ -969,8 +958,8 @@ def _chain_frame(payload: dict) -> pd.DataFrame:
|
|
|
969
958
|
class Options(_Namespace):
|
|
970
959
|
"""Options-chain enumeration.
|
|
971
960
|
|
|
972
|
-
Not part of
|
|
973
|
-
the whole ``definition`` schema and filter it yourself. This does
|
|
961
|
+
Not part of the usual historical-client surface, which expects you to
|
|
962
|
+
pull the whole ``definition`` schema and filter it yourself. This does
|
|
974
963
|
that server-side, because "download every instrument definition for
|
|
975
964
|
the day" is a poor answer to "what strikes exist".
|
|
976
965
|
|
|
@@ -1008,7 +997,7 @@ class Options(_Namespace):
|
|
|
1008
997
|
|
|
1009
998
|
|
|
1010
999
|
class Batch(_Namespace):
|
|
1011
|
-
"""
|
|
1000
|
+
"""Batch extracts, scoped to your own jobs.
|
|
1012
1001
|
|
|
1013
1002
|
Useful when a range is too large to stream in one sitting: submit,
|
|
1014
1003
|
walk away, then hydrate the result into the cache so ordinary
|
|
@@ -1059,7 +1048,7 @@ class Batch(_Namespace):
|
|
|
1059
1048
|
|
|
1060
1049
|
|
|
1061
1050
|
class Historical:
|
|
1062
|
-
"""
|
|
1051
|
+
"""An explicit-address view of QuantPad's cache.
|
|
1063
1052
|
|
|
1064
1053
|
Takes the same credentials as the v1 ``Client`` — an API key, or the
|
|
1065
1054
|
injected token when running inside a QuantPad sandbox.
|
|
@@ -21,6 +21,7 @@ from __future__ import annotations
|
|
|
21
21
|
|
|
22
22
|
import ast
|
|
23
23
|
import os
|
|
24
|
+
import re
|
|
24
25
|
import subprocess
|
|
25
26
|
import sys
|
|
26
27
|
import tarfile
|
|
@@ -53,6 +54,7 @@ def built(tmp_path_factory):
|
|
|
53
54
|
# ./dist would make it skip in CI, where nothing has built there yet --
|
|
54
55
|
# a test that skips exactly where it is needed.
|
|
55
56
|
"wheel_path": wheel,
|
|
57
|
+
"sdist_path": sdist,
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
|
|
@@ -182,3 +184,39 @@ class TestAFreshInstallExposesTheApi:
|
|
|
182
184
|
f"a fresh install cannot answer for its own API:\n"
|
|
183
185
|
f"{done.stdout}\n{done.stderr[-1500:]}"
|
|
184
186
|
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TestNothingShippedNamesTheDataVendor:
|
|
190
|
+
"""QuantPad presents its market data as its own, and a published package is
|
|
191
|
+
as public as a response: PyPI serves every file in both artifacts, the
|
|
192
|
+
README is the project page, and ``help()`` in a user's sandbox prints the
|
|
193
|
+
docstrings. So nothing shipped may name the vendor -- read out of the
|
|
194
|
+
artifacts, for the same reason as everything else in this file."""
|
|
195
|
+
|
|
196
|
+
VENDOR = re.compile(r"data[\s_-]?bento", re.IGNORECASE)
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _texts(built, kind):
|
|
200
|
+
if kind == "wheel":
|
|
201
|
+
with zipfile.ZipFile(built["wheel_path"]) as archive:
|
|
202
|
+
for name in archive.namelist():
|
|
203
|
+
yield name, archive.read(name)
|
|
204
|
+
else:
|
|
205
|
+
with tarfile.open(built["sdist_path"]) as archive:
|
|
206
|
+
for member in archive.getmembers():
|
|
207
|
+
if member.isfile():
|
|
208
|
+
yield member.name, archive.extractfile(member).read()
|
|
209
|
+
|
|
210
|
+
@pytest.mark.parametrize("kind", ["wheel", "sdist"])
|
|
211
|
+
def test_no_shipped_file_names_the_vendor(self, built, kind):
|
|
212
|
+
offenders = [
|
|
213
|
+
name
|
|
214
|
+
for name, data in self._texts(built, kind)
|
|
215
|
+
if self.VENDOR.search(data.decode("utf-8", errors="replace"))
|
|
216
|
+
]
|
|
217
|
+
assert not offenders, f"the {kind} names the data vendor in {offenders}"
|
|
218
|
+
|
|
219
|
+
def test_the_readme_is_in_what_was_checked(self, built):
|
|
220
|
+
"""Guards the test above: the wheel's METADATA carries the README."""
|
|
221
|
+
names = [name for name, _ in self._texts(built, "wheel")]
|
|
222
|
+
assert any(name.endswith("METADATA") for name in names)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""The
|
|
1
|
+
"""The explicit-address surface.
|
|
2
2
|
|
|
3
3
|
Two things are being checked. First that the wire calls carry exactly
|
|
4
4
|
what the caller said and nothing the SDK invented — the whole reason
|
|
@@ -95,7 +95,7 @@ class TestNothingIsInferred:
|
|
|
95
95
|
assert kwargs["json"]["symbols"] == ["ESZ6"]
|
|
96
96
|
|
|
97
97
|
def test_a_single_symbol_string_is_sent_as_a_list(self):
|
|
98
|
-
"""
|
|
98
|
+
"""Upstream accepts either; normalising here keeps the server
|
|
99
99
|
contract one shape instead of two."""
|
|
100
100
|
api, session = client_with([response(200, arrow_bytes(bars_table()))])
|
|
101
101
|
api.timeseries.get_range(
|
|
@@ -104,7 +104,7 @@ class TestNothingIsInferred:
|
|
|
104
104
|
)
|
|
105
105
|
assert session.calls[0][2]["json"]["symbols"] == ["ESZ6"]
|
|
106
106
|
|
|
107
|
-
def
|
|
107
|
+
def test_the_default_stype_matches_upstream_not_our_own_convention(self):
|
|
108
108
|
api, session = client_with([response(200, arrow_bytes(bars_table()))])
|
|
109
109
|
api.timeseries.get_range(
|
|
110
110
|
dataset="GLBX.MDP3", symbols="ESZ6", schema="ohlcv-1m",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|