quantpad-data 0.1.0__tar.gz → 0.2.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,159 @@
1
+ Metadata-Version: 2.5
2
+ Name: quantpad-data
3
+ Version: 0.2.0
4
+ Summary: Official Python client for the QuantPad market-data API
5
+ Project-URL: Documentation, https://api.quantpad.ai/external/docs
6
+ Author: QuantPad
7
+ License: Proprietary
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Typing :: Typed
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: pandas<3,>=2.2
13
+ Requires-Dist: pyarrow>=18
14
+ Requires-Dist: requests>=2.32
15
+ Provides-Extra: rth
16
+ Requires-Dist: exchange-calendars>=4.5; extra == 'rth'
17
+ Provides-Extra: test
18
+ Requires-Dist: build>=1.2; extra == 'test'
19
+ Requires-Dist: pytest>=8; extra == 'test'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # QuantPad Data Python SDK
23
+
24
+ Install into the project's existing environment/package manager:
25
+
26
+ ```bash
27
+ uv add quantpad-data # uv project
28
+ poetry add quantpad-data # Poetry project
29
+ ```
30
+
31
+ For a plain Python project, create a local virtual environment:
32
+
33
+ ```bash
34
+ python3 -m venv .venv
35
+ source .venv/bin/activate # Windows PowerShell: .venv\Scripts\Activate.ps1
36
+ python -m pip install --upgrade pip
37
+ python -m pip install quantpad-data
38
+ export QUANTPAD_API_KEY=qp_live_...
39
+ ```
40
+
41
+ Do not install into Homebrew/system Python or use `--break-system-packages`.
42
+ When a project environment already exists, use its package manager or
43
+ `python -m pip` rather than creating a second environment. Keep
44
+ `QUANTPAD_API_KEY` in the local environment—never source code or chat.
45
+
46
+ ```python
47
+ import time
48
+ import quantpad_data as qpd
49
+
50
+ end = int(time.time() * 1000)
51
+ bars = qpd.get_bars("ES.FUT", "1m", end - 86_400_000, end)
52
+
53
+ for chunk in qpd.get_ticks(
54
+ "AAPL", "trades", end - 3_600_000, end, columns=["t", "price", "size"]
55
+ ):
56
+ print(chunk.head())
57
+
58
+ # 10-level L2 market-by-price order-book depth. This streams chunks;
59
+ # project columns to avoid materializing all 60+ book fields when unnecessary.
60
+ for chunk in qpd.get_mbp10(
61
+ "ES.FUT",
62
+ end - 3_600_000,
63
+ end,
64
+ columns=["t", "bid_px_00", "ask_px_00", "bid_sz_00", "ask_sz_00"],
65
+ ):
66
+ print(chunk.head())
67
+
68
+ matches = qpd.get_universe("apple", asset_class="equity")
69
+ coverage = qpd.get_coverage("AAPL")
70
+ ```
71
+
72
+ `QuantPadClient` (also available as `Client`) accepts `api_key=`, `base_url=`,
73
+ and `max_retries=`. The default
74
+ client honors `Retry-After` and uses exponential backoff for transient failures.
75
+ QuantPad notebooks remain compatible through `QUANTPAD_NOTEBOOK_DATA_TOKEN`.
76
+
77
+ 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.
80
+
81
+ `mbp-10` is true L2 depth with bid/ask price, size, and order-count fields at
82
+ levels 0-9. It is available for supported CME futures, individual CME
83
+ options-on-futures contracts, and US equities, subject to `get_coverage()` and
84
+ the plan lookback window (currently 30 days).
85
+
86
+ This SDK intentionally exposes only bars, ticks, universe, coverage, and
87
+ symbology. It does not proxy FRED or SEC EDGAR.
88
+
89
+ ## Explicit addressing: `qpd.v2`
90
+
91
+ The helpers above infer which dataset a symbol lives in and what kind of
92
+ symbol it is. That is convenient until it is wrong, and when it is wrong it
93
+ fails quietly — asking for a specific futures contract returns an empty frame
94
+ that looks exactly like a coverage gap.
95
+
96
+ `qpd.v2.Historical` removes the inference. It takes the same arguments as
97
+ `databento.Historical`, so code written against Databento ports over by
98
+ swapping the client:
99
+
100
+ ```python
101
+ import quantpad_data as qpd
102
+
103
+ client = qpd.v2.Historical()
104
+
105
+ bars = client.timeseries.get_range(
106
+ dataset="GLBX.MDP3", # required — never guessed
107
+ symbols="ESZ6", # a specific contract month
108
+ stype_in="raw_symbol", # raw_symbol | instrument_id | parent | continuous
109
+ schema="ohlcv-1m",
110
+ start="2026-01-05",
111
+ end="2026-01-06",
112
+ )
113
+
114
+ # What actually answered, so you can confirm it was the contract you meant.
115
+ print(bars.attrs["quantpad"])
116
+ # {'dataset': 'GLBX.MDP3', 'stype_in': 'raw_symbol',
117
+ # 'resolved': 'ESZ6=651968', 'clamped': False, ...}
118
+ ```
119
+
120
+ Every response reports the address it used, and a window that had to be
121
+ shortened comes back with `clamped=True` and a warning rather than quietly
122
+ covering a different period than you asked for.
123
+
124
+ ### Large ranges
125
+
126
+ `get_range` buffers a single response. When that would be too large, it falls
127
+ back automatically to reading parquet straight from object storage — the bytes
128
+ never traverse the data service. Force either behaviour with
129
+ `mode="interactive"` or `mode="bulk"`, or stream day by day:
130
+
131
+ ```python
132
+ for day in client.timeseries.iter_range(
133
+ dataset="GLBX.MDP3", symbols="ESZ6", schema="mbp-10",
134
+ start="2026-01-05", end="2026-02-05",
135
+ ):
136
+ process(day)
137
+ ```
138
+
139
+ For ranges too large even for that, submit a batch job, then hydrate it into
140
+ the cache so ordinary `get_range` calls serve it from storage:
141
+
142
+ ```python
143
+ job = client.batch.submit(
144
+ dataset="GLBX.MDP3", symbols="ESZ6", schema="mbo",
145
+ start="2026-01-05", end="2026-02-05", hydrate=True,
146
+ )
147
+ client.batch.hydrate(job["job_id"])
148
+ ```
149
+
150
+ ### Other namespaces
151
+
152
+ `client.symbology.resolve(...)`, the `client.metadata.*` mirrors
153
+ (`list_datasets`, `list_schemas`, `list_fields`, `list_publishers`,
154
+ `get_dataset_range`, `get_dataset_condition`, `get_record_count`), and
155
+ `client.options.chain(...)` for enumerating strikes and expiries without
156
+ downloading a day's entire `definition` schema.
157
+
158
+ There is no cost or unit-price surface. QuantPad data is included in the plan
159
+ rather than metered per request.
@@ -0,0 +1,138 @@
1
+ # QuantPad Data Python SDK
2
+
3
+ Install into the project's existing environment/package manager:
4
+
5
+ ```bash
6
+ uv add quantpad-data # uv project
7
+ poetry add quantpad-data # Poetry project
8
+ ```
9
+
10
+ For a plain Python project, create a local virtual environment:
11
+
12
+ ```bash
13
+ python3 -m venv .venv
14
+ source .venv/bin/activate # Windows PowerShell: .venv\Scripts\Activate.ps1
15
+ python -m pip install --upgrade pip
16
+ python -m pip install quantpad-data
17
+ export QUANTPAD_API_KEY=qp_live_...
18
+ ```
19
+
20
+ Do not install into Homebrew/system Python or use `--break-system-packages`.
21
+ When a project environment already exists, use its package manager or
22
+ `python -m pip` rather than creating a second environment. Keep
23
+ `QUANTPAD_API_KEY` in the local environment—never source code or chat.
24
+
25
+ ```python
26
+ import time
27
+ import quantpad_data as qpd
28
+
29
+ end = int(time.time() * 1000)
30
+ bars = qpd.get_bars("ES.FUT", "1m", end - 86_400_000, end)
31
+
32
+ for chunk in qpd.get_ticks(
33
+ "AAPL", "trades", end - 3_600_000, end, columns=["t", "price", "size"]
34
+ ):
35
+ print(chunk.head())
36
+
37
+ # 10-level L2 market-by-price order-book depth. This streams chunks;
38
+ # project columns to avoid materializing all 60+ book fields when unnecessary.
39
+ for chunk in qpd.get_mbp10(
40
+ "ES.FUT",
41
+ end - 3_600_000,
42
+ end,
43
+ columns=["t", "bid_px_00", "ask_px_00", "bid_sz_00", "ask_sz_00"],
44
+ ):
45
+ print(chunk.head())
46
+
47
+ matches = qpd.get_universe("apple", asset_class="equity")
48
+ coverage = qpd.get_coverage("AAPL")
49
+ ```
50
+
51
+ `QuantPadClient` (also available as `Client`) accepts `api_key=`, `base_url=`,
52
+ and `max_retries=`. The default
53
+ client honors `Retry-After` and uses exponential backoff for transient failures.
54
+ QuantPad notebooks remain compatible through `QUANTPAD_NOTEBOOK_DATA_TOKEN`.
55
+
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.
59
+
60
+ `mbp-10` is true L2 depth with bid/ask price, size, and order-count fields at
61
+ levels 0-9. It is available for supported CME futures, individual CME
62
+ options-on-futures contracts, and US equities, subject to `get_coverage()` and
63
+ the plan lookback window (currently 30 days).
64
+
65
+ This SDK intentionally exposes only bars, ticks, universe, coverage, and
66
+ symbology. It does not proxy FRED or SEC EDGAR.
67
+
68
+ ## Explicit addressing: `qpd.v2`
69
+
70
+ The helpers above infer which dataset a symbol lives in and what kind of
71
+ symbol it is. That is convenient until it is wrong, and when it is wrong it
72
+ fails quietly — asking for a specific futures contract returns an empty frame
73
+ that looks exactly like a coverage gap.
74
+
75
+ `qpd.v2.Historical` removes the inference. It takes the same arguments as
76
+ `databento.Historical`, so code written against Databento ports over by
77
+ swapping the client:
78
+
79
+ ```python
80
+ import quantpad_data as qpd
81
+
82
+ client = qpd.v2.Historical()
83
+
84
+ bars = client.timeseries.get_range(
85
+ dataset="GLBX.MDP3", # required — never guessed
86
+ symbols="ESZ6", # a specific contract month
87
+ stype_in="raw_symbol", # raw_symbol | instrument_id | parent | continuous
88
+ schema="ohlcv-1m",
89
+ start="2026-01-05",
90
+ end="2026-01-06",
91
+ )
92
+
93
+ # What actually answered, so you can confirm it was the contract you meant.
94
+ print(bars.attrs["quantpad"])
95
+ # {'dataset': 'GLBX.MDP3', 'stype_in': 'raw_symbol',
96
+ # 'resolved': 'ESZ6=651968', 'clamped': False, ...}
97
+ ```
98
+
99
+ Every response reports the address it used, and a window that had to be
100
+ shortened comes back with `clamped=True` and a warning rather than quietly
101
+ covering a different period than you asked for.
102
+
103
+ ### Large ranges
104
+
105
+ `get_range` buffers a single response. When that would be too large, it falls
106
+ back automatically to reading parquet straight from object storage — the bytes
107
+ never traverse the data service. Force either behaviour with
108
+ `mode="interactive"` or `mode="bulk"`, or stream day by day:
109
+
110
+ ```python
111
+ for day in client.timeseries.iter_range(
112
+ dataset="GLBX.MDP3", symbols="ESZ6", schema="mbp-10",
113
+ start="2026-01-05", end="2026-02-05",
114
+ ):
115
+ process(day)
116
+ ```
117
+
118
+ For ranges too large even for that, submit a batch job, then hydrate it into
119
+ the cache so ordinary `get_range` calls serve it from storage:
120
+
121
+ ```python
122
+ job = client.batch.submit(
123
+ dataset="GLBX.MDP3", symbols="ESZ6", schema="mbo",
124
+ start="2026-01-05", end="2026-02-05", hydrate=True,
125
+ )
126
+ client.batch.hydrate(job["job_id"])
127
+ ```
128
+
129
+ ### Other namespaces
130
+
131
+ `client.symbology.resolve(...)`, the `client.metadata.*` mirrors
132
+ (`list_datasets`, `list_schemas`, `list_fields`, `list_publishers`,
133
+ `get_dataset_range`, `get_dataset_condition`, `get_record_count`), and
134
+ `client.options.chain(...)` for enumerating strikes and expiries without
135
+ downloading a day's entire `definition` schema.
136
+
137
+ There is no cost or unit-price surface. QuantPad data is included in the plan
138
+ rather than metered per request.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quantpad-data"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Official Python client for the QuantPad market-data API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -194,6 +194,14 @@ def iter_l1_mbp1_df_chunks(symbol, start_ms, end_ms, chunksize=250_000,
194
194
  return iter_mbp1(symbol, start_ms, end_ms, columns=usecols, chunksize=chunksize)
195
195
 
196
196
 
197
+ from . import lean # noqa: E402 (submodule: LEAN backtesting data staging)
198
+ from . import v2 # noqa: E402 (submodule: Databento-shaped explicit API)
199
+
200
+ # Exported at the root as well as on ``v2``: the sandbox module reaches it
201
+ # as ``qpd.Historical()``, and one spelling across both surfaces means
202
+ # notebook code moves between them unchanged.
203
+ Historical = v2.Historical
204
+
197
205
  __all__ = [
198
206
  "Client", "QuantPadClient", "QuantPadDataError", "QuantPadDataNotReady", "AuthenticationError",
199
207
  "PermissionDeniedError", "RateLimitError", "QuotaExceededError",
@@ -202,5 +210,6 @@ __all__ = [
202
210
  "get_trades", "get_mbp1", "get_mbp10", "get_trades_rth", "get_mbp1_rth",
203
211
  "get_mbp10_rth", "get_spread_1m_rth", "symbology_resolve", "get_universe",
204
212
  "get_coverage", "iter_trades", "iter_mbp1", "iter_l1_df_chunks",
205
- "iter_l1_trades_df_chunks", "iter_l1_mbp1_df_chunks", "np", "pd", "pa",
213
+ "iter_l1_trades_df_chunks", "iter_l1_mbp1_df_chunks", "lean", "v2",
214
+ "Historical", "np", "pd", "pa",
206
215
  ]
@@ -24,7 +24,7 @@ from .errors import (
24
24
  ValidationError,
25
25
  )
26
26
 
27
- VERSION = "0.1.0"
27
+ VERSION = "0.2.0"
28
28
  USER_AGENT = f"quantpad-data-python/{VERSION}"
29
29
  TICK_SCHEMAS = (
30
30
  "trades", "mbp-1", "mbp-10", "cmbp-1", "tcbbo", "cbbo-1s",
@@ -88,20 +88,21 @@ class Client:
88
88
  retry_after = float(response.headers["Retry-After"])
89
89
  except (KeyError, TypeError, ValueError):
90
90
  pass
91
- if response.status_code == 400:
91
+ status = response.status_code
92
+ if status == 400:
92
93
  raise ValidationError(message)
93
- if response.status_code == 401:
94
- raise AuthenticationError(message)
95
- if response.status_code == 403:
96
- raise PermissionDeniedError(message)
97
- if response.status_code == 404:
94
+ if status == 401:
95
+ raise AuthenticationError(message, status)
96
+ if status == 403:
97
+ raise PermissionDeniedError(message, status)
98
+ if status == 404:
98
99
  raise NotFoundError(message)
99
- if response.status_code == 429:
100
+ if status == 429:
100
101
  cls = QuotaExceededError if "quota" in message.lower() else RateLimitError
101
- raise cls(message, retry_after)
102
- if response.status_code >= 500:
103
- raise UpstreamError(message)
104
- raise QuantPadDataError(message)
102
+ raise cls(message, retry_after, status)
103
+ if status >= 500:
104
+ raise UpstreamError(message, status)
105
+ raise QuantPadDataError(message, status)
105
106
 
106
107
  def _request(self, method: str, path: str, **kwargs) -> requests.Response:
107
108
  kwargs.setdefault("timeout", self.timeout)
@@ -0,0 +1,60 @@
1
+ """Typed QuantPad Data API exceptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class QuantPadDataError(RuntimeError):
7
+ """Base error for API and transport failures.
8
+
9
+ ``status_code`` is set when the failure came from an HTTP response.
10
+ Callers that want to branch on a specific status — the bulk-delivery
11
+ fallback keys on 413 — can read it without re-parsing the message.
12
+ """
13
+
14
+ #: Class-level default so the attribute is always readable, including
15
+ #: on the subclasses that also inherit ValueError and therefore never
16
+ #: run this __init__.
17
+ status_code: int | None = None
18
+
19
+ def __init__(self, message: str, status_code: int | None = None):
20
+ super().__init__(message)
21
+ self.status_code = status_code
22
+
23
+
24
+ class AuthenticationError(QuantPadDataError):
25
+ pass
26
+
27
+
28
+ class PermissionDeniedError(QuantPadDataError):
29
+ pass
30
+
31
+
32
+ class RateLimitError(QuantPadDataError):
33
+ def __init__(
34
+ self,
35
+ message: str,
36
+ retry_after: float | None = None,
37
+ status_code: int | None = None,
38
+ ):
39
+ super().__init__(message, status_code)
40
+ self.retry_after = retry_after
41
+
42
+
43
+ class QuotaExceededError(RateLimitError):
44
+ pass
45
+
46
+
47
+ class NotFoundError(ValueError, QuantPadDataError):
48
+ pass
49
+
50
+
51
+ class ValidationError(ValueError, QuantPadDataError):
52
+ pass
53
+
54
+
55
+ class UpstreamError(QuantPadDataError):
56
+ pass
57
+
58
+
59
+ class QuantPadDataNotReady(QuantPadDataError):
60
+ """Backward-compatible error retained for older notebooks."""
@@ -0,0 +1,153 @@
1
+ """Stage QuantConnect LEAN futures data into a local data folder.
2
+
3
+ Flow (run inside the LEAN sandbox before the engine starts):
4
+
5
+ from quantpad_data import lean
6
+ lean.prepare_and_wait(["MNQ.FUT"], "2025-01-06", "2025-01-31", "minute")
7
+ lean.stage_data(["MNQ.FUT"], "2025-01-06", "2025-01-31", "minute",
8
+ data_folder="/home/jovyan/lean-data")
9
+
10
+ ``prepare`` asks the data service to convert/cache the range (202 + job);
11
+ ``stage_data`` fetches the manifest (authenticated) and downloads each
12
+ artifact directly from R2 via short-lived presigned URLs — no zip bytes flow
13
+ through the data service.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import time
20
+ from pathlib import Path
21
+ from typing import Sequence
22
+
23
+ import requests
24
+
25
+ from .client import Client
26
+ from .errors import QuantPadDataError
27
+
28
+
29
+ class LeanStageError(QuantPadDataError):
30
+ """Raised when preparing or staging LEAN data fails."""
31
+
32
+
33
+ def _client(client: Client | None) -> Client:
34
+ return client if client is not None else Client()
35
+
36
+
37
+ def prepare(
38
+ symbols: Sequence[str],
39
+ start: str,
40
+ end: str,
41
+ resolution: str,
42
+ *,
43
+ client: Client | None = None,
44
+ ) -> dict:
45
+ """Kick off conversion; returns the 202 body (``job_id``, ``poll_url``)."""
46
+ response = _client(client)._request(
47
+ "POST",
48
+ "/v1/lean/prepare",
49
+ json={
50
+ "symbols": list(symbols),
51
+ "start": start,
52
+ "end": end,
53
+ "resolution": resolution,
54
+ },
55
+ )
56
+ return response.json()
57
+
58
+
59
+ def wait_for_job(
60
+ job_id: str,
61
+ *,
62
+ client: Client | None = None,
63
+ poll_seconds: float = 2.0,
64
+ timeout_seconds: float = 1800.0,
65
+ ) -> dict:
66
+ """Poll ``/v1/jobs/{job_id}`` until completed; raises on failure/timeout."""
67
+ bound = _client(client)
68
+ deadline = time.monotonic() + timeout_seconds
69
+ while True:
70
+ status = bound._request("GET", f"/v1/jobs/{job_id}").json()
71
+ if status.get("status") == "completed":
72
+ return status
73
+ if status.get("status") == "failed":
74
+ raise LeanStageError(
75
+ f"lean prepare job {job_id} failed: {status.get('error')}"
76
+ )
77
+ if time.monotonic() > deadline:
78
+ raise LeanStageError(f"lean prepare job {job_id} timed out")
79
+ time.sleep(poll_seconds)
80
+
81
+
82
+ def prepare_and_wait(
83
+ symbols: Sequence[str],
84
+ start: str,
85
+ end: str,
86
+ resolution: str,
87
+ *,
88
+ client: Client | None = None,
89
+ poll_seconds: float = 2.0,
90
+ timeout_seconds: float = 1800.0,
91
+ ) -> dict:
92
+ accepted = prepare(symbols, start, end, resolution, client=client)
93
+ return wait_for_job(
94
+ accepted["job_id"],
95
+ client=client,
96
+ poll_seconds=poll_seconds,
97
+ timeout_seconds=timeout_seconds,
98
+ )
99
+
100
+
101
+ def get_manifest(
102
+ symbols: Sequence[str],
103
+ start: str,
104
+ end: str,
105
+ resolution: str,
106
+ *,
107
+ client: Client | None = None,
108
+ ) -> dict:
109
+ response = _client(client)._request(
110
+ "GET",
111
+ "/v1/lean/manifest",
112
+ params={
113
+ "symbols": ",".join(symbols),
114
+ "start": start,
115
+ "end": end,
116
+ "resolution": resolution,
117
+ },
118
+ )
119
+ return response.json()
120
+
121
+
122
+ def stage_data(
123
+ symbols: Sequence[str],
124
+ start: str,
125
+ end: str,
126
+ resolution: str,
127
+ *,
128
+ data_folder: str | os.PathLike[str],
129
+ client: Client | None = None,
130
+ download_session: requests.Session | None = None,
131
+ ) -> dict:
132
+ """Download a prepared range into ``data_folder`` laid out for LEAN.
133
+
134
+ Returns the manifest (including ``mapping_modes``, which the run guard
135
+ validates against the algorithm's requested mapping mode before the
136
+ engine starts).
137
+ """
138
+ manifest = get_manifest(symbols, start, end, resolution, client=client)
139
+ session = download_session if download_session is not None else requests.Session()
140
+ root = Path(data_folder)
141
+ for entry in manifest["files"]:
142
+ relative = Path(entry["path"])
143
+ if relative.is_absolute() or ".." in relative.parts:
144
+ raise LeanStageError(f"unsafe manifest path: {entry['path']}")
145
+ target = root / relative
146
+ target.parent.mkdir(parents=True, exist_ok=True)
147
+ response = session.get(entry["url"], timeout=120)
148
+ if response.status_code != 200:
149
+ raise LeanStageError(
150
+ f"download failed ({response.status_code}) for {entry['path']}"
151
+ )
152
+ target.write_bytes(response.content)
153
+ return manifest