Docs/Normalized quotes & trades
Normalized quotes & trades
How to read the official normalized quotes/trades lake from the platform API. You only need a PQC API key (pk_live_…). You do not receive lake AWS credentials.
This is a second research dataset next to compacted Athena ticks (quotes_v1 / trades_v1). Same account, quotas, and jobs model — different physical lake and different endpoints.
Related: Getting started, Research environment, Usage & plans.
When to use which path
| Need | Use |
|---|---|
| Compacted ticks / bars in the console | Datasets + Explore, or POST /v1/data/query |
| Normalized quotes or trades (API key only) | POST /v1/data/normalized/query or client.normalized.platform_query |
| A window too large for sync | Jobs normalized_export |
| Direct S3 on a machine that already has AWS | NormalizedClient + an explicit release_id (see SDK NORMALIZED.md) |
Datasets / Explore do not list normalized types. Those pages stay on Athena so a console query cannot hit the wrong lake.
Identity
Each US SIP trading date has at most one active release_id (32 lowercase hex). Quotes and trades for that day share the same release. There is no implicit latest. If a date is not activated, the API returns 404 — it does not guess.
Discover dates from the catalog. Do not hard-code a calendar; coverage grows as operations publishes and the platform activates.
Production origin: https://particles-data.com/backend. Nginx strips /backend onto the API. All examples below use that origin.
export PQC_BASE_URL=https://particles-data.com/backend
export PQC_API_KEY=pk_live_… # Settings → API keys
1. Catalog — which dates are readable
Auth: Authorization: Bearer $PQC_API_KEY (data:read).
GET /v1/catalog/normalized/releases?status=active
GET /v1/catalog/normalized/releases?start_date=2026-06-01&end_date=2026-06-30&status=active
GET /v1/catalog/normalized/dates/{YYYY-MM-DD}
GET /v1/catalog/normalized/releases/{release_id}
from pqc import Client
with Client.from_project() as client:
days = client.normalized.list_releases(
start_date="2026-06-01",
end_date="2026-06-30",
)
one = client.normalized.resolve_release("2026-06-01")
print(one.release_id, [row["trading_date"] for row in days][:3])
list_releases is a JSON array. Each row includes trading_date, release_id, status, run_id, and partition_count. A missing date is 404 with code=normalized_activation_not_found.
GET /v1/catalog/datasets still lists only compacted Athena types (quotes_v1, trades_v1, aggregates). Do not look for normalized_* there.
2. Sync query — small and medium windows
POST /v1/data/normalized/query
{
"dataset_type": "quotes",
"symbols": ["AAPL"],
"start_date": "2026-06-01",
"end_date": "2026-06-01",
"columns": ["symbol", "trading_date", "event_ts_ns", "bid_price_int", "price_scale"],
"limit": 5
}
dataset_type is quotes or trades (aliases: normalized_quotes_v1, normalized_trade_events_v1). Symbols are uppercased; BRK.B is valid. Omit end_date to query a single day.
from pqc import Client
with Client.from_project() as client:
page = client.normalized.platform_query(
dataset_type="quotes",
symbols=["AAPL", "MSFT"],
start_date="2026-06-01",
end_date="2026-06-02",
columns=["symbol", "trading_date", "event_ts_ns", "bid_price_int", "price_scale"],
limit=5,
)
print(page["row_count"], page["release_ids"])
Equivalent: client.data.normalized_query(...).
Leave release_id unset so the API resolves the active release per trading date. Pinning release_id applies that one closure to every day in the window — only do that for a single-day replay.
Response
| Field | Meaning |
|---|---|
rows / columns | This page |
row_count | Rows in this page |
total_row_count | Full window when known |
release_ids | Distinct releases used (one per day on the official lake) |
next_cursor | Pass back as cursor until it is null |
metadata | Estimates and request shape |
Prices stay int64. Convert explicitly:
from decimal import Decimal
price = Decimal(row["bid_price_int"]) / Decimal(row["price_scale"])
start_ns / end_ns are a half-open [start, end) filter on event_ts_ns after the symbol's row groups are read.
Sync gates
| Gate | Default | What happens |
|---|---|---|
| Symbols | ≤ 20 | 422 sync_too_large |
| Calendar days | ≤ 5 | 422 sync_too_large |
Fattest symbol-day (no limit) | ≤ 12,000,000 rows | 422 suggests normalized_export |
| JSON page | 2,000,000 rows | Split; follow next_cursor |
Explicit limit | ≤ 5,000 | 422 limit_exceeded if larger |
Window totals are not rejected. A multi-symbol, multi-day request can return more than 2M rows across pages. Repeat the same body and set cursor from the previous page:
cursor = None
while True:
page = client.normalized.platform_query(
dataset_type="quotes",
symbols=["AAPL"],
start_date="2026-06-01",
cursor=cursor,
)
# consume page["rows"]
cursor = page.get("next_cursor")
if not cursor:
break
limit cuts the concatenated table from the start. It is not required. A full liquid ETF day without limit is valid if that ticker-day is under 12M rows (QQQ on 2026-06-09 is about 10.9M quotes). Prefer a short start_ns/end_ns window or limit when you are probing.
3. Async export — large pulls
When sync returns sync_too_large, or you want Parquet instead of JSON, submit a normalized_export Job.
Current production rollout
The new batch protocol is deployed, but it is being enabled in stages. Production currently has:
NORMALIZED_BATCH_API_ENABLED=true
NORMALIZED_BATCH_PLANNER_ENABLED=false
NORMALIZED_COMBINED_ARTIFACT_ENABLED=false
NORMALIZED_OBJECT_CACHE_ENABLED=false
That means the existing single-symbol, single-dataset export path is available now. Multi-symbol planning and combined quotes+trades artifacts are implemented but not yet globally enabled. Requests beyond the active rollout return a clear 422 invalid_spec; they are not silently split or partially executed.
Compatible Job request (available now)
{
"type": "normalized_export",
"spec": {
"symbol": "AAPL",
"dataset_type": "quotes",
"trade_date": "2026-06-01",
"format": "parquet"
}
}
The older symbols: ["AAPL"] plus start_date/end_date spelling remains accepted. A legacy request returns one export.parquet (or export.csv) artifact.
job = client.jobs.create(
"normalized_export",
spec={
"symbol": "AAPL",
"dataset_type": "quotes",
"trade_date": "2026-06-01",
"format": "parquet",
},
)
done = client.jobs.wait(job)
Batch SDK contract (staged)
SDK 0.3.3 includes the server-side batch interface. During the current rollout, pass exactly one symbol and one dataset:
result = client.normalized.export_day(
date="2026-06-01",
symbols=["AAPL"],
datasets=["quotes"],
).wait()
paths = result.download(
"./normalized/2026-06-01",
reuse=True,
verify_checksum=True,
)
After planner and combined-artifact rollout, the same method accepts a symbol list and datasets=["quotes", "trades"]. The server then fixes the active release snapshot at Job creation, persists an immutable extent/object plan, opens each planned shard once, streams filtered batches into one Parquet per dataset, and adds a checksummed manifest.json.
Batch semantics:
symbolsanddatasetsare uppercased/normalized, deduplicated, and stably sorted;strict=truefails the whole Job when any requested symbol/dataset is absent;strict=falsepublishes available data and records each missing combination in the manifest;- batch output always retains
symboland never exposes source bucket/object keys; - artifacts are staged, size/checksum verified, and only become visible after a committed publish;
- retry uses the release and extent plan fixed when the Job was created.
General optional fields remain columns, start_ns, end_ns, limit, release_id, release_by_date, and format. Multi-dataset batch output is Parquet with layout=by_dataset; CSV remains a legacy single-dataset format. Calendar span is at most 31 days and the implemented batch symbol cap is 50, although the current production rollout effectively permits one export symbol until the planner flag is enabled.
data_export stays on compacted Athena. Do not mix the two lakes in one spec. Artifacts are a short-lived cache (about 48 hours), so copy them to your research machine if you need to keep them. See Console guide.
4. Errors you will actually see
| HTTP | code | Typical cause |
|---|---|---|
| 404 | normalized_activation_not_found | Date not activated yet |
| 422 | sync_too_large | N, D, or one ticker-day over the sync cap — use normalized_export |
| 422 | limit_exceeded | limit > 5,000 |
| 422 | invalid_spec | Export request exceeds the active rollout (for example multi-symbol planner or combined datasets is still disabled) |
| 422 | unsupported_dataset | Not quotes/trades |
| 422 | invalid_query | Empty columns, inverted start_ns/end_ns |
| 403 | data_plane_disabled | Org data plane off (Admin → Customers) |
| 429 | quota | Monthly data_read exhausted — Usage & plans |
Sync and export both increment data reads. A full-day tick pull is expensive; start with limit or a few minutes of start_ns/end_ns.
5. Direct S3 (optional)
If your research machine already has AWS credentials that can GetObject on the official prefix, NormalizedClient reads parquet without the control-plane proxy. You still must pass a release_id or resolve the date pointer — never list the bucket, never assume latest.
Most console users should stay on platform_query. Direct S3 is documented in the SDK package (NORMALIZED.md) after you install particles-quant[research]==0.3.3 from CodeArtifact (Research environment).