quantlab.factor.base.Factor#
- class quantlab.factor.base.Factor(config)[source]#
-
Backend-agnostic base class for factors and labels.
A
Factorholds a config whosedatasetattribute is the dataset it reads from. Asked for a date range, it answerscompute(start, end)from its inputs orread(start, end)from its own store, whichbuild(start, end)writes andextend(end)lengthens. None of these holds a panel or changes a config, the factor’s or its dataset’s, so one dataset object can feed several factors. The returned panel is what the model layer consumes as features; to use a factor as a label, wrap it inquantlab.label.forward.Forward. Subclasses implement_get_factor_namesand_compute_panel(both backends here do).Assigning
configruns the property setter, which setsnameand resolvesfactor_nameswhen they are not pinned, so factor names are known as soon as the object is constructed, before anything is computed.- Parameters:
- configBaseFactorConfig
The factor config. Its
datasetfield is the dataset it reads from, or a list of datasets, which the factor merges into oneMergedDataset(seequantlab.dataset.merged).
- Attributes:
- data_backendXrBackend
Holds the last bar a stream-mode factor computed; empty otherwise.
Examples
>>> factor = MyFactor(config) >>> factor.get_factor_names() # known before anything is computed >>> panel = factor.compute("2024-02-01", "2024-02-29") >>> factor.build("2024-01-01", "2024-06-30") >>> panel = factor.read("2024-02-01", "2024-02-29")
- PEAK_COPIES = 4#
How many times its own 8 bytes a variable takes at the peak of a computation: the copies, intermediate results and buffers made on the way. A rough, conservative figure; see
cell_bytes.
- analyze(start, end, factor_names=None, frets=None, output_dir=None, quantiles=5, data_strategy='cal', plot=True)[source]#
Report how well this factor predicts forward returns, alphalens style.
Every analyzed factor variable is paired with every variable of every forward-return label (fret). For each pair the factor and fret panels are joined on their common timestamps and symbols, then
quantlab.analysis.factor_report.FactorAnalyzercomputes the information analysis (per-period Spearman IC, its mean, std, IR, t-statistic, p-value, skew, kurtosis and monthly means), the returns analysis (mean forward return per factor quantile, top-minus-bottom spread, cumulative returns) and the turnover analysis (quantile turnover, lag-1 factor rank autocorrelation), and draws one composite matplotlib figure unlessplotis False.The factor and every fret are asked for their panels from
starttoend: computed from their inputs withcompute(start, end)underdata_strategy="cal", read from their stores withread(start, end)under"read".- Parameters:
- start, endstr, datetime.date or pd.Timestamp
The range to analyze, both inclusive.
- factor_nameslist of str, optional
Variables of this factor to analyze. All of
get_factor_names()when None.- fretslist of Forward
Forward-return labels, for example
quantlab.label.predefined.fret.Returnor any factor wrapped inquantlab.label.forward.Forward; each one’s panel gives the forward returns and itsspan_bars()the horizon. Required.- output_dirstr, optional
When given, the directory is created and
summary.json,summary.csv,ic.csv,monthly_ic.csv,quantile_returns.csv,turnover.csv, one<factor>__<fret>.pngper pair andconfig.jsonare written there, plusfactor_correlation.csv,factor_correlation_pairs.csv,factor_clusters.csvandfactor_correlation.pngwhen two or more factor variables are analyzed.config.jsonholds{"factor": ..., "frets": [...]}, each rebuildable withrebuild. Withplot=Falseno PNG is written. When None, nothing is written.- quantilesint, default 5
Number of equal-count factor buckets per timestamp.
- data_strategy{“cal”, “read”}, default “cal”
Whether the panels are computed or read from the stores
buildwrote, for the factor and every fret alike.- plotbool, default True
Draw the figures: to PNG files in
output_dir, else intofigures. False draws none, which is most of the time a report over many factors takes.
- Returns:
- FactorAnalysis
pairs(metrics per"<factor>__<fret>"),figures(matplotlib figures, same keys), the headline metrics per pair throughsummary()and tidy tables throughsummary_table(),ic_table(),quantile_returns_table(),turnover_table()andmonthly_ic_table(); with two or more factor variables alsocorrelation, theirFactorCorrelation.
- Raises:
- ValueError
If
fretsis empty,data_strategyis neither"cal"nor"read", a factor name is unknown, a fret’s most common bar spacing differs from the factor’s, or the panels share no cells.
Examples
factoris aMomentum(momentum_5) over eight symbols, andfwdaquantlab.label.predefined.fret.Returnwithn_forward_periods=1over the same symbols and dates, itsadjOpen0.99 times the momentum’sClose:>>> result = factor.analyze( ... "2024-02-01", "2024-02-29", frets=[fwd], quantiles=4, ... output_dir="data/analysis/momentum", ... ) >>> list(result.pairs) ['momentum_5__ret_1'] >>> cols = ["factor", "fret", "ic_mean", "ic_t_stat", "mean_spread"] >>> result.summary_table()[cols].round(4) factor fret ic_mean ic_t_stat mean_spread 0 momentum_5 ret_1 -0.0279 -0.4301 -0.0014 >>> sorted(os.listdir("data/analysis/momentum")) ['config.json', 'ic.csv', 'momentum_5__ret_1.png', 'monthly_ic.csv', 'quantile_returns.csv', 'summary.csv', 'summary.json', 'turnover.csv'] >>> result = factor.analyze( ... "2024-02-01", "2024-02-29", frets=[fwd], quantiles=4, ... output_dir="data/analysis/momentum_tables", plot=False, ... ) >>> result.figures, sorted(os.listdir("data/analysis/momentum_tables")) ({}, ['config.json', 'ic.csv', 'monthly_ic.csv', 'quantile_returns.csv', 'summary.csv', 'summary.json', 'turnover.csv'])
- append_rows(rows, end)[source]#
Append
rows, the bars after the recorded range up toend, and move its end.The write half of
extend, for rows computed elsewhere; the axes widen to fit as inextend, and emptyrows(no bar in the range) only move the recorded end.- Parameters:
- rowsxr.Dataset
The factor’s outputs on the bars after the recorded end, up to
end.- endstr, datetime.date or pd.Timestamp
The new end of the store’s range.
- Returns:
- Self
self, for chaining.
- Raises:
- ValueError
As
extendable_range.
Examples
>>> factor.append_rows(factor.compute("2024-02-01", "2024-02-29"), "2024-02-29").store_range() ('2024-01-01', '2024-02-29')
- bar_after(date, n)[source]#
Return the bar
nbars afterdateon the factor calendar, or the last bar there is.The forward counterpart of
bar_before, withBaseDataset.bar_after’s rules: the bars afterdateare those stamped strictly later,n=1is the first of them,n=0returnsdateitself, and when fewer thannbars follow the calendar’s last bar is returned,dateitself when none follow, so the result is never beforedate. A resampled factor counts its resampled bars. Only timestamps are read.- Parameters:
- datestr, datetime.date or pd.Timestamp
The date to count forward from.
- nint
How many bars to count forward; non-negative.
- Returns:
- pd.Timestamp
The timestamp of that bar.
- Raises:
- ValueError
If
nis negative.
Examples
>>> daily = minute.resample("1d", "last") # five business days from 2024-01-02 >>> daily.bar_after("2024-01-05", 1) # Friday: the bar after is Monday Timestamp('2024-01-08 00:00:00') >>> daily.bar_after("2024-01-04", 10) # the bars end on 2024-01-08 Timestamp('2024-01-08 00:00:00')
- bar_before(date, n)[source]#
Return the bar
nbars beforedateon the factor calendar.The bars before
dateare those stamped strictly earlier;n=1is the last of them andn=0returnsdateitself, asBaseDataset.bar_beforedoes. A factor that is not resampled answers with its dataset’s; a resampled factor counts its resampled bars (calendar), so a daily factor computed from minute bars moves by trading days. Only timestamps are read.- Parameters:
- datestr, datetime.date or pd.Timestamp
The date to count back from.
- nint
How many bars to count back; non-negative.
- Returns:
- pd.Timestamp
The timestamp of that bar.
- Raises:
- ValueError
If
nis negative.- InsufficientHistoryError
If fewer than
nbars exist beforedate; a subclass ofValueErrorcarryingavailableandrequested, both in the factor’s bars.
Examples
>>> daily = minute.resample("1d", "last") # five business days from 2024-01-02 >>> daily.bar_before("2024-01-08", 1) # Monday: the bar before is Friday Timestamp('2024-01-05 00:00:00') >>> daily.bar_before("2024-01-03", 3) Traceback (most recent call last): quantlab.dataset.base.InsufficientHistoryError: Momentum.bar_before(): only 1 bar(s) exist before '2024-01-03' in the panel held in memory resampled to '1d', but 3 were requested.
- build(start, end)[source]#
Compute
starttoendand write it as the factor store.The store at
store_pathis replaced bycompute(start, end), its symbol axis insort_symbol_axisorder asextendkeeps it, and the range is recorded beside it, in<store_path>.range.json, forstore_range,readandextend. The factor holds nothing afterwards and no config changes. Only the store’s owner builds it (seeowns_store). The store and its range are written throughquantlab.backend.ranged.RangedStore, as a factor risk model’s stores are.- Parameters:
- start, endstr, datetime.date or pd.Timestamp
The range the store covers.
- Returns:
- Self
self, for chaining.
- Raises:
- ValueError
If the factor has no store path or does not own its store.
Examples
>>> factor.build("2024-01-01", "2024-03-31").store_range() ('2024-01-01', '2024-03-31')
- calendar(start, end)[source]#
Return the factor’s bars from
starttoend, both inclusive, timestamps only.The factor calendar: the bars of the factor’s output panel, which
bar_beforeandbar_aftercount on (ADR 0051). A factor that is not resampled answers with its dataset’s calendar (BaseDataset.calendar). A factor made withresampleanswers with its resampled bars: the dataset’s bars over the range, bucketed the way the dataset cuts them (BaseDataset.resample_labels) and labelled as the resampled panel labels them. Only timestamps are read, so aDataRecorderrecords nothing.- Parameters:
- start, endstr, datetime.date or pd.Timestamp
The range. A date-only
endincludes every bar of that day.
- Returns:
- pd.DatetimeIndex
The sorted bar timestamps in the range.
- Raises:
- ValueError
If
startis afterend.
Examples
A momentum on one-minute bars, and the same factor resampled to daily (five business days of four minute bars each):
>>> minute.calendar("2024-01-04", "2024-01-05") DatetimeIndex(['2024-01-04 09:30:00', '2024-01-04 09:31:00', '2024-01-04 09:32:00', '2024-01-04 09:33:00', '2024-01-05 09:30:00', '2024-01-05 09:31:00', '2024-01-05 09:32:00', '2024-01-05 09:33:00'], dtype='datetime64[ns]', freq=None) >>> daily = minute.resample("1d", "last") >>> daily.calendar("2024-01-04", "2024-01-08") # Thursday to Monday DatetimeIndex(['2024-01-04', '2024-01-05', '2024-01-08'], dtype='datetime64[ns]', freq=None)
- cell_bytes()[source]#
Return the bytes one bar of one symbol takes: at peak, and as output.
The output is 8 bytes per factor output. The peak counts every input variable the factor reads (
_input_variables; as many as the outputs when it reads all of them) and every output at 8 bytes,PEAK_COPIEStimes over. A chunked build (quantlab.factor.chunking) sizes its chunks with it.- Returns:
- tuple[int, int]
(peak, output)bytes per bar and symbol.
Examples
>>> factor.cell_bytes() # one input column, one output (64, 8)
- property class_name#
Bare class name, used in log and error messages.
Examples
>>> factor.class_name 'Momentum'
- compute(start, end)[source]#
Compute the factor from
starttoend, both inclusive.The inputs are requested from the dataset from
warmup_barsbars beforestartup toend, so rolling operators are warm on the first requested bar; the result holds only the requested range. A resampled factor is computed on its dataset’s own bars and returned on its resampled bars. Neither this factor’s config nor its dataset’s config changes, and the factor holds nothing afterwards.If the dataset holds fewer than
warmup_barsbars beforestart, aUserWarningstates the shortfall in bars and the computation starts from the first bar there is.- Parameters:
- start, endstr, datetime.date or pd.Timestamp
The range to return. A date-only
endincludes every bar of that day.
- Returns:
- xr.Dataset
The factor panel on
(timestamp, symbol), in memory.
- Raises:
- ValueError
If
startis afterend.
Examples
>>> panel = factor.compute("2024-02-01", "2024-02-10") # warmup_bars=20 >>> dict(panel.sizes) {'timestamp': 10, 'symbol': 8}
With only two bars before
start, the call warns:UserWarning: Momentum.compute(): 20 warm-up bar(s) are needed before '2024-01-03' but the dataset holds only 2; the first bars are short by 18 bar(s) of warm-up.
- property config#
The factor’s normalised config:
nameandfactor_namesfilled in.Examples
>>> factor.config is config False >>> factor.config.name, config.name ('quantlab.factor.predefined.momentum.Momentum', None)
- copy()[source]#
Return a copy with its own config, components and empty backend.
Every component the config holds (its
dataset, a wrappedfactor, a roster, a benchmark, each dataset of a many-field) is copied with its owncopy(), and the rest of the config is deep-copied, so the copy shares no mutable state with this factor, not even akwargsdict.Examples
>>> other = factor.copy() >>> other == factor, other.config.dataset is factor.config.dataset (True, False)
- extend(end)[source]#
Append the bars after the recorded range, up to
end.The bars are computed with
compute, so they are warmed from the dataset’s history, then appended to the store; its timestamp, symbol and variable axes widen to fit, and cells created by widening are filled from_widen_fill_values(). The recorded range then ends atend.A resampled factor appends resampled bars.
computereads every source bar a resampled bar covers, past the range’s ends too, so each stored bar is whole and a store built in pieces equals one built at once: a minute factor’s daily store can be built a quarter at a time.- Parameters:
- endstr, datetime.date or pd.Timestamp
The new end of the store’s range.
- Returns:
- Self
self, for chaining.
- Raises:
- ValueError
If the factor has no store path, does not own its store (see
owns_store), the store has no recorded range, or the recorded range already reachesend.
Examples
>>> factor.build("2024-01-01", "2024-03-31").store_range() ('2024-01-01', '2024-03-31') >>> factor.extend("2024-04-30").store_range() ('2024-01-01', '2024-04-30')
- extendable_range(end)[source]#
Return the recorded range, refusing an
extendtoend.extend’s refusals, before anything is computed: a caller that computes the rows elsewhere asks this first, then hands them toappend_rows.- Parameters:
- endstr, datetime.date or pd.Timestamp
The end an
extendwould reach.
- Returns:
- tuple of str
The recorded
(start, end).
- Raises:
- ValueError
If the factor has no store path, does not own its store, the store has no recorded range, or the recorded range already reaches
end.
Examples
>>> factor.extendable_range("2024-03-31") ('2024-01-01', '2024-02-29') >>> factor.extendable_range("2024-02-10") Traceback (most recent call last): ValueError: Momentum.extend(): the store at ... already covers 2024-01-01 to 2024-02-29; ...
- get_factor_names()[source]#
Return the names of the columns this factor produces.
Examples
>>> factor.get_factor_names() ('momentum_20',)
- keep_compiled()[source]#
Keep whatever the factor compiles across
computecalls inside the block.Does nothing here;
FactorKunQuantkeeps its compiled library, which it otherwise looks up again on every call (compile_graphscompiles a graph only once). A computation split into many calls (quantlab.factor.chunking) runs inside it.Examples
>>> with factor.keep_compiled(): ... january = factor.compute("2024-01-01", "2024-01-31") ... february = factor.compute("2024-02-01", "2024-02-29")
- property num_factors#
Number of columns this factor produces.
Examples
>>> factor.num_factors 1
- output_symbols()[source]#
Return the symbols
computeoutputs, without computing anything.The symbol axis of
config.dataset’s store here. A factor whose dataset is not where its symbols come from (a wrapper reading its exposures, or keeping a roster) overrides it.quantlab.factor.chunkingsizes its chunks on it.Examples
>>> factor.output_symbols()[:2] ['S0USDT', 'S1USDT']
- owns_store()[source]#
Whether the factor may write its store: it holds exactly its outputs.
A store is written only by its owner, the factor whose outputs are every variable of it; a factor pinned to some of them reads it but never writes it (ADR 0029). A store that does not exist yet belongs to whoever writes it first.
Examples
>>> owner.owns_store(), owner_pinned_to_one_output.owns_store() (True, False)
- read(start, end, symbols=None)[source]#
Return the factor store from
starttoend, both inclusive.The stored panel is opened lazily; the factor holds nothing afterwards. The range must lie inside the one recorded beside the store by
buildandextend(seestore_range). A resampled factor reads its own store when one has been built, otherwise it resamples on read as a resampled dataset’spaneldoes (quantlab.backend.resample.resample_on_read): the part of the source factor’s store the range needs, cut tosymbolsfirst, is aggregated, and those source bars are what is recorded.Only the factor’s own outputs (
get_factor_names()) are returned, so a factor pinned to some outputs of a store shared with a factor of more outputs reads just those (ADR 0029).symbolsis a pure query, likeBaseDataset.panel’s; a model reads a store on a roster throughquantlab.factor.predefined.roster.RosterFactor.This is the factor read seam: inside an open
quantlab.runs.record.DataRecorderthe request, its symbols included, is logged and fingerprinted, over every returned variable, when the recorder closes (over the source bars, keyed by the source store when the factor is not mapped, for a resample on read).- Parameters:
- start, endstr, datetime.date or pd.Timestamp
The range to return. A date-only
endincludes every bar of that day.- symbolssequence, optional
Symbol labels to keep, in the order given, of the store’s own type.
Nonekeeps every symbol of the store.
- Returns:
- xr.Dataset
The panel on
(timestamp, symbol).
- Raises:
- ValueError
If
startis afterend, the store has no recorded range, the recorded range does not contain the requested one, or the store lacks one of the factor’s outputs or ofsymbols.
Examples
>>> factor.build("2024-01-01", "2024-03-31").store_range() ('2024-01-01', '2024-03-31') >>> dict(factor.read("2024-02-01", "2024-02-10").sizes) {'timestamp': 10, 'symbol': 8} >>> panel = factor.read("2024-02-01", "2024-02-10", symbols=["S3USDT", "S1USDT"]) >>> panel["symbol"].values.tolist() ['S3USDT', 'S1USDT'] >>> factor.read("2024-03-20", "2024-04-10") Traceback (most recent call last): ValueError: Momentum.read(): the store at ... covers 2024-01-01 to 2024-03-31, ...
- refuse_unless_owner(method)[source]#
Raise unless the factor owns its store (see
owns_store): the guard of every write.build,extend,write_rows,append_rowsandextendable_rangerun it; a caller that computes rows elsewhere runs it before computing anything. A view overrides it.- Parameters:
- methodstr
The write being refused, named in the message (
"build"or"extend").
- Raises:
- ValueError
If the store at
store_pathexists and its variables are not exactlyget_factor_names().
Examples
>>> owner.refuse_unless_owner("build") # the owner passes >>> owner_pinned_to_one_output.refuse_unless_owner("build") Traceback (most recent call last): ValueError: Momentum.build(): the store at ... holds [...], not just this factor's outputs [...]; ...
- resample(freq, how)[source]#
Return a copy of this factor whose panel is resampled onto
freq.The factor is still computed on its dataset’s own bars; only the computed panel is aggregated, so a minute-bar factor becomes a daily one without changing what it measures. The copy’s config carries
resample_freq=freqandresample_how=how;compute,readandbuildon it answer on the resampled bars. This factor and its dataset are not changed.A resampled factor cannot stream.
build()andextend()write tostore_path, the resampled store beside the source. The bars are cut the way the dataset cuts them (seeBaseDataset.resample_labels).- Parameters:
- freqstr
A
ResampleFrequencytoken, coarser than the dataset’s bars.- howdict[str, str] or str
One
ResampleMethodfor every factor variable, or a{variable: method}dict naming every one.
- Returns:
- Self
A new factor of the same class.
- Raises:
- ValueError
If
freqor a method is not a known token. Whetherfreqis coarser than the bars andhownames every variable is checked when a panel is requested.
Examples
>>> daily = factor.resample("1d", "last") # factor on minute bars >>> factor.compute("2024-01-02", "2024-01-03").sizes["timestamp"] 780 >>> daily.compute("2024-01-02", "2024-01-03").sizes["timestamp"] 2
- rewind(end)[source]#
Drop the bars after
endand end the recorded range there: the inverse ofextend.The bars up to
endstay as they are and the symbol and variable axes keep every label, so the nextextendcomputes the dropped bars again (quantlab.backend.ranged.RangedStore.rewind). Only the factor owning its store may rewind it (refuse_unless_owner).- Parameters:
- endstr, datetime.date or pd.Timestamp
The new end of the store’s range, inside it and before its end.
- Returns:
- Self
self, for chaining.
- Raises:
- ValueError
If the factor has no store path, does not own its store, the store has no recorded range, or
endis not inside the recorded range and before its end.
Examples
>>> factor.build("2024-01-01", "2024-03-31").rewind("2024-02-29").store_range() ('2024-01-01', '2024-02-29') >>> factor.extend("2024-03-31").store_range() ('2024-01-01', '2024-03-31')
- property store_path#
Return the Zarr store this factor reads and writes.
This is
config.file_pathfor a factor that is not resampled. A resampled factor uses a store beside it with_resample_<freq>added to the name, so the resampled panel never overwrites the source bars.Nonewhenconfig.file_pathisNone.Examples
>>> factor.store_path 'data/factors/momentum.zarr' >>> factor.resample("1d", "last").store_path 'data/factors/momentum_resample_1d.zarr'
- store_range()[source]#
Return the
(start, end)the store was built for, if recorded.Nonewhen the store was not written bybuild.Examples
>>> factor.build("2024-01-01", "2024-03-31").store_range() ('2024-01-01', '2024-03-31')
- store_uses()[source]#
Return the store this factor uses itself: its store, owned or only read.
A factor with a store declares it with itself as the
RangedStoreHandle(store_range(),extend), so the live day and the data update extend a factor and a factor risk model’s stores in one loop:"owns"when it may write the store (owns_store),"reads"when it is pinned to some of the store’s variables. A factor without a store declares none; a view (FactorView) declares the wrapped store. SeeComponent.store_uses.Examples
>>> [(use.relation, use.handle is factor) for use in factor.store_uses()] [('owns', True)] >>> [use.relation for use in owner_pinned_to_one_output.store_uses()] ['reads'] >>> factor_without_file_path.store_uses() ()
- stored_symbols()[source]#
Return the symbol axis of the store
readreads, in axis order.Only the coordinate is read, never a data variable. A resampled factor without a store of its own reads the source factor’s store, as
readdoes. The labels keep the store’s own type.Examples
>>> factor.build("2024-01-01", "2024-03-31").stored_symbols()[:2] ['S0USDT', 'S1USDT']
- property warmup_bars#
Bars of history
computereads before the requested start.Counted on the input dataset’s own calendar, so weekends and holidays are skipped, not counted. It is
config.warmup_bars.Examples
>>> factor.warmup_bars 20
- write_rows(rows, start, end)[source]#
Replace the store with
rows, computed overstarttoend, and record the range.The write half of
build, for rows computed elsewhere: a chunk computed in a worker process (ChunkedBuilder). The rows are written asbuildwritescompute(start, end), through the samequantlab.backend.ranged.RangedStoreand the same owner guard (refuse_unless_owner), sowrite_rowsthenappend_rowsleave the storebuildthenextendwould.- Parameters:
- rowsxr.Dataset
The factor’s outputs on
(timestamp, symbol)over the range.- start, endstr, datetime.date or pd.Timestamp
The range
rowscover, recorded as the store’s range.
- Returns:
- Self
self, for chaining.
- Raises:
- ValueError
If the factor has no store path or does not own its store.
Examples
>>> factor.write_rows(factor.compute("2024-01-01", "2024-01-31"), "2024-01-01", "2024-01-31").store_range() ('2024-01-01', '2024-01-31')