openquant._core.backtesting_engine
Compiled from Rust (crates/pyopenquant). Signatures and types are from the shipped .pyi stubs; descriptions are the functions’ own docstrings, as help() shows them. All Python modules.
Functions
cpcv_path_count
cpcv_path_count(n_groups: int, test_groups: int) -> intNumber of CPCV backtest paths, phi[N, k] = k / N * C(N, k) (AFML section 12.4.1).
Each of the N groups is tested in exactly phi of the C(N, k) splits, so the splits can be stitched into phi full-length out-of-sample paths. For example N = 6, k = 2 gives 15 splits and 5 paths.
Parameters
n_groups: intNumber of contiguous groups N; at least 2.
test_groups: intNumber of groups tested per split k;
1 <= test_groups < n_groups.
Returns
- int
The path count phi[N, k].
Raises
- ValueError
If the core rejects the input (e.g.
n_groups < 2,test_groups == 0,test_groups >= n_groups, or C(N, k) too large for the platform's integers).
run_cpcv
run_cpcv(
t0: Sequence[int],
t1: Sequence[int],
split_returns: Sequence[Sequence[float]],
*,
n_groups: int,
test_groups: int,
pct_embargo: float,
mode_provenance: str,
trials_count: int,
safeguards: Mapping[str, str],
) -> dict[str, Any]Run a combinatorial purged cross-validation backtest on precomputed returns (AFML 12.4).
The samples are cut into n_groups contiguous groups (the first n % n_groups one sample larger) and one split is built for each of the C(N, k) ways to test test_groups of them, in lexicographic order; this is the numbering of openquant.cross_validation.cpcv_splits with n_splits=n_groups, n_test_splits=test_groups. Each split trains on the other groups after purging (closed-interval overlap with any test span) and an embargo of ceil(pct_embargo * n) samples after each run of adjacent test groups. No model is fitted here: fit and predict on those splits yourself, then pass one array of out-of-sample returns per split. Path j takes each group's returns from the j-th split that tests it, giving phi[N, k] paths that each cover every sample once. The paths reuse the same predictions, so their spread is not a confidence interval.
sharpe in the results is a t-statistic, mean / std * sqrt(n) with the sample standard deviation (ddof 1); it is not annualised, grows with n, and is 0 when the deviation is 0.
Parameters
t0: list[int]Start of each sample's label span, as int64 nanoseconds since the epoch (plain integers also work). One per sample, in time order.
t1: list[int]End of each sample's label span, in the same units as
t0. Must be>= t0.split_returns: list[list[float]]One array per split, in
split_idorder, each holding one finite out-of-sample return per entry of that split'stest_indices, in the same order.n_groups: intNumber of contiguous groups N;
2 <= n_groups <= len(t0). Keyword-only.test_groups: intNumber of groups tested per split k;
1 <= test_groups < n_groups. Keyword-only.pct_embargo: floatEmbargo as a fraction of the whole sample count, in
[0, 1), rounded up. Keyword-only.mode_provenance: strNon-blank free-text note on where this configuration came from. Keyword-only.
trials_count: intNumber of configurations tried to reach this one (what a deflated Sharpe ratio needs); must be positive. Keyword-only.
safeguards: dict[str, str]How the run controls the pitfalls of AFML section 11.4, as non-blank free text, with exactly the keys
survivorship_bias_control,look_ahead_control,data_mining_control,cost_assumptionandmultiple_testing_control. Keyword-only.
Returns
- dict[str, Any]
A dict with keys:
folds(list[dict]): one per split, withsplit_id(int),sharpe(float),mean_return(float),std_return(float) andobservations(int).splits(list[dict]): one per split, withsplit_id(int),train_indices(list[int]),test_indices(list[int]),test_groups(list[int], ascending),purged_count(int) andembargo_count(int, samples embargoed that the purge had not already removed).path_count(int): phi[N, k].path_assignments(list[list[int]]): per path, elementgis thesplit_idthat supplies groupg's returns.path_distribution(list[dict]): one per path, withpath_id(int),sharpe,mean_return,std_return(floats) andobservations(int).diagnostics(dict):mode(always "combinatorial_purged_cross_validation"),mode_provenance(str),trials_count(int),split_count(int),pct_embargo(float),safeguards(dict[str, str], as passed),uses_label_span_purging(bool, always True),uses_embargo(bool,pct_embargo > 0),total_purged(int) andtotal_embargoed(int).
Raises
- ValueError
If
t0andt1differ in length; ifsafeguardsis missing a required key or has an unknown one; ifsplit_returnsdoes not have exactly one entry per split or an entry's length differs from its split's test set; or if the core rejects the input (e.g. no samples, a span that ends before it starts, a blankmode_provenanceor safeguard,trials_count == 0,n_groups < 2,test_groupsnot in[1, n_groups),n_groupsabove the sample count,pct_embargooutside[0, 1), purging and embargo emptying a training set, or an empty or non-finitesplit_returnsentry).