quantlab.model.base.BaseModel#
- class quantlab.model.base.BaseModel(config)[source]#
-
Framework-agnostic base class of every model head.
A head is configured with a list of factor objects (its features) and a list of label objects (its targets).
collect()pulls both into one panel indexed by(timestamp, symbol);train()fits the head on thetrain_*dates of its config and writes a checkpoint;load()restores one;predict()andpredict_panel()run inference. Those public entry points are implemented once, here, and are not overridden by any head.The training framework is the job of the two variants.
TorchModelis the torch variant andLibraryModelthe numpy variant. Both accept the oneModelConfig, named by the class attributeconfig_cls: theconfigsetter checks it first, and the config loader reads it from the class before creating an instance. Each variant declarescheckpoint_suffix, the checkpoint file suffix.trainandtrain_cvuse it to name files, andload()uses it to reject a file of the wrong kind before building any model.trainwrites a trained unit (quantlab.runs.trained_run) underconfig.model_save_diras{class}_trial_{timestamp}/: the checkpoint{class}_total{suffix}, theconfig.jsonthat rebuilds the model, the evaluation files andrun.json, which records the windows, the metrics and what the head was trained on.- Parameters:
- configModelConfig
The model configuration.
- Attributes:
- modelobject or None
The fitted model (an
nn.Moduleor a library model object), or None beforetrain()orload().- collected_panelCollectedPanel or None
What
collect()read (or the paneluse_panel()handed over), None before either.
- Raises:
- TypeError
If
configis not an instance ofconfig_cls.
Examples
Given a head
MyHead(a subclass ofLibraryModelorTorchModel), a factor object exposing variablesf_aandf_b, and a label object exposingret:>>> model = MyHead(ModelConfig( ... factors=[factor], labels=[label], ... model_save_dir="checkpoints", ... factor_data_strategy="read", label_data_strategy="read", ... train_start="2024-01-01", train_end="2024-01-30", ... test_start="2024-01-31", test_end="2024-02-09", ... )) >>> checkpoint = model.collect().train() >>> checkpoint.name 'MyHead_total.joblib'
- check_checkpoint(p)[source]#
Check a checkpoint’s recorded variables against the model’s own.
The feature and label names (and their order) the checkpoint was trained on are read from
trained_onin its unit’srun.json. Onlyrun.jsonis read, so the check can run before any data is collected;loadruns it too.- Parameters:
- pPath | str
Path to a checkpoint file.
- Returns:
- TrainedRun
The checkpoint’s trained unit.
- Raises:
- FileNotFoundError
If
pdoes not exist.- ValueError
If the checkpoint is not part of a trained run
TrainedRuncan open, or the recorded and declared variables differ, naming the checkpoint and both variable lists.
Examples
>>> model.check_checkpoint(checkpoint).kind # trained on the same variables 'model' >>> other.check_checkpoint(checkpoint) Traceback (most recent call last): ValueError: FirstFeatureHead: checkpoint .../FirstFeatureHead_total.joblib was trained on factor variables ['past_ret_1'] (trained_on in its run.json), but this model declares ['past_ret_2']; loading it would feed the model different or permuted inputs
- check_hyperparameters()[source]#
Validate the reserved hyperparameters this variant reads.
Called first by
collect,trainandtrain_cv, before any data is read or any tracking run or checkpoint directory is opened. The default checks nothing.- Raises:
- ValueError
If a reserved hyperparameter is invalid.
Examples
>>> model.check_hyperparameters() # a valid setting returns None
- check_reload(p, bars=5, rtol=0.0001, atol=1e-06)[source]#
Check that the model predicts its checkpoint’s first test bars as the training run did.
The first
warmup_bars + barsbars of the predictions the trained run stored (test_predictions.zarr) are predicted again withpredict_window, from freshly requested features, as a backtest or a live run predicts them, and compared on the symbols both hold. The training run predicted them from one panel read from itsstart_date; a difference means a later prediction does not see the inputs the model was scored on: a feature requested without its warm-up, a store rewritten since training, or a different checkpoint loaded. Call it on the trained or loaded model.- Parameters:
- pPath | str
Path to the checkpoint file; its trained unit holds the stored predictions.
- barsint
Bars compared after the warm-up bars.
- rtol, atolfloat
Tolerances of
numpy.isclose; NaN equals NaN.
- Returns:
- TrainedRun
The checkpoint’s trained unit.
- Raises:
- ValueError
If the unit stored no test predictions, or a label’s predictions differ on some bar, naming the bars and the largest difference.
Examples
>>> model.check_reload(checkpoint).kind 'model'
- abstract property checkpoint_suffix#
The checkpoint file suffix, including the leading dot.
Concrete variants satisfy it with a plain class attribute.
Examples
>>> LibraryModel.checkpoint_suffix '.joblib'
- property class_name#
The head’s class name.
Examples
>>> model.class_name MyHead
- collect()[source]#
Read the features and labels into the model’s collected panel.
The hyperparameters are checked first (
check_hyperparameters), so an invalid one fails before any data is read. Each factor and label is asked for its panel fromstart_datetoend_date:read(start, end)from its store under the"read"strategy,compute(start, end)from its inputs under"cal". Factor panels startwarmup_barsbars earlier (see_collect_all_features); labels do not, so they are NaN on those bars. The factor panels are merged on the union of their(timestamp, symbol)coordinates, and the labels are joined onto the features’ bars and symbols (CollectedPanel.assemble: a label’s symbol or bar without features is dropped); the panel is kept, sorted on both axes, ascollected_panel. Call this beforetrain()ortrain_cv().What it reads is recorded by a
DataRecorderkeyed by component path within the model (factors.0.dataset), and written into therun.jsonof the unittrain()ortrain_cv()writes next (seetraining_record).- Returns:
- Self
The model itself, for chaining.
- Raises:
- ValueError
If a hyperparameter this variant reads is invalid.
Examples
>>> model.collect().num_times 40
- property config#
The model’s configuration object.
Examples
>>> model.config.model_save_dir checkpoints
- config_cls#
The config class every model accepts; the config loader reads it from the class to rebuild a model from
config.json.alias of
ModelConfig
- evaluation_segments()[source]#
The bars the model is scored on: its purged train, validation and test segments.
A segment
config.metric_splitsdoes not name is empty.The collected panel’s timestamps, cut as training cuts them (see
_fit_segments): the training window split byval_size, then each segment followed by another losing its last L bars, L the largestlookahead_bars()of the model’s labels. Callcollect()first.- Returns:
- Segments
Timestamps of each segment;
valis empty whenval_sizeis 0 or the purge empties it.
- Raises:
- ValueError
If no training bar is left, by
val_sizeor by the purge.
Examples
With
val_size0:>>> segments = model.collect().evaluation_segments() >>> [split for split, _ in segments.splits()] ['train', 'test']
- property fitted_train_bounds#
The training window actually fitted,
(train_start, train_end), after the purge.After
train()it is the window the fit used; afterload()the one the checkpoint’srun.jsonrecords.- Raises:
- RuntimeError
If neither
train()norload()has been called.
Examples
>>> model.train_bounds, model.fitted_train_bounds (('2024-01-01', '2024-02-09'), ('2024-01-01', '2024-02-07T00:00:00.000000000'))
- get_factor_names()[source]#
Return the feature variable names, in factor order then variable order.
Each factor contributes its pinned
config.factor_nameswhen set, else every name its class can produce (see_variable_names).Examples
>>> model.get_factor_names() ['f_a', 'f_b']
- get_label_names()[source]#
Return the label variable names, in label order then variable order.
Examples
>>> model.get_label_names() ['ret']
- head_hyperparameters(hyperparameters)[source]#
Return
hyperparameterswithout the keys this variant reads itself.A torch head that forwards its hyperparameters to a network passes them through this first;
LibraryModelapplies it before calling_init_model, so a library head receives the result. Only the variant’s ownreserved_hyperparametersare dropped: a library head keepslr, which a torch head reserves, because the library may take it. The dict given is not modified.Examples
>>> xgb_head.head_hyperparameters({"early_stopping": True, "lr": 0.1}) {'lr': 0.1} >>> torch_head.head_hyperparameters({"epochs": 5, "hidden": 8}) {'hidden': 8}
- property label_delays#
Each label’s
delayin bars, in the order oflabels.Examples
>>> model.label_delays (1,)
- property label_scales#
Each label name’s prediction scale:
"raw"or"standardized"."raw"means the prediction is in the label’s own units (a forward return in return units),"standardized"that it only ranks the cross-section. A model is"raw"exactly when its training target is the label itself; the model layer’s variants report"standardized"for every label once a head overrides_transform_target. A model without a training-target hook predicts its labels as they are.Examples
>>> model.label_scales {'fwd_ret_1': 'raw'}
- property labels#
The label objects the model is trained on, in config order.
Examples
>>> [label.get_factor_names() for label in model.labels] [('fwd_ret_1',)]
- load(p)[source]#
Restore the model from a checkpoint file.
The file suffix is checked first, so a
.pthfile is never handed to joblib and a.joblibfile never totorch.load. The feature and label variables recorded in the unit’srun.jsonmust then match this model’s declared variables, name for name and in order; neither torch nor a tree library would notice a permuted or substituted input by itself. Finally the checkpoint is loaded, and the model takes the recorded training and test windows:train_bounds,test_boundsandfitted_train_boundsthen describe the checkpoint, whatever the config said before.- Parameters:
- pPath | str
Path to a checkpoint file ending in
checkpoint_suffix.
- Returns:
- Self
The model itself, for chaining.
- Raises:
- FileNotFoundError
If
pdoes not exist.- ValueError
If the suffix is wrong, the checkpoint is not part of a trained run
quantlab.runs.trained_run.TrainedRuncan open, or the recorded variables differ from the model’s declared variables.
Examples
>>> model.load(checkpoint) is model True
- property model_save_dir#
The directory
trainandtrain_cvcreate trial directories in.Examples
>>> model.model_save_dir.name 'checkpoints'
- property num_factors#
Number of feature variables across all configured factors.
Examples
>>> model.num_factors 2
- property num_labels#
Number of label variables across all configured labels.
Examples
>>> model.num_labels 1
- property num_null#
Total number of NaN cells across every variable of the collected panel.
Examples
>>> model.num_null 0
- property num_symbols#
Number of symbols in the collected panel.
Examples
>>> model.num_symbols 3
- property num_times#
Number of timestamps in the collected panel.
Examples
>>> model.num_times 40
- predict(data)[source]#
Return
[T, S, L]predictions for a[T, S, F]input.The variant’s
_predictdecides the accepted and returned types: the torch variant returns a tensor, the numpy variant an array.- Raises:
- ValueError
If neither
train()norload()has been called.
Examples
>>> x = np.random.default_rng(0).standard_normal((5, 3, 2)) >>> model.predict(x).shape (5, 3, 1)
- predict_panel(features)[source]#
Predict from a feature panel and return a panel of the same shape.
The result has one variable per label name, on
("timestamp", "symbol"), with coordinates taken from the sorted panel that was actually fed toto_array, so values and coordinates cannot drift apart. Positions where every feature is NaN get NaN for every label: a head that fills NaN with zero would otherwise produce finite predictions for symbols that do not exist yet. Any symbol may be present, including ones the model never saw in training.The variant hook
_predict_panel_arrayperforms the numeric prediction. A torch head predicts bar t from the bars before it infeatures, so passwarmup_barsbars before the first bar you need.- Parameters:
- featuresxr.Dataset
A dataset containing every variable named by
get_factor_names().
- Raises:
- ValueError
If a factor variable is missing, or if the prediction does not have shape
[num_times, num_symbols, num_labels].
Examples
>>> feats = panel[["f_a", "f_b"]].isel(timestamp=slice(0, 5)) >>> out = model.predict_panel(feats) >>> dict(out.sizes), list(out.data_vars) ({'timestamp': 5, 'symbol': 3}, ['ret'])
- predict_scored()[source]#
Predict the bars of the collected panel the model is scored on, from the panel’s feature array.
The bars run from
warmup_barsbars before the first bar ofevaluation_segments()(its first scored segment) to the last, so a windowed head’s first scored bar has its history; each is predicted aspredict_panelwould. The array is thecollected_panel’s, built once in the panel’s own type and shared by every caller, so evaluating after each fit does not convert the panel again.- Returns:
- xr.Dataset
One variable per label on the predicted bars and the panel’s symbols.
Examples
With
metric_splits=("test",)only the 10 test bars are predicted:>>> model.collect().train() >>> model.predict_scored().sizes["timestamp"] 10
- predict_window(start, end)[source]#
Predict every bar from
starttoendfrom freshly requested features.Each factor is asked for its panel from
warmup_barsbars beforestart(counted as_feature_startscounts them) toend, byconfig.factor_data_strategy; the panel goes throughpredict_paneland the result is cut tostart..``end``. No config is changed. The model must be trained or loaded.- Parameters:
- start, endstr
First and last bar to predict, inclusive.
- Returns:
- xr.Dataset
One variable per label name on
(timestamp, symbol).
Examples
>>> out = model.predict_window("2024-02-12", "2024-03-11") >>> list(out.data_vars), out.sizes["timestamp"] (['fwd_ret_1'], 21)
- provenance()[source]#
What the top unit records about its data and its code.
data_fingerprintistraining_record;codethe code record of this model’s tree (quantlab.runs.record.code_of), taken when the unit is written.Examples
>>> sorted(model.collect().provenance()) ['code', 'data_fingerprint']
- property purge_bars#
L, the largest
lookahead_bars()among the model’s labels.Each segment followed by another loses its last L bars, so no fitted label reads a bar of the next segment.
Examples
>>> model.purge_bars # one forward-return label over 1 bar, delay 1 2
- reserved_hyperparameters = frozenset({})#
The
hyperparameterskeys this variant reads itself, whichhead_hyperparametersleaves out.
- property symbols#
Symbols of the collected panel, as a plain list.
Examples
>>> model.symbols ['S0', 'S1', 'S2']
- property test_bounds#
The configured test window,
(test_start, test_end).Examples
>>> model.test_bounds ('2024-02-05', '2024-02-09')
- to_array(data, variables, dtype=None)[source]#
Convert a panel to a
[num_times, num_symbols, len(variables)]array.Both axes are sorted, and the last axis follows the order of
variablesexactly (not alphabetical order), sox[..., i]is alwaysvariables[i].- Parameters:
- dataxr.Dataset
A dataset indexed by
(timestamp, symbol).- variableslist[str]
The data variables to stack, in the wanted order.
- dtypenumpy dtype, optional
The array’s type; by default the variables’ common type.
Examples
>>> panel = model.collected_panel.data >>> x = model.to_array(panel, ["f_b", "f_a"]) >>> x.shape (40, 3, 2) >>> np.allclose(x[..., 0], panel["f_b"].values) True >>> model.to_array(panel, ["f_b", "f_a"], dtype=np.float32).dtype dtype('float32')
- property tracker#
The tracker every run of the model is opened through,
config.tracker.Examples
>>> type(model.tracker).__name__ 'NullTracker'
- property tracking_project#
The default project of the model’s tracking runs: its class name.
A tracker’s own
projectreplaces it.Examples
>>> model.tracking_project 'MyHead'
- train()[source]#
Train once on the config’s
train_*/test_*dates and save.A new trial directory is created under
model_save_dirand is the trained unit:train_intotrains into it under a tracking run{class}_total, grouped by the directory’s name, and writes the checkpoint,config.json, the evaluation files andrun.json. Returning the path lets a caller record exactly which model was trained and reload it later instead of retraining;TrainedRun.openreads the rest of the run from it.- Returns:
- Path
Absolute path of the checkpoint file, named
{class}_total{checkpoint_suffix}.
Examples
>>> checkpoint = model.train() >>> run = TrainedRun.open(checkpoint) >>> checkpoint.name, run.path.name.startswith("MyHead_trial_") ('MyHead_total.joblib', True) >>> sorted(run.metrics)[:4] ['test_ic', 'test_icir', 'test_loss', 'test_mae'] >>> sorted(p.name for p in run.path.iterdir()) ['MyHead_total.joblib', 'config.json', 'ic_series.csv', 'run.json', 'test_predictions.zarr']
- property train_bounds#
The configured training window,
(train_start, train_end), before the purge.Examples
>>> model.train_bounds ('2024-01-01', '2024-02-02')
- train_cv(train_periods, expanding=False, test_periods=None)[source]#
Run a walk-forward cross-validation and return the walk-forward unit.
Walk-forward cross-validation trains on a window of past data and tests on the period right after it, then moves the test period forward, so a test period never precedes its training data. The training window either slides with it at a fixed length (the default) or, with
expanding=True, keeps the first fold’s start and grows to all history before the test period. Both modes test on the same periods, so their results compare bar for bar. Folds are laid out byquantlab.model.split.walk_forward_foldsover the timestamps betweenconfig.start_dateandconfig.end_date. Each fold’s training window loses its last L bars, L being the largestlookahead_bars()among the labels, so no fitted label reads a test-period bar.A new trial directory
{class}_trial_{timestamp}/is created undermodel_save_dirand becomes a"walk_forward"unit (quantlab.runs.trained_run). Every fold trains on its own dates into its own"model"unitfold_{i}/, with the checkpoint{class}_cv_fold_{i}{suffix}and a tracking run of that name. The fold means of everytrain_*/val_*/test_*metric, keyedcv_mean_{key}, pluscv_n_foldsare written to the summary of a separate{class}_cv_summaryrun. Last, the trial’srun.jsonrecords the folds and those means. A backtester’srun_cvreplays the unit. The procedure isquantlab.model.walk_forward_training.train_walk_forward, the one an ensemble’strain_cvruns too. Afterwards the model keeps the dates it was configured with, not the last fold’s.- Parameters:
- train_periodsint
Number of timestamps in the first fold’s training segment, and in every fold’s when sliding.
- expandingbool, default False
Train every fold from the first fold’s start instead of sliding a fixed-length window. Test segments, fold count and the purge are those of the sliding mode; the validation segment stays the last
val_sizeshare of each growing window. The mode is not recorded: the fold windows carry it.- test_periodsint, optional
Number of timestamps in each fold’s test segment, and the step from one fold to the next. Defaults to
train_periods // 5. Like the mode, it is carried by the fold windows.
- Returns:
- TrainedRun
The
"walk_forward"unit:foldsholds each fold’s unit (index, configured and fitted training window, test window, metrics, checkpoint) andcv_meanthe fold means (empty when no fold has metrics).
- Raises:
- ValueError
If
test_periodsis below 1, or is not given andtrain_periodsis below 5 (the test segment would be empty), no timestamps fall inside the config’s date range, or the purge leaves a fold no training bar, or a reserved hyperparameter is invalid (seecheck_hyperparameters).
Examples
>>> cv = model.train_cv(train_periods=20) >>> cv.kind, len(cv.folds) ('walk_forward', 4) >>> cv.folds[0].index, cv.folds[0].checkpoint.name (0, 'MyHead_cv_fold_0.joblib')
An expanding run tests on the same bars, and every fold trains from the first fold’s start:
>>> expanding = model.train_cv(train_periods=20, expanding=True) >>> len({fold.train_window[0] for fold in expanding.folds}) 1 >>> [f.test_window for f in expanding.folds] == [f.test_window for f in cv.folds] True
test_periodssets the test length instead of one fifth:>>> len(model.train_cv(train_periods=20, test_periods=10).folds) 2
- train_fold(fold, run_dir, group)[source]#
Train one walk-forward fold into
run_dir, then restore the model’s own dates.The config gets the fold’s training window before the purge and its test window (
fold_config);train_intopurges, trains and writes the"model"unit with the checkpoint and tracking run{class}_cv_fold_{i}. The config the model had before is put back afterwards, also when training raises.- Parameters:
- foldFold
The fold to train.
- run_dirPath or str
The fold’s unit directory,
fold_{i}/of the trial.- groupstr
Tracking group, the trial directory’s name.
Examples
>>> before = model.config >>> model.collect().train_fold(fold, tmp / "fold_0", group="trial") >>> model.config == before, TrainedRun.open(tmp / "fold_0").kind (True, 'model')
- train_into(run_dir, group, experiment_name, provenance=None)[source]#
Train, evaluate and save once into a caller-given run directory.
The random generators are reseeded from
config.random_seedfirst, so models trained one after another in one process do not share random state. A tracking run namedexperiment_nameis opened in the groupgroup(see_tracking_run), and the variant’s_fittrains and writes the checkpoint{experiment_name}{checkpoint_suffix}and itsconfig.jsonintorun_dir, returning its{split}_loss. The trained model is then scored by_evaluate, which writesic_series.csvandtest_predictions.zarrbeside the checkpoint; the losses and the evaluation metrics, merged, go once to the tracking run’s summary and, last, torun.json, which makesrun_dira trained unit (quantlab.runs.trained_run); the fitted window becomesfitted_train_bounds. No trial directory is created:trainand everytrain_cvfold pass the directory they lay out themselves.- Parameters:
- run_dirPath or str
Directory that receives the checkpoint and the evaluation files. It must not exist yet; it is created with its parents.
- groupstr
Tracking group of the run, the trial directory’s name.
- experiment_namestr
Tracking run name, also the checkpoint file’s stem.
- provenancedict, optional
provenance(), for the top unit (train); a fold or a member records none.
- Returns:
- tuple[Path, dict or None]
The absolute checkpoint path and the metrics: the losses
_fitreturned and the_evaluatemetrics, or None when_fitreturned None.
- Raises:
- RuntimeError
If
run_diralready exists (see_save_model).
Examples
>>> checkpoint, metrics = model.collect().train_into( ... tmp / "member_0", group="trial", experiment_name="MyHead_member_0" ... ) >>> checkpoint.name, sorted(metrics)[:1] ('MyHead_member_0.joblib', ['test_ic'])
- property training_record#
What the last
collect()read, by component path; empty before it.train()andtrain_cv()write it as it is: a config changed aftercollect()is not re-read, so collect again after changing what is read.Examples
>>> sorted(model.collect().training_record) ['factors.0.dataset', 'labels.0.factor.dataset']
- use_panel(panel)[source]#
Take
panel, collected by a model reading the same data, as this model’s collected panel.The models then share the panel and every array built from it (each fit converts it once in all), and this model’s
training_recordis the panel’s. The panel must have been read for the same factor and label configs over the samestart_dateandend_date, with the same warm-up and read strategies; other hyperparameters and the seed may differ.- Parameters:
- panelCollectedPanel
Another model’s
collected_panel.
- Returns:
- Self
The model itself, for chaining.
- Raises:
- ValueError
If
panelwas collected for other factors, labels or dates.
Examples
>>> deep = MyHead(dataclasses.replace(config, hyperparameters={"max_depth": 6})) >>> deep.use_panel(model.collect().collected_panel).collected_panel is model.collected_panel True
- walk_forward_bars()[source]#
The collected bars between
config.start_dateandconfig.end_date.train_cvlays its folds out over them. Callcollect()first.Examples
>>> len(model.collect().walk_forward_bars()) 30
- property warmup_bars#
Bars of features the model needs before the first bar it predicts.
0 here: a row model predicts each bar from that bar alone.
TorchModelreturnswindow_bars - 1.Examples
>>> model.warmup_bars 0