Module Algostream_optimization.Search

Parameter search.

Every searcher takes an eval callback rather than a strategy and a data source. That keeps the search algorithms independent of what is being evaluated — walk-forward passes an eval closed over one fold's data, cross-validation passes one closed over one split, and the unit tests pass a closed-form function with a known optimum.

Evaluation runs through Montecarlo.Pool, indexed by trial number, so results do not depend on how many Domains ran the search. Seed each trial's backtest from substream ~root_seed ~index:trial and the whole search reproduces.

module Rng = Algostream_rng.Rng
type trial = {
  1. index : int;
  2. params : (string * float) list;
  3. metrics : Metrics.t option;
    (*

    None when the evaluation failed

    *)
  4. score : float;
    (*

    neg_infinity on failure

    *)
  5. error : string option;
}
type report = {
  1. objective : string;
  2. trials : trial array;
    (*

    in trial-index order, never in completion order

    *)
  3. best : trial option;
  4. n_evaluated : int;
  5. n_failed : int;
    (*

    Standard deviation of scores across trials — the input Overfitting.deflated_sharpe_ratio needs for trial_sharpe_stdev.

    *)
  6. score_stdev : float;
}
type eval = (string * float) list -> Metrics.t

Shared primitives

Exposed so Genetic builds its report exactly the way every other strategy here does. A second copy of this logic would be free to drift, and the fields it fills — n_evaluated above all — are what Overfitting.deflated_sharpe_ratio charges a search for.

val evaluate : points:(string * float) list array -> objective:Objective.t -> eval:eval -> n_domains:int -> trial array

Score a batch of points through the pool. Results come back in point order whatever order they finished in, and a point whose evaluation raised becomes a failed trial rather than taking the batch down.

val assemble : objective:Objective.t -> trials:trial array -> report

Summarise trials into a report: best non-failed trial, failure count, score dispersion.

val grid : space:Search_space.t -> objective:Objective.t -> eval:eval -> n_domains:int -> max_points:int -> (report, [ `Too_large of int ]) Stdlib.result

Exhaustive grid. Returns `Too_large n rather than sampling a subset, so a caller never mistakes a partial sweep for a full one.

val random : space:Search_space.t -> objective:Objective.t -> eval:eval -> n_domains:int -> n:int -> root_seed:int64 -> report

Uniform random search over n points.

val stratified : space:Search_space.t -> objective:Objective.t -> eval:eval -> n_domains:int -> n:int -> root_seed:int64 -> report

Latin-hypercube search: same budget as random, exact marginal coverage. Prefer it as the default sweep; see Search_space.stratified_points for what it does and does not guarantee.

val coordinate_descent : space:Search_space.t -> objective:Objective.t -> eval:eval -> x0:(string * float) list -> max_passes:int -> report

Coordinate descent from x0: repeatedly step to the best grid neighbour until no neighbour improves, or max_passes is spent. Cheap local polish on a coarse grid winner.

val nelder_mead_refine : space:Search_space.t -> objective:Objective.t -> eval:eval -> x0:(string * float) list -> (report, [ `Too_many_dimensions of int ]) Stdlib.result

Local refinement via the existing Advanced_models.Nelder_mead.

Hard limit of 4 continuous dimensions, which is Nelder_mead's own documented bound — above that it degrades badly, so this returns Error `Too_many_dimensions rather than running and quietly reporting a poor optimum as if it were a good one. Use it to polish the best point from a grid or Latin-hypercube sweep, never as a global search.

val report_to_string : report -> string