Data sources#
This page covers how quantlab gets vendor data onto your disk: the vendors it knows, credentials, the two ways to download, where the files land, how an interrupted download resumes, and how to inspect what is already downloaded. Turning downloaded files into panels is covered in Datasets. Each vendor with concepts of its own has a page: Sharadar, Massive and WRDS.
The runnable companion is
examples/inspect_data_sources.py.
It needs no credentials or network access.
Raw tier and panel#
A download writes a raw tier: Parquet files holding the vendor’s rows as they arrived,
plus small JSON bookkeeping files. Nothing downstream reads the raw tier. A separate
conversion step turns it into a Zarr store holding a (timestamp, symbol) panel, which
is what factors, models and backtests read.
The two steps are separate so each can be interrupted and resumed on its own, and so a raw tier can be converted again with other settings without downloading anything.
The vendors#
Every vendor is described once by a SourceDescriptor
in the DataSourceRegistry. A descriptor lists the
environment variables holding the vendor’s credentials and its capabilities: each
(market, frequency, data_type) combination it serves, with the dataset class that
converts it to a panel.
Vendor |
Serves |
Credentials |
|---|---|---|
|
US stock and fund daily prices, fundamentals, valuations, events, insider and 13F holdings, S&P 500 membership, tickers |
|
|
Every US-equity SIP trade, minute and day aggregates, dividends, short interest and volume, news, 8-K disclosures, IPOs |
|
|
Interest-rate series such as DTB3, the 3-month Treasury bill rate |
none |
|
US equity daily bars |
|
|
US equity daily and minute bars; tick quotes and trades |
|
|
CRSP daily stock file; TAQ NBBO quotes (retired) |
|
Sharadar, Massive and FRED are the sources the daily data update keeps current. Tiingo and Alpaca are registered, tested and convertible library interfaces, but nothing in the project downloads from them. WRDS is retired: its code still converts existing raw files, see WRDS.
Alpaca’s quotes and trades are individually timestamped events on an irregular time axis.
No dense panel can hold them without inventing data, so they stay in the raw tier, where
you read them with polars. Binance crypto klines are not a registry source:
SpotKlineDataset converts monthly CSV files you have
downloaded yourself from Binance’s bulk-download site.
Asking the registry#
>>> from quantlab.acquisition.base import DataSourceRegistry
>>> [d.vendor for d in DataSourceRegistry.all()]
['alpaca', 'fred', 'massive', 'sharadar', 'tiingo', 'wrds']
>>> wrds = DataSourceRegistry.get("wrds")
>>> wrds.supports("us_equity", "tick", "nbbo")
True
>>> wrds.supports("us_equity", "1m")
False
>>> alpaca = DataSourceRegistry.get("alpaca")
>>> [c.data_type for c in alpaca.capabilities_for("us_equity", "tick")]
['quotes', 'trades']
Importing quantlab.acquisition.registry imports every vendor module, so the
listing is always complete.
Credentials#
Credentials are read from environment variables and nowhere else. No function takes a key as an argument and no config field holds one, because configs are saved as JSON beside model checkpoints. Keys are removed from every error message before it is logged or written to disk.
export SHARADAR_API_KEY=...
export MASSIVE_API_KEY=...
export MASSIVE_S3_ACCESS_KEY_ID=...
credential_status() returns {name: is_set} and
is_configured() whether every variable is set and
non-empty. Neither reads a value into anything else.
>>> from quantlab.acquisition.registry import credential_status, is_configured
>>> credential_status(wrds)
{'WRDS_USERNAME': False}
>>> is_configured(wrds)
False
A credential is demanded only when a vendor client is constructed, the moment a download starts, and a missing one raises before any request is made. Sizing a request, inspecting files and converting raw files to a panel work without one.
Two ways to download#
The registry downloads in one of two ways, chosen by what the caller knows.
Keep a raw tier current.
refresh_raw_tier(source, download_dir)
brings every part of the vendor’s raw tier (a Sharadar table, a Massive REST endpoint, a
FRED series) from its watermark up to today. The caller names no roster and no window.
This is what the daily update calls. It returns a
RawTierRefreshResult with one entry per part; a
part the vendor or the network failed is recorded in failed and the other parts still
run, while a missing credential raises before any part. The options are the vendor’s own:
trading_days for Sharadar, endpoints for Massive, series and start for FRED.
from quantlab.acquisition.base import DataSourceRegistry
from quantlab.acquisition.registry import refresh_raw_tier
result = refresh_raw_tier(DataSourceRegistry.get("fred"), "/data/quantlab/downloads",
series=["DTB3"], start="1954-01-04")
result.failed
Tiingo, Alpaca and WRDS register no raw-tier refresh, and refresh_raw_tier refuses them:
>>> refresh_raw_tier(DataSourceRegistry.get("tiingo"), "downloads")
Traceback (most recent call last):
...
ValueError: Tiingo EOD registers no raw-tier refresh; download it with download_symbols().
Download a roster over a window.
download_symbols(source, config)
runs the vendor’s SymbolBatchAcquisition
over the symbols and dates of an AcquisitionConfig
and returns an AcquisitionResult. Only the
symbol-batched vendors have one (Tiingo, Alpaca, FRED, WRDS); Sharadar and Massive are
refused with a pointer to refresh_raw_tier. A descriptor’s config_factory builds a
config with the vendor’s standard paths. This needs TIINGO_API_KEY and the network, so no
output is shown:
from quantlab.acquisition.registry import download_symbols
source = DataSourceRegistry.get("tiingo")
config = source.config_factory(
symbols=("AAPL", "MSFT"),
start_date="2024-01-01",
end_date="2024-05-31",
kwargs={"max_workers": 4},
)
result = download_symbols(source, config)
print(result.succeeded, result.failures, result.coverage)
result.failures maps each symbol that failed in this run to a message;
result.cancelled and result.quota_aborted say whether the run stopped early. Per-run
tuning lives in config.kwargs, so a run is fully described by its config: batch_size
(symbols per request), max_workers (batches in flight), resume, progress, the quota
options below, and vendor options such as Alpaca’s data_type and feed.
The FRED series is a complete example that needs no key. A rate is published in annualized
percent; FredRateDataset keeps it as rate and adds
risk_free, a decimal return per trading day. A holiday has no observation and is NaN.
from quantlab.acquisition.config import AcquisitionConfig
from quantlab.acquisition.registry import convert, download_symbols
from quantlab.dataset.config import FredRateConfig
from quantlab.dataset.fred import FredRateDataset
source = DataSourceRegistry.get("fred")
download_symbols(source, AcquisitionConfig(
market="us_equity", frequency="1d", vendor="fred",
raw_data_dir_path="downloads/fred", watermark_path="downloads/_watermarks/fred",
symbols=("DTB3",), start_date="1954-01-04",
))
config = FredRateConfig(zarr_file_path="fred_dtb3_1d.zarr", raw_data_dir_path="downloads/fred")
convert(source, config)
panel = FredRateDataset(config).panel("2024-12-20", "2024-12-31")
print(panel["rate"].sel(symbol="DTB3").to_series().round(2).to_string())
timestamp
2024-12-20 4.23
2024-12-23 4.24
2024-12-24 4.28
2024-12-25 NaN
2024-12-26 4.24
2024-12-27 4.19
2024-12-30 4.23
2024-12-31 4.23
Progress and cancelling#
A download draws a progress bar on stderr by default. To follow it from your own code,
pass reporter=: CallbackProgressReporter calls a
function with each ProgressEvent, and
NullProgressReporter discards them. An exception
raised inside a reporter is logged and ignored, so a bug in a progress display cannot end
a long download.
A reporter cannot stop a run; a CancelToken passed as
cancel= can, from any thread. The run stops at the next batch boundary, keeps every
completed batch on disk, and a later run resumes from there. In the example, the token is
set as soon as the first batch lands:
from quantlab.utils.progress import CallbackProgressReporter, CancelToken
token = CancelToken()
def stop_after_first_batch(event):
if event.kind == "batch_completed":
token.cancel()
result = download_symbols(source, wider_config,
reporter=CallbackProgressReporter(stop_after_first_batch),
cancel=token)
print("cancelled:", result.cancelled, "newly completed:", result.succeeded)
cancelled: True newly completed: ('AMD',)
convert() takes the same two arguments and stops
between time windows.
Where files land#
Library paths derive from one data root: the QUANTLAB_DATA_DIR environment variable,
else the data/ directory of the repository in a source checkout and ~/.quantlab/data
in an installed package. set_data_root() overrides it for the
process; call it before building any config, because configs record their paths when they
are created.
Raw downloads live under downloads/, one directory per vendor, so two vendors’ rows
never mix. The bookkeeping lives in a sibling _watermarks/ directory rather than inside
the raw tree, because a polars directory scan reads every file under the root it is
given. After the example’s Tiingo-layout download the tree looks like this:
<root>/downloads/us_equity/1d/nasdaq_data/
tiingo/month=2024-01/part-6f28d5ecbc46ad74-00000.pqt raw shards
tiingo/month=2024-02/...
_watermarks/tiingo/AAPL.json one sidecar per symbol
_watermarks/tiingo/_failures.json failure manifest
_watermarks/tiingo/_pages/6f28d5ecbc46ad74.pages.json page ledgers
<root>/data/us_equity/1d/stock.zarr the converted panel
Each Parquet file is a shard: the rows of one request page. Directories named
key=value form a hive layout, so a reader skips whole months (daily data), days
(minute data) or day and symbol directories (tick data) without opening them. A vendor
with several data types keeps each in a directory of its own (alpaca/quotes/,
alpaca/trades/). Shard names are derived from the request, so the same request always
writes the same file names. Raw tick data is written exactly as the vendor sent it.
Shared stores are kept one per folder, <root>/<category>/<group>/<name>/<name>.zarr,
beside a short README.md: price bars and vendor tables under market/<vendor>/,
membership panels under universe/<group>/.
Resuming an interrupted download#
You resume a symbol-batched download by running the same call again. The state that drives resumption lives on disk.
A watermark is a small JSON sidecar per symbol, written once the symbol’s batch completes:
{"last_date": "2024-01-05", "start_date": "2024-01-02"}
Before fetching anything, a run classifies every requested symbol against the window and skips the ones already covered. Running the example’s first download a second time fetches nothing:
coverage: {'requested': 3, 'pending': 0, 'skipped': 3, 'covered': 3, 'widened': 0, 'legacy': 0, 'no_data': 0}
Class |
Meaning |
|---|---|
|
Already downloaded over the window. Skipped. |
|
No sidecar, or it ends elsewhere than the window end. Fetched. |
|
Ends on the window end but starts later than the window start, so history is missing. Fetched. |
|
The vendor was asked about this window and returned nothing. Recorded so the same window is not asked again; not a failure. |
|
A sidecar from an older version with no start date. Skipped with a warning; |
Three variations follow from this:
Move the window forward.
download_symbols(..., refresh=True)fetches each symbol from its own last date to the window end and never widens the start. The last covered day is fetched twice; conversion keeps the later row.Backfill earlier history. Lower
start_dateand download again: symbols whose recorded start is later becomewidenedand are fetched, the rest are skipped.Start over.
kwargs["resume"] = Falseignores the watermarks and requests every page again; each page overwrites its own shard.
Vendors that answer one request as a chain of pages (Alpaca, and WRDS, where a page is one
trading day or one calendar year) also keep a page ledger per batch under _pages/. It
records which pages have landed and the token of the next one, so an interrupted batch
resumes at its next page rather than starting over. Each page’s shard is written before
the ledger records it, so a crash between the two refetches that page and overwrites its
own file: nothing is duplicated or lost. If a shard the ledger records has gone missing,
the batch is refused rather than resumed with a hole; restore the file, or delete the
ledger named in the message so the batch restarts at page 0. See
Internals for how the ledger works.
Symbols that fail are listed with the reason in _failures.json. A failure in one batch
never stops the others, and a failed symbol has no watermark, so the next run tries it
again. The manifest accumulates across runs, and a symbol leaves it when it succeeds.
Rate limits and quotas#
Each vendor class decides what a refusal means. A dropped connection, a 5xx or a
short-lived 429 is repeated after the server’s Retry-After when it sends one, otherwise
after a wait that doubles from 2 seconds, up to 5 times (2, 4, 8, 16 and 32 seconds),
before the batch is recorded as failed for the next run. Only the failed page is requested
again.
Tiingo answers HTTP 429 when the account’s hourly allocation is spent. That affects every
remaining symbol, so the run stops sending requests at once, result.quota_aborted is
true, and the symbols not reached are not recorded as failures. A run that hit a quota can
have an empty failure manifest with symbols still pending; check quota_aborted or the
coverage report. To have the run wait instead, set kwargs["wait_for_quota"] = True: it
sleeps quota_wait_seconds (default 3600) and resumes, at most quota_max_waits times
(default 3).
Nothing estimates a download’s size before it runs, and nothing refuses a large request.
Scope a request with the symbol list and the window. Symbols must match the ticker pattern
(uppercase letters and digits, at most seven characters, up to two . or - suffixes),
because each becomes both a path segment and a query value; anything else stops the run
before any request.
Inspecting what is on disk#
SourceInspector (quantlab.acquisition._support.inspector) answers questions from local
files. It builds no vendor client, so it needs no credentials and makes no network
request. Each method takes the config that locates the data.
from quantlab.acquisition._support.inspector import SourceInspector
inspector = SourceInspector()
inspector.coverage(acq_config) # what a download would skip or fetch
inspector.failures(acq_config) # {symbol: reason} across all runs
inspector.inventory(acq_config, ds_config) # shard, sidecar and store figures
inspector.browse_raw(ds_config, ["AAPL"], "2024-01-01", "2024-01-04") # polars LazyFrame
inspector.browse_zarr(ds_config, ["AAPL"], "2024-01-02", "2024-01-04") # xarray Dataset
After the example’s cancelled run, coverage reports that two of the six requested
symbols are still to fetch, and inventory summarises the raw tier:
{'requested': 6, 'pending': 2, 'skipped': 4, 'covered': 4, 'widened': 0, 'legacy': 0, 'no_data': 0}
root: <root>/downloads/us_equity/1d/nasdaq_data/tiingo
shards: 12
symbols_with_watermark: 4
coverage_start: 2024-01-01
coverage_last_date: 2024-03-29
failures: 0
browse_raw and browse_zarr require a symbol list and a date window, so what they
return is always narrow. browse_zarr raises for a symbol the store does not hold rather
than returning an empty column, which would look like a security with no history.
Converting to a panel#
convert(source, dataset_config) looks
up the capability for the dataset config’s market and frequency and runs the chunked
conversion of its dataset class. It reads only local files.
from quantlab.config import stock_kline_config
from quantlab.acquisition.base import DataSourceRegistry
from quantlab.acquisition.registry import convert
ds_config = stock_kline_config(start_date="2024-01-01", end_date="2024-03-29",
symbols=("AAPL", "AMD", "MSFT", "NVDA"))
result = convert(DataSourceRegistry.get("tiingo"), ds_config, granularity="month")
print(result.windows_written, result.windows_planned, result.rows_written)
windows written: 3/3, rows: 65, store: <root>/data/us_equity/1d/stock.zarr
dims: {'timestamp': 65, 'symbol': 4}
granularity and on_new_listing are described under
Chunked conversion. A capability with no dense panel
form, such as Alpaca’s tick quotes, is refused.
Adding a vendor is described in Extending quantlab.