Metadata-Version: 2.5
Name: quantpad-data
Version: 0.6.0
Summary: Official Python client for the QuantPad market-data API
Project-URL: Documentation, https://api.quantpad.ai/external/docs
Author: QuantPad
License: Proprietary
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: pandas<3,>=2.2
Requires-Dist: pyarrow>=18
Requires-Dist: requests>=2.32
Provides-Extra: rth
Requires-Dist: exchange-calendars>=4.5; extra == 'rth'
Provides-Extra: test
Requires-Dist: build>=1.2; extra == 'test'
Requires-Dist: exchange-calendars>=4.5; extra == 'test'
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown

# QuantPad Data Python SDK

Install into the project's existing environment/package manager:

```bash
uv add quantpad-data       # uv project
poetry add quantpad-data   # Poetry project
```

For a plain Python project, create a local virtual environment:

```bash
python3 -m venv .venv
source .venv/bin/activate  # Windows PowerShell: .venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install quantpad-data
export QUANTPAD_API_KEY=qp_live_...
```

Do not install into Homebrew/system Python or use `--break-system-packages`.
When a project environment already exists, use its package manager or
`python -m pip` rather than creating a second environment. Keep
`QUANTPAD_API_KEY` in the local environment—never source code or chat.

```python
import time
import quantpad_data as qpd

end = int(time.time() * 1000)
bars = qpd.get_bars("ES.FUT", "1m", end - 86_400_000, end)

for chunk in qpd.get_ticks(
    "AAPL", "trades", end - 3_600_000, end, columns=["t", "price", "size"]
):
    print(chunk.head())

# 10-level L2 market-by-price order-book depth. This streams chunks;
# project columns to avoid materializing all 60+ book fields when unnecessary.
for chunk in qpd.get_mbp10(
    "ES.FUT",
    end - 3_600_000,
    end,
    columns=["t", "bid_px_00", "ask_px_00", "bid_sz_00", "ask_sz_00"],
):
    print(chunk.head())

matches = qpd.get_universe("apple", asset_class="equity")
coverage = qpd.get_coverage("AAPL")
```

`QuantPadClient` (also available as `Client`) accepts `api_key=`, `base_url=`,
and `max_retries=`. The default
client honors `Retry-After` and uses exponential backoff for transient failures.
A read timeout is the exception and is never retried: the budget was already
spent in full, so the request is probably still running on the server, and
repeating it adds load rather than replacing it. Connection failures and
connect timeouts are still retried, because failing to reach the service
costs the service nothing.
QuantPad notebooks remain compatible through `QUANTPAD_NOTEBOOK_DATA_TOKEN`.

Bars preserve the notebook helper's OHLCV aliases and smart futures
back-adjustment default. Tick timestamps are epoch nanoseconds.

`mbp-10` is true L2 depth with bid/ask price, size, and order-count fields at
levels 0-9. It is available for supported CME futures, individual CME
options-on-futures contracts, and US equities, subject to `get_coverage()` and
the plan lookback window (currently 30 days).

This SDK intentionally exposes only bars, ticks, universe, coverage, and
symbology. It does not proxy FRED or SEC EDGAR.

## Explicit addressing: `qpd.v2`

> **Not open yet.** `/v2` is limited to a small allowlist while it is
> finished, so everything in this section will raise
> `V2NotAvailableError` for you today — an API key is refused with 403
> and a notebook token with 404, whatever arguments you pass. Nothing
> above this section is affected: the `/v1` helpers carry the same data
> and are generally available. Contact QuantPad support if you need
> `/v2` access. This notice comes down when the gate does.

The helpers above infer which dataset a symbol lives in and what kind of
symbol it is. That is convenient until it is wrong, and when it is wrong it
fails quietly — asking for a specific futures contract returns an empty frame
that looks exactly like a coverage gap.

`qpd.v2.Historical` removes the inference. It takes the same arguments as
`databento.Historical`, so code written against Databento ports over by
swapping the client:

```python
import quantpad_data as qpd

client = qpd.v2.Historical()

bars = client.timeseries.get_range(
    dataset="GLBX.MDP3",       # required — never guessed
    symbols="ESZ6",            # a specific contract month
    stype_in="raw_symbol",     # raw_symbol | instrument_id | parent | continuous
    schema="ohlcv-1m",
    start="2026-01-05",
    end="2026-01-06",
)

# What actually answered, so you can confirm it was the contract you meant.
print(bars.attrs["quantpad"])
# {'dataset': 'GLBX.MDP3', 'stype_in': 'raw_symbol',
#  'resolved': 'ESZ6=651968', 'clamped': False, ...}
```

Every response reports the address it used, and a window that had to be
shortened comes back with `clamped=True` and a warning rather than quietly
covering a different period than you asked for.

### Large ranges

`get_range` buffers a single response, so it tells the service up front how much
this process can hold. Two different things can then be too large, and they are
answered differently:

- **Too large for the service to assemble in one response.** `get_range` falls
  back automatically to reading parquet straight from object storage — the bytes
  never traverse the data service.
- **Too large for this process to hold.** You get a `ResponseTooLargeError`
  naming the estimated size, because no delivery mode makes a frame fit in
  memory it does not fit in. Stream it with `iter_range` instead, or pass
  `max_decoded_bytes=` if the memory really is available.

The budget is derived from the memory visible to the process (a container's
limit, if there is one). Override it per call with `max_decoded_bytes=`, or
globally with the `QUANTPAD_MAX_DECODED_BYTES` environment variable. Force a
delivery path with `mode="interactive"` or `mode="bulk"`, or stream day by day:

```python
for day in client.timeseries.iter_range(
    dataset="GLBX.MDP3", symbols="ESZ6", schema="mbp-10",
    start="2026-01-05", end="2026-02-05",
):
    process(day)
```

`iter_range` holds one day at a time rather than the whole window, and splits
the window itself when a single request would have to fetch too much uncached
data at once. On a cold window the first frame can take a while to arrive, since
the days behind it are fetched before it is handed over.

For ranges too large even for that, submit a batch job, then hydrate it into
the cache so ordinary `get_range` calls serve it from storage:

```python
job = client.batch.submit(
    dataset="GLBX.MDP3", symbols="ESZ6", schema="mbo",
    start="2026-01-05", end="2026-02-05", hydrate=True,
)
client.batch.hydrate(job["job_id"])
```

### Regular trading hours

`get_range` returns the whole published session, which for US equities includes
pre- and post-market. That is what the upstream feed publishes, and it is more
data than QuantPad's older `/v1` tick endpoints returned: those enumerated
exchange sessions, and the XNYS calendar's open and close *are* regular hours,
so `/v1` excluded the extended session without saying so — around 7% of the
rows on a liquid name.

If you want that narrower scope, ask for it:

```python
df = client.timeseries.get_range_rth(
    dataset="XNAS.ITCH", symbols="BA", schema="trades",
    start="2026-09-16", end="2026-09-17",
)
```

`iter_range_rth` is the streaming form. Both follow the exchange calendar, so
half-days close early and holidays are skipped, and both filter on `ts_event`
— the clock `/v1` presented as its `t` column — so the numbers are comparable
with a `/v1` result. Requires the calendar extra: `pip install
'quantpad-data[rth]'`.

The window sent to the service is still the whole range; the narrowing happens
here. So the size budget applies to the full session either way, and a window
too large unfiltered is too large for `get_range_rth` too — use
`iter_range_rth` for those.

### The disk cache

Settled `/v2` answers are cached on disk, so asking twice costs the network
once. Measured against production on a day of `BA` `mbp-10` (237,916 rows):
1.30s cold, 0.04s warm for `get_range`, and 1.01s to 0.14s for `iter_range`,
which caches each day's partition separately.

Only *final* answers are stored. A window reaching into the current day is
never cached, and neither is one the service clamped at the end, because both
legitimately return more data later. A cached answer carries the same
`.attrs['quantpad']` as the network one, so a hit and a miss are
indistinguishable to your code.

Defaults, all overridable:

| Variable | Default | Meaning |
| --- | --- | --- |
| `QPD_CACHE` | `1` | Set `0` to disable entirely |
| `QPD_CACHE_DIR` | platform user-cache dir | Where entries live |
| `QPD_CACHE_MAX_BYTES` | 1 GiB | Least-recently-used entries are evicted past this |

The cache is best-effort by design: a full disk, an unwritable directory or a
corrupt entry costs you the speedup and never the request.

```python
from quantpad_data import cache

cache.total_bytes()   # how much is on disk
cache.clear()         # throw it all away
```

### Other namespaces

`client.symbology.resolve(...)`, the `client.metadata.*` mirrors
(`list_datasets`, `list_schemas`, `list_fields`, `list_publishers`,
`get_dataset_range`, `get_dataset_condition`, `get_record_count`), and
`client.options.chain(...)` for enumerating strikes and expiries without
downloading a day's entire `definition` schema.

There is no cost or unit-price surface. QuantPad data is included in the plan
rather than metered per request.
