Metadata-Version: 2.5
Name: quantpad-data
Version: 0.5.1
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.
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.
