Module Algostream_stochastic.Resample

Bootstrap resampling for historical series.

Four variants, in increasing order of how much serial structure they preserve:

Block length matters more than the variant. Too short and dependence is destroyed anyway; too long and there are too few distinct blocks for the resamples to differ. rule_of_thumb gives the standard n^(1/3) starting point. The automatic Politis-White selector is deliberately not implemented — it needs a flat-top-kernel spectral density estimate that we are not going to hand-roll to a defensible standard, and a wrong automatic answer is worse than an explicit rule of thumb.

module Rng = Algostream_rng.Rng
val iid : Rng.t -> data:float array -> n:int -> float array

iid rng ~data ~n draws n observations with replacement.

val moving_block : Rng.t -> data:float array -> block_len:int -> n:int -> float array

Fixed-length blocks, no wrap. Start indices are uniform on [0, length data - block_len].

val circular_block : Rng.t -> data:float array -> block_len:int -> n:int -> float array

Fixed-length blocks with circular wrap — every observation appears with equal probability.

val stationary : Rng.t -> data:float array -> mean_block_len:float -> n:int -> float array

Politis-Romano stationary bootstrap. Block lengths are Geometric with mean mean_block_len; wraps circularly.

val rule_of_thumb : n:int -> int

n^(1/3) rounded, floored at 1 — the conventional starting point for block length. Tune it against the series' autocorrelation rather than trusting it blindly.

Cross-sectional resampling

joint_index is the one that matters for pairs trading and is easy to get catastrophically wrong. Bootstrapping each leg of a pair independently destroys the cointegration the strategy exists to trade, so the resampled world contains no tradable relationship and the Monte Carlo reports a strategy that cannot possibly work. Resampling the shared time index and applying it to every series preserves the cross-sectional structure while still randomizing the path.

Always use this for multi-asset scenarios.

val joint_index : Rng.t -> n_source:int -> n:int -> block_len:int -> int array

joint_index rng ~n_source ~n ~block_len returns n indices into [0, n_source) drawn as circular blocks. Apply the same index array to every series. block_len <= 1 degenerates to iid index resampling.

val take : data:float array -> idx:int array -> float array

Apply an index array produced by joint_index (or any index array) to a series. Raises Invalid_argument if any index is out of range.

Randomization tests

val permute : Rng.t -> 'a array -> 'a array

Uniformly random permutation (a copy; the input is not mutated).

val sign_flip : Rng.t -> float array -> float array

Multiply each element by a random ±1. The standard randomization test for "is this mean distinguishable from zero" under a symmetry assumption.