Module Algostream_optimization.Cross_validation

Time-series cross-validation with purging and embargo.

Naive k-fold on financial returns leaks. Observations near a fold boundary are serially correlated with observations on the other side, so a model fitted on one has partial knowledge of the other, and the out-of-sample Sharpe comes back inflated. The correction (López de Prado) is two-part:

Combinatorial_purged goes further. Rather than one train/test split per fold, it takes every combination of n_test_groups out of n_groups as the test set, producing many distinct out-of-sample paths — and therefore a distribution of out-of-sample Sharpe instead of a point estimate. Combined with Stochastic.Quantile.percentile_interval that gives an honest confidence interval on out-of-sample performance with no new dependencies.

type scheme =
  1. | Purged_kfold of {
    1. k : int;
    2. embargo_ns : int64;
    }
  2. | Combinatorial_purged of {
    1. n_groups : int;
    2. n_test_groups : int;
    3. embargo_ns : int64;
    }
type split = {
  1. index : int;
  2. train : (int64 * int64) array;
    (*

    possibly several intervals, since purging cuts holes

    *)
  3. test : (int64 * int64) array;
}
val splits : scheme -> lo_ns:int64 -> hi_ns:int64 -> split array

Build the splits covering [lo_ns, hi_ns].

val n_splits : scheme -> int

Number of splits a scheme will produce, without building them. For Combinatorial_purged this is C(n_groups, n_test_groups) and grows fast — 10 choose 2 is 45, 20 choose 4 is 4845.

val is_leak_free : split -> embargo_ns:int64 -> bool

True if no training interval in split intersects any test interval or its embargo. The property the whole module exists to guarantee; asserted directly in the test suite.

type report = {
  1. scheme : string;
  2. per_split : Metrics.t array;
  3. mean_oos : float;
    (*

    mean of the objective across splits

    *)
  4. oos_distribution : Quantile.summary;
    (*

    the point of CPCV: a distribution, not a point

    *)
  5. n_splits : int;
  6. n_failed : int;
}
val run : scheme:scheme -> lo_ns:int64 -> hi_ns:int64 -> objective:Objective.t -> eval:(split -> Metrics.t) -> n_domains:int -> report

run evaluates eval on each split's test window and summarizes. eval receives the split so it can restrict its data accordingly.

val report_to_string : report -> string