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

NeedUse
Compacted ticks / bars in the consoleDatasets + 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 syncJobs normalized_export
Direct S3 on a machine that already has AWSNormalizedClient + 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

FieldMeaning
rows / columnsThis page
row_countRows in this page
total_row_countFull window when known
release_idsDistinct releases used (one per day on the official lake)
next_cursorPass back as cursor until it is null
metadataEstimates 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

GateDefaultWhat happens
Symbols≤ 20422 sync_too_large
Calendar days≤ 5422 sync_too_large
Fattest symbol-day (no limit)≤ 12,000,000 rows422 suggests normalized_export
JSON page2,000,000 rowsSplit; follow next_cursor
Explicit limit≤ 5,000422 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:

  • symbols and datasets are uppercased/normalized, deduplicated, and stably sorted;
  • strict=true fails the whole Job when any requested symbol/dataset is absent;
  • strict=false publishes available data and records each missing combination in the manifest;
  • batch output always retains symbol and 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

HTTPcodeTypical cause
404normalized_activation_not_foundDate not activated yet
422sync_too_largeN, D, or one ticker-day over the sync cap — use normalized_export
422limit_exceededlimit > 5,000
422invalid_specExport request exceeds the active rollout (for example multi-symbol planner or combined datasets is still disabled)
422unsupported_datasetNot quotes/trades
422invalid_queryEmpty columns, inverted start_ns/end_ns
403data_plane_disabledOrg data plane off (Admin → Customers)
429quotaMonthly 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).