openquant.backtesting_engine
Pure Python. Signatures are as written in the source; descriptions are the docstrings. All Python modules.
CPCV backtest paths from out-of-sample results you computed (AFML chapters 11 and 12).
The workflow keeps the model in Python and the bookkeeping in Rust:
1. splits = openquant.cross_validation.cpcv_splits(t0, t1, n_groups, test_groups, pct_embargo) 2. for each split, fit on train_indices and compute the strategy's out-of-sample return for every sample in test_indices (in that order); 3. run_cpcv(t0, t1, split_returns, ...) scores every split and every one of the φ[N, k] backtest paths, with the Rust openquant::backtesting_engine::run_cpcv.
:func:assemble_cpcv_paths stitches any per-split out-of-sample values (predictions, positions, returns) into full paths, for statistics the engine does not compute.
Functions
assemble_cpcv_paths
assemble_cpcv_paths(
split_values: Sequence[Sequence[float]],
splits: Sequence[Mapping[str, Any]],
paths: Any,
) -> np.ndarrayStitch per-split out-of-sample values into full CPCV paths.
splits is the output of :func:openquant.cross_validation.cpcv_splits, paths that of :func:openquant.cross_validation.cpcv_paths, and split_values[s] holds one value per sample of splits[s]["test_indices"]. Returns an (n_paths, n_samples) float array whose row p takes, for every fold g, the values split paths[p, g] produced for that fold's samples: one out-of-sample value per sample, per path.
cpcv_path_count
cpcv_path_count(n_groups: int, test_groups: int) -> intφ[N, k] = k/N · C(N, k), the number of backtest paths CPCV produces (AFML §12.4).
run_cpcv
run_cpcv(
t0: Any,
t1: Any,
split_returns: Sequence[Sequence[float]],
*,
n_groups: int,
test_groups: int,
pct_embargo: float,
mode_provenance: str,
trials_count: int,
safeguards: Mapping[str, str],
) -> dictScore precomputed CPCV out-of-sample returns per split and per backtest path.
split_returns[s] holds split s's out-of-sample returns, one per sample of that split's test_indices from :func:openquant.cross_validation.cpcv_splits with the same n_groups, test_groups and spans; the two modules test the same samples in the same split order. mode_provenance, trials_count (how many configurations were tried, for deflating the Sharpe ratio later) and the five safeguards statements (survivorship_bias_control, look_ahead_control, data_mining_control, cost_assumption, multiple_testing_control) are required and recorded in the diagnostics, as AFML chapter 11 asks.
Returns a dict with folds (per-split sharpe, mean_return, std_return, observations), splits, path_count, path_assignments (one list per path, split_for_group), path_distribution (per-path statistics) and diagnostics. Each sharpe is the t-statistic mean / std * sqrt(n), not annualised.
The engine purges and embargoes its own copy of the splits, reported in splits. Its purge compares each training label with each test label and its embargo is the one cross_validation uses, so with increasing label starts its training sets equal cpcv_splits's. Those training sets are reported, not used: only the test indices, identical in both modules, and your returns are.