quantpad-data 0.7.1__tar.gz → 0.8.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.
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/PKG-INFO +6 -2
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/README.md +5 -1
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/pyproject.toml +1 -1
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/quantpad_data/__init__.py +5 -1
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/quantpad_data/client.py +1 -1
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/quantpad_data/v2.py +50 -1
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/tests/test_v2.py +73 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/.gitignore +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/quantpad_data/cache.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/quantpad_data/errors.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/quantpad_data/lean.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/quantpad_data/memory.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/quantpad_data/py.typed +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/quantpad_data/rth.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/tests/conftest.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/tests/test_client.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/tests/test_lean.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/tests/test_message_encoding.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/tests/test_packaging.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/tests/test_refusal_hint.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/tests/test_rth.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/tests/test_v2_cache.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/tests/test_v2_gate.py +0 -0
- {quantpad_data-0.7.1 → quantpad_data-0.8.0}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: quantpad-data
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
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
|
|
@@ -243,7 +243,11 @@ cache.clear() # throw it all away
|
|
|
243
243
|
(`list_datasets`, `list_schemas`, `list_fields`, `list_publishers`,
|
|
244
244
|
`get_dataset_range`, `get_dataset_condition`, `get_record_count`), and
|
|
245
245
|
`client.options.chain(...)` for enumerating strikes and expiries without
|
|
246
|
-
downloading a day's entire `definition` schema.
|
|
246
|
+
downloading a day's entire `definition` schema. A chain is capped at `limit`
|
|
247
|
+
(5,000 by default, 50,000 at most); the frame's `attrs` carry the server's
|
|
248
|
+
`count`, `total` and `truncated`, and a cut chain raises
|
|
249
|
+
`TruncatedChainWarning` — `warnings.simplefilter("error",
|
|
250
|
+
qpd.TruncatedChainWarning)` makes a partial chain a hard failure.
|
|
247
251
|
|
|
248
252
|
There is no cost or unit-price surface. QuantPad data is included in the plan
|
|
249
253
|
rather than metered per request.
|
|
@@ -221,7 +221,11 @@ cache.clear() # throw it all away
|
|
|
221
221
|
(`list_datasets`, `list_schemas`, `list_fields`, `list_publishers`,
|
|
222
222
|
`get_dataset_range`, `get_dataset_condition`, `get_record_count`), and
|
|
223
223
|
`client.options.chain(...)` for enumerating strikes and expiries without
|
|
224
|
-
downloading a day's entire `definition` schema.
|
|
224
|
+
downloading a day's entire `definition` schema. A chain is capped at `limit`
|
|
225
|
+
(5,000 by default, 50,000 at most); the frame's `attrs` carry the server's
|
|
226
|
+
`count`, `total` and `truncated`, and a cut chain raises
|
|
227
|
+
`TruncatedChainWarning` — `warnings.simplefilter("error",
|
|
228
|
+
qpd.TruncatedChainWarning)` makes a partial chain a hard failure.
|
|
225
229
|
|
|
226
230
|
There is no cost or unit-price surface. QuantPad data is included in the plan
|
|
227
231
|
rather than metered per request.
|
|
@@ -204,12 +204,16 @@ from . import v2 # noqa: E402 (submodule: Databento-shaped explicit API)
|
|
|
204
204
|
# as ``qpd.Historical()``, and one spelling across both surfaces means
|
|
205
205
|
# notebook code moves between them unchanged.
|
|
206
206
|
Historical = v2.Historical
|
|
207
|
+
# At the root for the same reason: ``warnings.simplefilter("error",
|
|
208
|
+
# qpd.TruncatedChainWarning)`` is how a script turns a cut chain into a
|
|
209
|
+
# failure, and it should not need to know which submodule warns.
|
|
210
|
+
TruncatedChainWarning = v2.TruncatedChainWarning
|
|
207
211
|
|
|
208
212
|
__all__ = [
|
|
209
213
|
"Client", "QuantPadClient", "QuantPadDataError", "QuantPadDataNotReady", "AuthenticationError",
|
|
210
214
|
"PermissionDeniedError", "RateLimitError", "QuotaExceededError",
|
|
211
215
|
"NotFoundError", "V2NotAvailableError", "ValidationError", "UpstreamError",
|
|
212
|
-
"ResponseTooLargeError", "TICK_SCHEMAS",
|
|
216
|
+
"ResponseTooLargeError", "TruncatedChainWarning", "TICK_SCHEMAS",
|
|
213
217
|
"ROLL_ADJUST_MODES", "get_bars", "get_bars_with_retry", "get_ticks",
|
|
214
218
|
"get_trades", "get_mbp1", "get_mbp10", "get_trades_rth", "get_mbp1_rth",
|
|
215
219
|
"get_mbp10_rth", "get_spread_1m_rth", "symbology_resolve", "get_universe",
|
|
@@ -38,6 +38,7 @@ import io
|
|
|
38
38
|
import json
|
|
39
39
|
import os
|
|
40
40
|
import tempfile
|
|
41
|
+
import warnings
|
|
41
42
|
from datetime import date, timedelta
|
|
42
43
|
from typing import Any, Iterator, Sequence
|
|
43
44
|
|
|
@@ -920,6 +921,51 @@ class Metadata(_Namespace):
|
|
|
920
921
|
return self._client._request("GET", path, params=clean).json()
|
|
921
922
|
|
|
922
923
|
|
|
924
|
+
class TruncatedChainWarning(UserWarning):
|
|
925
|
+
"""``options.chain`` returned fewer contracts than the filters matched.
|
|
926
|
+
|
|
927
|
+
The service caps a chain at ``limit`` (5,000 by default, 50,000 at
|
|
928
|
+
most) and says so in the body; this is that flag made audible. Raise
|
|
929
|
+
``limit`` or narrow the filters (``expiration``, ``strike_min``/
|
|
930
|
+
``strike_max``, ``instrument_class``) for the whole chain.
|
|
931
|
+
"""
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _chain_frame(payload: dict) -> pd.DataFrame:
|
|
935
|
+
"""The chain as a frame, carrying the server's count beside the rows.
|
|
936
|
+
|
|
937
|
+
The route answers ``count`` (rows in the body), ``total`` (rows the
|
|
938
|
+
filters matched) and ``truncated`` beside ``contracts``. Until this
|
|
939
|
+
the frame was built from ``contracts`` alone and the other three were
|
|
940
|
+
dropped, so a notebook handed exactly 5,000 rows could not tell a cut
|
|
941
|
+
chain from a whole one -- verified on prod, where an unfiltered
|
|
942
|
+
SPY.OPT came back as exactly the default limit against ~12,470
|
|
943
|
+
instruments. The three ride on ``frame.attrs`` (the same place
|
|
944
|
+
``get_range`` keeps its echo) and a truncation is also warned, since
|
|
945
|
+
``attrs`` is invisible to anyone not already looking for it.
|
|
946
|
+
|
|
947
|
+
Older servers send no ``total``; it is then unknown rather than
|
|
948
|
+
guessed, and ``truncated`` is read as the server sent it.
|
|
949
|
+
"""
|
|
950
|
+
contracts = payload.get("contracts", [])
|
|
951
|
+
frame = pd.DataFrame(contracts)
|
|
952
|
+
truncated = bool(payload.get("truncated", False))
|
|
953
|
+
frame.attrs["count"] = payload.get("count", len(contracts))
|
|
954
|
+
frame.attrs["total"] = payload.get("total")
|
|
955
|
+
frame.attrs["truncated"] = truncated
|
|
956
|
+
if truncated:
|
|
957
|
+
total = frame.attrs["total"]
|
|
958
|
+
matched = f"{total:,} matched" if isinstance(total, int) else "more matched"
|
|
959
|
+
warnings.warn(
|
|
960
|
+
f"options.chain returned {len(contracts):,} contracts but "
|
|
961
|
+
f"{matched}: the chain was cut at the service's limit. Raise "
|
|
962
|
+
"limit= (up to 50,000) or narrow the filters to see the rest.",
|
|
963
|
+
TruncatedChainWarning,
|
|
964
|
+
stacklevel=3,
|
|
965
|
+
)
|
|
966
|
+
return frame
|
|
967
|
+
|
|
968
|
+
|
|
923
969
|
class Options(_Namespace):
|
|
924
970
|
"""Options-chain enumeration.
|
|
925
971
|
|
|
@@ -927,6 +973,9 @@ class Options(_Namespace):
|
|
|
927
973
|
the whole ``definition`` schema and filter it yourself. This does
|
|
928
974
|
that server-side, because "download every instrument definition for
|
|
929
975
|
the day" is a poor answer to "what strikes exist".
|
|
976
|
+
|
|
977
|
+
The frame's ``attrs`` carry ``count``, ``total`` and ``truncated``;
|
|
978
|
+
a truncated chain also raises :class:`TruncatedChainWarning`.
|
|
930
979
|
"""
|
|
931
980
|
|
|
932
981
|
def chain(
|
|
@@ -955,7 +1004,7 @@ class Options(_Namespace):
|
|
|
955
1004
|
payload = self._client._request(
|
|
956
1005
|
"GET", "/v2/options/chain", params=clean
|
|
957
1006
|
).json()
|
|
958
|
-
return
|
|
1007
|
+
return _chain_frame(payload)
|
|
959
1008
|
|
|
960
1009
|
|
|
961
1010
|
class Batch(_Namespace):
|
|
@@ -2117,6 +2117,79 @@ class TestTheOtherNamespaces:
|
|
|
2117
2117
|
assert "strike_min" not in params
|
|
2118
2118
|
assert "instrument_class" not in params
|
|
2119
2119
|
|
|
2120
|
+
# chaintruncate: the route says when it cut the chain, and the frame
|
|
2121
|
+
# used to drop that. Verified on prod: unfiltered SPY.OPT returned
|
|
2122
|
+
# exactly 5,000 rows (the default limit) against ~12,470 instruments,
|
|
2123
|
+
# with no way to tell from the frame.
|
|
2124
|
+
|
|
2125
|
+
def test_a_cut_chain_warns_and_carries_the_servers_count(self):
|
|
2126
|
+
import warnings
|
|
2127
|
+
|
|
2128
|
+
api, _ = client_with([json_response({
|
|
2129
|
+
"count": 2, "total": 12470, "truncated": True,
|
|
2130
|
+
"contracts": [
|
|
2131
|
+
{"raw_symbol": "SPY 260116C00500000"},
|
|
2132
|
+
{"raw_symbol": "SPY 260116P00500000"},
|
|
2133
|
+
],
|
|
2134
|
+
})])
|
|
2135
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
2136
|
+
warnings.simplefilter("always")
|
|
2137
|
+
chain = api.options.chain(
|
|
2138
|
+
dataset="OPRA.PILLAR", underlier="SPY.OPT", date="2026-01-05",
|
|
2139
|
+
)
|
|
2140
|
+
assert len(chain) == 2
|
|
2141
|
+
assert chain.attrs["truncated"] is True
|
|
2142
|
+
assert chain.attrs["count"] == 2
|
|
2143
|
+
assert chain.attrs["total"] == 12470
|
|
2144
|
+
(warning,) = [
|
|
2145
|
+
w for w in caught if issubclass(w.category, quantpad_data.TruncatedChainWarning)
|
|
2146
|
+
]
|
|
2147
|
+
text = str(warning.message)
|
|
2148
|
+
assert "2 contracts" in text and "12,470 matched" in text
|
|
2149
|
+
assert "limit=" in text # the way out is named, not just the fact
|
|
2150
|
+
|
|
2151
|
+
def test_a_whole_chain_is_silent_and_says_so(self):
|
|
2152
|
+
import warnings
|
|
2153
|
+
|
|
2154
|
+
api, _ = client_with([json_response({
|
|
2155
|
+
"count": 1, "total": 1, "truncated": False,
|
|
2156
|
+
"contracts": [{"raw_symbol": "ESM6 C4800"}],
|
|
2157
|
+
})])
|
|
2158
|
+
with warnings.catch_warnings():
|
|
2159
|
+
warnings.simplefilter("error") # any warning here is a failure
|
|
2160
|
+
chain = api.options.chain(
|
|
2161
|
+
dataset="GLBX.MDP3", underlier="ES.OPT", date="2026-01-05",
|
|
2162
|
+
)
|
|
2163
|
+
assert chain.attrs["truncated"] is False
|
|
2164
|
+
assert chain.attrs["total"] == 1
|
|
2165
|
+
|
|
2166
|
+
def test_an_older_server_without_total_still_reports_the_cut(self):
|
|
2167
|
+
"""The deployed route sent ``truncated`` before it sent ``total``;
|
|
2168
|
+
a frame from it must still say it was cut, with the total unknown
|
|
2169
|
+
rather than invented."""
|
|
2170
|
+
import warnings
|
|
2171
|
+
|
|
2172
|
+
api, _ = client_with([json_response({
|
|
2173
|
+
"count": 1, "truncated": True, "contracts": [{"raw_symbol": "A"}],
|
|
2174
|
+
})])
|
|
2175
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
2176
|
+
warnings.simplefilter("always")
|
|
2177
|
+
chain = api.options.chain(
|
|
2178
|
+
dataset="GLBX.MDP3", underlier="ES.OPT", date="2026-01-05",
|
|
2179
|
+
)
|
|
2180
|
+
assert chain.attrs["truncated"] is True
|
|
2181
|
+
assert chain.attrs["total"] is None
|
|
2182
|
+
assert any(
|
|
2183
|
+
issubclass(w.category, quantpad_data.TruncatedChainWarning) for w in caught
|
|
2184
|
+
)
|
|
2185
|
+
|
|
2186
|
+
def test_the_truncation_warning_is_exported_at_the_root(self):
|
|
2187
|
+
"""``warnings.simplefilter("error", quantpad_data.TruncatedChainWarning)`` is
|
|
2188
|
+
how a script refuses to run on a partial chain; it needs the name
|
|
2189
|
+
without knowing which submodule warns."""
|
|
2190
|
+
assert quantpad_data.TruncatedChainWarning is quantpad_data.v2.TruncatedChainWarning
|
|
2191
|
+
assert issubclass(quantpad_data.TruncatedChainWarning, UserWarning)
|
|
2192
|
+
|
|
2120
2193
|
def test_batch_submit_and_hydrate_are_reachable(self):
|
|
2121
2194
|
api, session = client_with([
|
|
2122
2195
|
json_response({"job_id": "j1"}),
|
|
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
|
|
File without changes
|
|
File without changes
|