Clone our Wealth-Lab 8 Extension Demo project on GitHub to get a head start in developing your own Extensions!

Optimizer API

An Optimizer Extension controls how WealthLab 9 tests Strategy Parameter values during an optimization. The built-in Exhaustive Optimizer evaluates every possible combination of enabled Parameter values. Depending on the number of Parameters and their ranges, this can result in a very large number of backtests. Other Optimizers can use different techniques to reduce the search space or concentrate on more promising Parameter combinations. Each Strategy Parameter defines an optimization range using:

  • MinValue
  • MaxValue
  • StepValue

The Optimizer determines which combinations of these values should be tested and in what order.

Build Environment

You can create an Optimizer in a .NET development tool such as Visual Studio 2026. Create a class library project that targets .NET10, then reference the WealthLab.Core library DLL that you'll find in the WL9 installation folder.

Your Optimizer will be a class in this library that descends from OptimizerBase, which is defined in the WealthLab.Core library, in the WealthLab.Backtest namespace. After you implement and build your library, simply copy the resulting assembly DLL into the WL9 installation folder. The next time WL9 starts up, it will discover your Optimizer, making it available in appropriate locations of the WL9 user interface.

Visual Studio 2026 Build Environment

Accessing the Host (WL9) Environment

The IHost interface provides access to the current WealthLab environment. Extensions can use it to retrieve application-level information and services, such as the location of the user's WealthLab data folder or the DataSets defined by the user.

You can access the current IHost instance from anywhere in your extension through the WLHost singleton and its Instance property. For example, the following code retrieves the path to the user's WealthLab data folder:

string folder = WLHost.Instance.DataFolder;

Use WLHost.Instance whenever your extension needs access to functionality exposed by the IHost interface.

Configuring an Optimizer

OptimizerBase derives from Configurable, which provides the standard WealthLab configuration framework. By default, OptimizerBase uses a ConfigurableType of ParameterListType, so the Optimizer itself can expose configurable Parameter instances. Define these Parameters by overriding GenerateParameters, as described in the Configurable class reference. These are separate from the Strategy Parameters being optimized. Optimizer Parameters can control behavior such as population size, iteration count, convergence settings, or the Performance Metric used to evaluate candidate runs.

Permutations

public virtual int GetTotalPermutations(ParameterList pl)

Override this method to return the total number of optimization runs your Optimizer expects to perform. The supplied ParameterList contains the Strategy Parameters being optimized. The result can be exact or estimated, depending on the nature of your optimization algorithm. The calculation will typically depend on both:

  • The ranges of the Strategy Parameters in pl
  • The configuration Parameters of your Optimizer

WealthLab can use this value when reporting optimization progress.

Initialization

public virtual void Initialize(
    StrategyOptimizer mo,
    ParameterList pl)

Override this method to perform initialization before an optimization begins. The StrategyOptimizer supplied in mo provides the functionality your Optimizer uses to execute individual optimization runs. The supplied ParameterList contains the Strategy Parameter values and optimization ranges. If you override Initialize, be sure to call the base implementation:

base.Initialize(mo, pl);

Running the Optimization

public abstract void Optimize(
    ParameterList pl,
    bool resumePrevious)

Override this method to implement the Optimizer's search algorithm. Your Optimizer determines which Parameter combinations should be tested, assigns those values to the Parameters in pl, and executes each run through StrategyOptimizer.ExecuteOptimizationRun. When assigning numeric Parameter values, use the Parameter's SetNumericValue method:

runPL[index].SetNumericValue(value);

This is preferable to assigning directly to Value because it correctly handles the Parameter's numeric type.

ExecuteOptimizationRun

Use:

StrategyOptimizer.ExecuteOptimizationRun(pl);

to execute a backtest with the supplied Parameter values. The method returns an OptimizationResult containing the Performance Metrics generated by the run. The available metrics correspond to the Performance Metrics selected by the user. For example:

OptimizationResult result =
    StrategyOptimizer.ExecuteOptimizationRun(runPL);

Your Optimizer can use the returned result when deciding which Parameter combinations to explore next.

Resuming a Previous Optimization

The resumePrevious parameter is true when WealthLab is resuming a previously saved or paused optimization. If your Optimizer maintains its own search state, use this flag together with the internal state methods described later to continue from the appropriate point instead of starting over.

Detecting Cancellation

public bool IsCancelled

Check this property regularly while your optimization algorithm is running. If it becomes true, the user has canceled the optimization and your Optimizer should stop processing as soon as practical.

For example:

if (IsCancelled)
    return;

Long-running loops and recursive algorithms should check IsCancelled frequently.

Accessing StrategyOptimizer

public StrategyOptimizer StrategyOptimizer

Returns the StrategyOptimizer associated with the current optimization. Your Optimizer uses this instance primarily to execute backtest runs:

StrategyOptimizer.ExecuteOptimizationRun(pl);

It also provides other optimization-related functionality, such as progress reporting.

Working with Performance Metrics

Optimizers can access the Performance Metrics selected by the user through:

ScoreCardFactory.Instance.SelectedMetrics

This returns a list of strings containing the selected metric names. An Optimizer can use these metrics, for example, to allow the user to select which Performance Metric its algorithm should maximize or minimize.

ScoreCardChanged

protected virtual void ScoreCardChanged()

Override this method if your Optimizer needs to respond when the user changes the selected Performance Metrics. The method retains the historical ScoreCardChanged name for compatibility.

SetMetrics

protected void SetMetrics(Parameter p)

Call this helper method to configure one of your Optimizer Parameters as a Performance Metric selector. The method changes the supplied Parameter to a StringChoice and populates its Choices with the currently selected metrics from:

ScoreCardFactory.Instance.SelectedMetrics

A typical Optimizer might call SetMetrics during initialization:

SetMetrics(Parameters[0]);

and again from ScoreCardChanged so the choices remain synchronized when the user's selected metrics change.

Internal State

More sophisticated Optimizers can maintain internal state while searching through Parameter combinations. For example, an Optimizer might track:

  • A current population
  • Best-performing Parameter combinations
  • A search region
  • Iteration numbers
  • Random generator state
  • Previously evaluated candidates

Because optimizations can be saved, paused, and resumed, OptimizerBase provides methods for storing and restoring this state.

GetInternalState

public virtual string GetInternalState()

Override this method to return a string containing everything required to restore the Optimizer's current internal state. WealthLab stores this string with the saved optimization. You can use any serialization format appropriate for your implementation.

SetInternalState

public virtual void SetInternalState(
    string s,
    ParameterList pl)

Override this method to restore the Optimizer's internal state from the supplied string. The pl parameter contains the Strategy Parameters associated with the resumed optimization.

Duplicate Runs

Even if your Optimizer does not persist its own state, WealthLab can avoid some redundant work. If StrategyOptimizer.ExecuteOptimizationRun is called with the same Parameter values as an OptimizationResult already present in the optimization results, WealthLab can return the existing result rather than executing another identical backtest.

Parallel Processing Considerations

If your Optimizer executes multiple optimization runs concurrently, take particular care when working with ParameterList instances. ParameterList is mutable, so multiple threads should not modify the same instance. Clone the ParameterList before changing Parameter values:

ParameterList runPL = pl.Clone();

Then modify and pass the clone to ExecuteOptimizationRun.

For example:

ParameterList runPL = pl.Clone();
runPL[index].SetNumericValue(value);
StrategyOptimizer.ExecuteOptimizationRun(runPL);

This prevents Parameter values from different optimization runs from interfering with one another. Even in a single-threaded Optimizer, cloning ParameterLists can make recursive or nested optimization logic safer and easier to reason about.

Reporting Progress

An Optimizer can report its estimated completion percentage through the StrategyOptimizer. For example:

StrategyOptimizer.ReportEstimatedCompletion(
    completedRuns * 100.0 / totalRuns);

This is especially useful when GetTotalPermutations provides a meaningful estimate of the total number of runs.

Example: Non-Parallel Exhaustive Optimizer

The following example implements a simple exhaustive Optimizer that runs each Parameter combination sequentially on a single thread. It recursively walks through every enabled Strategy Parameter and executes a backtest for every possible value.

using System.Collections.Generic;
using WealthLab.Core;

namespace WealthLab.Backtest
{
    public class ExhaustiveNonParallel : OptimizerBase
    {
        public override string Name =>
            "Exhaustive (non-Parallel)";

        public override string Description =>
            "Executes the Strategy on each permutation " +
            "of parameter values, but does not leverage " +
            "parallel processing.";

        public override void Optimize(
            ParameterList pl,
            bool resumePrevious)
        {
            totalRuns = GetTotalPermutations(pl);
            runs = 0;

            ProcessParameter(pl, 0);
        }

        private int totalRuns;
        private int runs;

        private void ProcessParameter(
            ParameterList pl,
            int depth)
        {
            ParameterList myPL = pl.Clone();

            if (depth == myPL.Count)
            {
                StrategyOptimizer.ExecuteOptimizationRun(
                    myPL);

                runs++;

                StrategyOptimizer.ReportEstimatedCompletion(
                    runs * 100.0 / totalRuns);

                return;
            }

            if (myPL[depth].IsChecked)
            {
                List<double> values =
                    new List<double>(myPL[depth]);

                foreach (double value in values)
                {
                    if (IsCancelled)
                        return;

                    ParameterList runPL =
                        myPL.Clone();

                    runPL[depth].SetNumericValue(
                        value);

                    ProcessParameter(
                        runPL,
                        depth + 1);
                }
            }
            else
            {
                if (!IsCancelled)
                {
                    ProcessParameter(
                        myPL,
                        depth + 1);
                }
            }
        }
    }
}

This example demonstrates the basic responsibilities of an Optimizer:

  • Determine the Parameter combinations to evaluate.
  • Clone ParameterLists before modifying them.
  • Assign Parameter values with SetNumericValue.
  • Execute runs through StrategyOptimizer.ExecuteOptimizationRun.
  • Check IsCancelled during processing.
  • Report estimated progress through ReportEstimatedCompletion.

More advanced Optimizers can replace the exhaustive search with genetic algorithms, stochastic searches, adaptive searches, or other techniques while using the same underlying StrategyOptimizer interface.