quantlab.backend.zarr.XrBackend#
- class quantlab.backend.zarr.XrBackend[source]#
Bases:
DataBackendZarr-backed storage for an
xarray.Datasetpanel.readandwritemove the whole panel between memory and a Zarr directory.appendextends an existing store along one dimension (timestampby default) after checking that the incoming window cannot silently corrupt it.widen_symbol_axis,widen_data_varsandwiden_and_appendhandle the case where the panel written today has more symbols or more variables than the store already holds.Two class constants govern the on-disk layout:
APPEND_DIM_CHUNKis the chunk length pinned along the append dimension when a store is created, andMAX_WIDEN_BYTESis the largest panel a widen will hold in memory at once before switching to a block-by-block rewrite.Examples
>>> backend = XrBackend().to_internal(panel) >>> backend.write("prices.zarr") >>> backend.to_internal(next_month).widen_and_append("prices.zarr") >>> ds = XrBackend().read("prices.zarr").get_xarray_dataset( ... ["timestamp", "symbol"] ... )
- APPEND_DIM_CHUNK = 512#
Chunk length pinned along the append dimension when a store is created by
append,widen_symbol_axisorwiden_data_vars. Without an explicit encoding Zarr would adopt the first window’s own length as the chunk size, and later windows of a different length would be misaligned with the on-disk grid. The grid a store ends up with ismin(APPEND_DIM_CHUNK, extent), whereextentis the panel’s length along the append dimension or, when the caller states it withappend_dim_size, the store’s eventual total length. Zarr fixes the grid at creation; an append cannot revise it, and amode="w"rewrite re-pins it.
- MAX_WIDEN_BYTES = 4294967296#
Largest number of bytes a symbol-axis widen may materialise at once. At or under this budget
widen_symbol_axisreindexes the whole store in memory and writes it once; above it, the store is rewritten block by block along the append dimension. The value routes between the two strategies and never refuses a widen. It is a constant of this module rather than an import because the storage layer must not depend on the acquisition layer for a scalar.
- SYMBOL_DIM_CHUNK = 1024#
Chunk width pinned along the symbol axis when a store is created by
append,widen_and_appendor a widen rewrite. A store’s symbol axis grows at its end (extend_symbol_axis, ADR 0032), so a width taken from the first window would leave a store that started with a few symbols in thousands of narrow chunks. Stores created before the constant keep their own width; Zarr fixes it at creation.
- WIDENING_SUFFIX = '.widening.tmp'#
Sidecar suffixes used by
widen_symbol_axis’s atomic directory swap..widening.tmpholds the rewritten store before it becomes authoritative and is never read by anything..superseded.tmpbriefly holds the original store between the two renames and is the one artefact a crash can leave behind that still contains real data.
- append(path, append_dim='timestamp', *, append_dim_size=None, **kwargs)[source]#
Create the store at
path, or extend it alongappend_dim.When no store exists,
datais written as the first window with the chunk grid pinned by_append_encoding. Otherwise the window is appended after_assert_append_compatiblehas checked that it cannot silently corrupt the store. Rawto_zarr(mode="a")performs none of those checks: a changed coordinate overwrites the stored labels, a float NaN written into an integer variable becomes0, an overlapping window leaves the axis non-monotonic, and a changed variable set leaves the store unopenable. A gap between the stored end and the incoming start is allowed; overlap is refused and has no opt-out, becauseappendextends a store and does not recompute one. To replace a range the store already holds, rewrite the store withwrite.After the append the store’s 1-D index coordinates are rewritten in one chunk by
rechunk_index_coordinates: Zarr fixes a coordinate’s chunk grid at creation, so without it a store created from a one-bar window would keeptimestampin one chunk per bar.A panel that has grown a variable must go through
widen_data_varsorwiden_and_appendfirst; a panel missing a stored variable is refused outright, since the only fill would be NaN over the incoming dates of a variable that was complete.- Parameters:
- pathstr
Directory of the Zarr store.
- append_dimstr, default “timestamp”
The dimension the store grows along.
- append_dim_sizeint, optional
The store’s eventual total length along
append_dim, if the caller knows it. Only the creating write reads it; it pins the chunk grid to the value a single whole-range write would have chosen instead of the first window’s length. Ignored against an existing store, so an incremental writer may pass it on every window.- **kwargs
Passed through to
Dataset.to_zarr. Anencodingentry is dropped on the append path because xarray rejects it there.
- Raises:
- ValueError
If the window fails any compatibility check.
Examples
Two windows on the same symbol axis; the first call creates the store, the second extends it:
>>> XrBackend().to_internal(first_window).append("prices.zarr") XrBackend() >>> XrBackend().to_internal(next_window).append("prices.zarr") XrBackend() >>> xr.open_zarr("prices.zarr").sizes["timestamp"] 6
- create(path, fill_values=None, *, dim='symbol', append_dim='timestamp', **kwargs)[source]#
Write
dataas a new store atpath, laid out to grow.The
dimaxis is put insort_symbol_axisorder (sort_symbols) and the chunk grid and Zarr fill values are pinned asappendandwiden_and_appendpin them (_append_encoding):APPEND_DIM_CHUNKalongappend_dim,SYMBOL_DIM_CHUNKalongdim, and thefill_valuesentry as the fill of each variable ondim, so a symbol appended later (extend_symbol_axis) reads as it without being written. Any store already atpathis replaced, as bywrite.- Parameters:
- pathstr
Directory of the Zarr store.
- fill_valuesMapping[str, object], optional
Per-variable fill of the cells a later symbol extension adds.
- dimstr, default “symbol”
The symbol axis.
- append_dimstr, default “timestamp”
The dimension the store grows along.
- **kwargs
Passed through to
write; anencodinggiven here wins.
Examples
>>> import tempfile >>> from pathlib import Path >>> import numpy as np >>> import pandas as pd >>> import xarray as xr >>> import zarr >>> from quantlab.backend.zarr import XrBackend >>> panel = xr.Dataset( ... {"close": (("timestamp", "symbol"), np.ones((2, 2)))}, ... coords={"timestamp": pd.date_range("2024-01-02", periods=2), ... "symbol": np.array(["BBB", "AAA"], dtype=object)}, ... ) >>> path = str(Path(tempfile.mkdtemp()) / "prices.zarr") >>> _ = XrBackend().to_internal(panel).create(path) >>> xr.open_zarr(path)["symbol"].values.tolist() ['AAA', 'BBB'] >>> zarr.open_group(path)["close"].chunks (2, 1024)
- extend_symbol_axis(path, symbols, dim='symbol', fill_values=None)[source]#
Append
symbolsat the end of the store’sdimaxis, in place.Each Zarr array carrying
dimis resized; no stored chunk is rewritten, so the cost is a few metadata writes whatever the store’s size. The new cells read as each array’s fill value: NaN for a floating-point variable, thefill_valuesentry otherwise, an empty string for a text coordinate ondim. A variable whose Zarr fill value is not that (a store created before_append_encodingpinned it, a datetime variable) has its new cells written, which rewrites at most the chunks the old axis ends in. The coordinate is then swapped for one holding the old labels followed bysymbols, in one chunk, and the metadata consolidated; arrays withoutdimare untouched.datais not touched.Labels the store already holds are skipped; the others keep the order given (
widen_and_appendsorts them among themselves). They are re-spelled in the stored axis’s dtype first, so a ticker handed to an integer axis raises. A fixed-width text axis is stored at the width of its longest label.The swap of the coordinate is the commit: a process killed before it leaves arrays longer than the coordinate, which the next writing call cuts back (
_recover_axis_extension), so the store is as it was before. Do not run this while another process reads the store.- Parameters:
- pathstr
Directory of the Zarr store.
- symbolsSequence
The labels to append.
- dimstr, default “symbol”
The axis to extend.
- fill_valuesMapping[str, object], optional
Per-variable fill of the new cells. A variable on
dimthat is not floating-point needs one, as inwiden_symbol_axis.
- Raises:
- FileNotFoundError
If no store exists at
path.- ValueError
If a non-floating variable on
dimhas no fill value.
Examples
>>> import tempfile >>> from pathlib import Path >>> import numpy as np >>> import pandas as pd >>> import xarray as xr >>> from quantlab.backend.zarr import XrBackend >>> panel = xr.Dataset( ... {"close": (("timestamp", "symbol"), np.ones((2, 2))), ... "volume": (("timestamp", "symbol"), np.ones((2, 2)))}, ... coords={"timestamp": pd.date_range("2024-01-02", periods=2), ... "symbol": np.array(["BBB", "DDD"], dtype=object)}, ... ) >>> path = str(Path(tempfile.mkdtemp()) / "prices.zarr") >>> _ = XrBackend().to_internal(panel).append(path) >>> _ = XrBackend().extend_symbol_axis( ... path, ["AAA"], fill_values={"volume": 0.0} ... ) >>> stored = xr.open_zarr(path) >>> stored["symbol"].values.tolist() ['BBB', 'DDD', 'AAA'] >>> stored["close"].sel(symbol="AAA").values.tolist() [nan, nan] >>> stored["volume"].sel(symbol="AAA").values.tolist() [0.0, 0.0]
- filter_by_date(col, start_date, end_date)[source]#
Narrow
datain place to the label slicestart_date..end_date.- Parameters:
- colstr
The date dimension to slice.
- start_datestr
First label to keep, inclusive.
- end_datestr
Last label to keep, inclusive.
Examples
>>> backend.filter_by_date("timestamp", "2024-01-02", "2024-01-03") XrBackend() >>> backend.data["timestamp"].values.astype("datetime64[D]") array(['2024-01-02', '2024-01-03'], dtype='datetime64[D]')
- filter_by_symbol(col, symbols)[source]#
Narrow
datain place to the given labels oncol.- Parameters:
- colstr
The dimension to select on.
- symbolstuple[str, …]
The labels to keep.
Examples
>>> backend.filter_by_symbol("symbol", ("BBB",)) XrBackend() >>> backend.data["symbol"].values.tolist() ['BBB']
- get_lazyframe()[source]#
Return
dataas a long-formatpolars.LazyFrame.Examples
>>> backend.get_lazyframe().collect().columns ['timestamp', 'symbol', 'close']
- get_xarray_dataset(indexes=None)[source]#
Return
dataindexed by exactly the dimensions inindexes.indexesnames the result’s dimensions, in order: data variables laid out on any other dimension are dropped, dimensions no longer used are dropped with their coordinates, and the survivors are transposed onto the requested order. This is the same meaningPlBackend.get_xarray_datasetgives the argument.Nonereturnsdataitself, not a copy, since callers rely on receiving the object the backend holds.datais never modified here; thefilter_by_*methods are the in-place ones.- Parameters:
- indexeslist[str], optional
The dimensions to index by, or
Nonefor no shape request.
- Raises:
- ValueError
If a requested name is not a dimension of
data.
Examples
>>> ds = backend.get_xarray_dataset(["timestamp", "symbol"]) >>> tuple(ds.dims) ('timestamp', 'symbol') >>> backend.get_xarray_dataset(["symbol", "timestamp"])["close"].dims ('symbol', 'timestamp') >>> backend.get_xarray_dataset() is backend.data True
- head(path, n)[source]#
Return at most
nrows of the store atpathas a lazy frame.- Parameters:
- pathstr
Directory of the Zarr store.
- nint
Maximum number of rows (and of labels per dimension) to read.
- The store is opened by path with the same opener ``read`` uses, and
- every dimension is sliced to ``n`` before conversion, so the whole
- store is never materialised. ``data`` is neither read nor written:
- a probe must not observe or inherit the in-place narrowing that
- ``filter_by_date`` leaves behind.
- Raises:
- FileNotFoundError
If
pathdoes not exist.
Examples
>>> XrBackend().head("prices.zarr", 2).collect().shape (2, 3)
- read(path, **kwargs)[source]#
Open the Zarr store at
pathlazily intodata.Every call opens the store again and replaces whatever
dataheld; nothing is cached across calls. Zarr reads a string dimension coordinate (such assymbol) back as numpy’s variable-widthStringDType; it is handed back as object strings, the form a panel built from a frame has, so labels from a store and from memory compare, align and cast tostralike.- Parameters:
- pathstr or os.PathLike
Directory of the Zarr store.
- **kwargs
Passed through to
xarray.open_dataset.
- Raises:
- FileNotFoundError
If
pathdoes not exist.
Examples
>>> import tempfile >>> from pathlib import Path >>> import numpy as np >>> import pandas as pd >>> import xarray as xr >>> from quantlab.backend.zarr import XrBackend >>> panel = xr.Dataset( ... {"close": (("timestamp", "symbol"), np.ones((4, 2)))}, ... coords={"timestamp": pd.date_range("2024-01-02", periods=4), ... "symbol": np.array(["AAA", "BBB"], dtype=object)}, ... ) >>> path = Path(tempfile.mkdtemp()) / "prices.zarr" >>> _ = XrBackend().to_internal(panel).write(str(path)) >>> backend = XrBackend().read(path) >>> dict(backend.data.sizes) {'timestamp': 4, 'symbol': 2} >>> backend.data["symbol"].dtype dtype('O')
- resample(labels, how)[source]#
Aggregate
dataonto the barslabelsassigns, in place.Every variable with a
timestampdimension is grouped by its label and reduced with its method fromhow; the other dimensions are kept as they are. A variable without atimestampdimension is carried over unchanged.countyields integers; the other methods keep pandas’ skip-NaN semantics, so an all-NaN bar is NaN underfirst,last,max,minandmeanand 0 undersum.- Parameters:
- labelspd.Series
Source timestamp to target timestamp; see
DataBackend.- howdict[str, str]
Variable name to aggregation method, one entry per variable.
- Raises:
- ValueError
If a held timestamp has no label, or a variable has no method.
Examples
>>> labels = pd.Series(minutes.floor("D"), index=minutes) >>> backend.resample(labels, {"close": "last", "volume": "sum"}) >>> backend.data["timestamp"].values.astype("datetime64[D]").tolist() [datetime.date(2024, 1, 2), datetime.date(2024, 1, 3)]
- shorten_axis(path, last, dim='timestamp')[source]#
Drop the labels of the store’s
dimaxis afterlast, in place.A write killed midway is settled first (
_recover_interrupted_writes), and the axis is then read without the consolidated metadata, so a shortening killed midway is finished by running it again; with no label afterlastnothing is written. The index coordinate ofdimis cut first, then every other array carryingdim, and the metadata is consolidated again. No stored chunk is rewritten: the cut cells stay in the axis’s last chunk until the axis grows over them, and an append writes every cell it adds. Cutting the coordinate is the commit: a process killed after it leaves arrays longer than their axis, which the next writing call cuts back. The other axes are untouched. Do not run this while another process reads the store.- Parameters:
- pathstr
Directory of the Zarr store.
- lastlabel
The last label of
dimkept (apd.Timestampon the time axis); the axis is ordered.- dimstr, default “timestamp”
The axis to shorten.
- Raises:
- FileNotFoundError
If no store exists at
path.
Examples
>>> import tempfile >>> from pathlib import Path >>> import numpy as np >>> import pandas as pd >>> import xarray as xr >>> from quantlab.backend.zarr import XrBackend >>> panel = xr.Dataset( ... {"close": (("timestamp", "symbol"), np.arange(6.0).reshape(3, 2))}, ... coords={"timestamp": pd.date_range("2024-01-02", periods=3), ... "symbol": np.array(["AAA", "BBB"], dtype=object)}, ... ) >>> path = str(Path(tempfile.mkdtemp()) / "prices.zarr") >>> _ = XrBackend().to_internal(panel).append(path) >>> _ = XrBackend().shorten_axis(path, pd.Timestamp("2024-01-03")) >>> xr.open_zarr(path)["close"].values.tolist() [[0.0, 1.0], [2.0, 3.0]]
- sort_symbols(dim='symbol')[source]#
Put
data’sdimaxis insort_symbol_axisorder.Every store keeps its symbol axis in that order, the one
widen_and_appendwrites, so a store has one layout whether it was written whole or appended to. Labels and values move together; a panel withoutdimis left as it is.- Parameters:
- dimstr, default “symbol”
The axis to order.
- Returns:
- Self
self, for chaining.
Examples
>>> backend = XrBackend().to_internal(panel).sort_symbols() >>> backend.data["symbol"].values.tolist() ['AAA', 'BBB', 'CCC']
- to_internal(data)[source]#
Adopt an in-memory
xarray.Datasetasdata.- Parameters:
- dataxr.Dataset
The panel to hold.
Examples
>>> backend = XrBackend().to_internal(panel) >>> backend.data is panel True
- widen_and_append(path, append_dim='timestamp', dim='symbol', fill_values=None, **kwargs)[source]#
Reconcile every axis with the store at
path, thenappend.The opt-in sibling of
appendfor a panel that has grown. In order: the incoming labels the store lacks are appended at the end of itsdimaxis, sorted among themselves (extend_symbol_axis, which rewrites no stored chunk), anddatais reindexed onto the stored axis followed by them; then any variable the store lacks is added withwiden_data_vars; then the unchangedappendruns. A window carrying the store’s labels in another order is only reindexed. Variables are widened after the symbol axis so the filler is built once at the final width. Calling this unconditionally is cheap: when both axes already agree it delegates straight toappend, and when no store exists yet it puts thedimaxis in sorted order (sort_symbols) and creates the store with its chunk grid and the Zarr fill values offill_valuespinned (_append_encoding).The closing
appendis what keeps the compatibility checks in force. Because the widens commit before it runs, a window it then refuses (an overlappingappend_dimrange, say) can leave the store with the grown symbol axis and variable set while its append dimension and stored history are untouched. That is the accepted cost of inheriting the refusal rather than duplicating it.append_dim_sizemay ride inkwargs. It is read, not consumed, so the same value reaches the symbol widen, the variable widen and the closingappend; those three cannot disagree on the chunk grid for what this call writes. Variables the store held from an earlier write keep whatever grid they were created with.- Parameters:
- pathstr
Directory of the Zarr store.
- append_dimstr, default “timestamp”
The dimension the store grows along.
- dimstr, default “symbol”
The symbol axis to reconcile.
- fill_valuesMapping[str, object], optional
Per-variable fill of the cells a new symbol or a new variable adds (NaN when absent), passed to both widens, pinned as the Zarr fill value of a new store and used to reindex
data.- **kwargs
Passed through to
append.
Examples
A store holding
AAAandCCC, a window carryingDDD,BBBand a variable the store has not seen:>>> XrBackend().to_internal(window).widen_and_append( ... "prices.zarr", fill_values={"flag": False} ... ) XrBackend() >>> stored = xr.open_zarr("prices.zarr") >>> stored["symbol"].values.tolist(), sorted(stored.data_vars) (['AAA', 'CCC', 'BBB', 'DDD'], ['amount', 'close', 'flag', 'volume'])
- widen_data_vars(path, variables, append_dim='timestamp', fill_values=None, *, append_dim_size=None)[source]#
Add data variables to the store at
path, backfilled over its extent.The variable-set counterpart of
widen_symbol_axis. Each name invariablesthat the store does not yet hold is materialised across the store’s whole existing extent with the fill value (NaN for a floating-point dtype, thefill_valuesentry otherwise). Names the store already holds are left alone, and when nothing is new the method returns without writing. This is the explicit opt-in for a panel that grew a column; a plainappendstill refuses a changed variable set because Zarr would otherwise write the new variable over the incoming window only and leave the store unopenable.The filler carries dimensions but no coordinates: the store already holds them, and writing a decoded coordinate back can land on a different dtype than the one Zarr recorded. No directory swap is needed because a failed partial write raises and leaves the store intact.
- Parameters:
- pathstr
Directory of the Zarr store.
- variablesMapping[str, object]
Maps each variable name to its dtype. The dtype is all the filler needs, so the caller’s panel is never held.
- append_dimstr, default “timestamp”
The store’s append dimension, used to pin the filler’s chunk grid to the store’s.
- fill_valuesMapping[str, object], optional
Per-variable fill for new variables that are not floating-point. Without an entry such a variable is refused, because
np.fullwith NaN yields0for integers andTruefor booleans, fabricating history.- append_dim_sizeint, optional
The store’s eventual length along
append_dim, so a filler added to a store that has not reached its final extent joins on the same grid as the variables already there.
- Raises:
- FileNotFoundError
If no store exists at
path.- ValueError
If a new non-floating variable has no fill value.
Examples
>>> XrBackend().widen_data_vars("prices.zarr", {"volume": "float64"}) XrBackend() >>> XrBackend().widen_data_vars( ... "prices.zarr", {"flag": "bool"}, fill_values={"flag": False} ... ) XrBackend() >>> stored = xr.open_zarr("prices.zarr") >>> sorted(stored.data_vars), stored["flag"].dtype (['close', 'flag', 'volume'], dtype('bool'))
- widen_symbol_axis(path, symbols, dim='symbol', append_dim='timestamp', fill_values=None, *, append_dim_size=None)[source]#
Rewrite the store at
pathonto a supersetdimaxis.Every label already in the store keeps its history unchanged; each new label is filled across the whole stored extent (NaN for a floating-point variable, the value named in
fill_valuesotherwise).datais not touched, so a caller can widen a store without holding a panel. This is the explicit opt-in for a roster that grew between runs; a plainappendstill refuses a changed coordinate.The rewrite lands in a
.widening.tmpsidecar and is then swapped in with two same-parent renames (store to.superseded.tmp, sidecar to store) followed by removal of the superseded copy. A crash between the renames leaves no store atpathand a superseded sidecar holding the only copy; the next call refuses and names the manual move rather than guessing which directory is authoritative. A non-empty superseded sidecar beside a live store is also refused, because it may be a real store left by an interrupted rebuild. Empty or never-authoritative residue is removed and the widen proceeds.The strategy is chosen by size: at or under
MAX_WIDEN_BYTESthe whole store is reindexed in memory and written once; above it the store is rewritten block by block alongappend_dimand a warning names the figures involved. Both leave an identical store.symbolsis re-spelled in the stored axis’s dtype before anything is compared, so a ticker handed to an integer axis raises fromnormalize_to_axis_dtypeinstead of matching nothing and writing an all-NaN store.Widening backfills NaN over a new label’s history; it does not recover data a vendor may have had. Rebuilding from raw data is the route when that history matters.
- Parameters:
- pathstr
Directory of the Zarr store.
- symbolsSequence[str]
The labels the rewritten
dimaxis must contain. Must be a superset of the stored axis.- dimstr, default “symbol”
The axis being widened.
- append_dimstr, default “timestamp”
The store’s append dimension, used to size blocks and to pin the rewritten chunk grid.
- fill_valuesMapping[str, object], optional
Per-variable fill for variables carrying
dimthat are not floating-point. Without an entry such a variable is refused, because an unfilled reindex would silently upcast it to float64.- append_dim_sizeint, optional
The store’s eventual length along
append_dim. This rewrite re-pins the chunk grid, so a caller widening a store that has not yet reached its final extent states the extent here;Nonesizes the grid from the store as it is now.
- Raises:
- FileNotFoundError
If no store exists at
path.- ValueError
If crash residue makes the store’s identity ambiguous, if
symbolswould drop a stored label, or if a non-floating variable has no fill value.
Examples
>>> XrBackend().widen_symbol_axis("prices.zarr", ["AAA", "BBB", "CCC"]) XrBackend() >>> stored = xr.open_zarr("prices.zarr") >>> stored["symbol"].values.tolist() ['AAA', 'BBB', 'CCC'] >>> stored["close"].sel(symbol="CCC").values # backfilled history array([nan, nan, nan, nan, nan, nan])
- write(path, **kwargs)[source]#
Write
datatopath, replacing any store already there.The parent directory is created if needed and
modedefaults to"w". No chunk encoding is applied, so the store lands on Zarr’s default chunk grid.- Parameters:
- pathstr
Directory of the Zarr store.
- **kwargs
Passed through to
Dataset.to_zarr.
Examples
>>> XrBackend().to_internal(panel).write("prices.zarr") XrBackend() >>> Path("prices.zarr").is_dir() True