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

Optimizer API

This document details the API for building Optimizer extensions for Wealth-Lab 8. An Optimizer decides how Strategy Parameter values should be tested during an optimization.

The baseline Optimizer, Exhaustive, simply iterates through each possible combination of Parameter values. As you might imagine, this can lead to run times exceeding hours, days, weeks, months or more depending on how many Parameter combinations a Strategy has. Other Optimizers attempt a different approach to narrow down profitable Parameter ranges.

Each Parameter instance has a MinValue, MaxValue, and StepValue property that determines the possible range of values that should be considered during an optimization. An Optimizer orchestrates the optimization, determining how the Parameter values change from run to run during the optimization process.

Build Environment

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

Note: If you are using Visual Studio 2022, it will need to be updated to at least version 17.8.6 to use .NET8.

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 WL8 installation folder. The next time WL8 starts up, it will discover your Optimizer, making it available in appropriate locations of the WL8 user interface.

Visual Studio 2022 Build Environment

Accessing the Host (WL8) Environment

The IHost interface allows your extension to access information about the user's WealthLab environment. For example, the location of the user data folder, or obtaining a list of DataSets defined by the user. At any point in your extension's code base, you can access an instance of the IHost interface using the singleton class WLHost and its Instance property. Example:

//get user data folder
string folder = WLHost.Instance.DataFolder;

Configuration of an Optimizer

OptimizerBase descends from the base class Configurable, which provides a way to allow the user to configure the Broker Adapter. Consult the Configurable class reference for details. By default, OptimizerBase assigns the ParameterListType value to its ConfigurableType property, so the OptimizerBase will use a ParameterList containing Parameter instances for configuration. You'll define these Parameters in the GenerateParameters method, as described in the Configurable class reference.

Permutations

public virtual int GetTotalPermutations(ParameterList pl)

Override this method to return the total (or at least estimated) number of optimization runs that your Optimizer will need to perform based on the Strategy Parameters instances passed in the ParameterList pl parameter. The answer will likely be based on a combination of these Strategy parameters, and your Optimizer's own Parameters values.

Optimizing

public virtual void Initialize(StrategyOptimizer mo, ParameterList pl)

Override this method to perform any initialization your Optimizer might require. This is called prior to each optimization run. Your Optimizer is passed an instance of the StrategyOptimizer class, which contains methods you'll call to perform an optimization run. It's also passed a ParameterList instance that contains the Optimizer's Parameter values configured by the user.

Note: be sure to call base.Initialize if you do implement this method.

public abstract void Optimize(ParameterList pl, bool resumePrevious)

Override this method to perform the optimization. See the example code below for a minimal implementation of an exhaustive Optimizer. Your Optimizer will use its bespoke logic to come up with parameter value combinations, and execute optimization runs by calling StrategyOptimizer.ExecuteOptimizationRun.

You specify which parameter values to test in an optimization run by assigning their values to the Value properties of each Parameter instance in the ParameterList pl parameter. The safest way to assign a parameter value is by using the SetNumericValue method of the Parameter class. In the example code below you'll see this happen in the middle of the ProcessParameter method.

ExecuteOptimizationRun returns an instance of the OptimizationResult class, which provides all of the calculated performance metrics for the run. The performance metrics returned are based on which metrics the users selected in WL8.

If resumePrevious true, this indicates that the optimization is resuming a previously saved or paused run. See the section on Internal State below for ideas on how to handle resuming previous runs in your Optimizers.

public bool IsCancelled

During your optimization process loops, you should check the value of the IsCancelled property, and fall out if it is true. This indicates that the user cancelled the optimization.

public StrategyOptimizer StrategyOptimizer

Returns the instance of the StrategyOptimizer that your Optimizer uses to execute optimization runs. Specifically, your Optimizer should call the ExecuteOptimizationRun to execute a run.

Working with Performance Metrics

Optimizers can determine which Performance Metrics the user has selected in the Preferences tool by examining the ScoreCardFactory.Instance SelectedMetrics property. This property returns a list of strings containing the selected metric names.

protected virtual void ScoreCardChanged()

You can override this method to take some action when the user changes the selected Performance Metrics in WL8's Preferences tool. The method is named ScoreCardChanged rather than MetricsChanged in order to retain backward compatibility.

protected void SetMetrics(Parameter p)

You can call this method to set up one of your Optimizer's Parameters to be able to select one of the selected Performance Metrics. It sets the Parameter to be of type StringChoice, and fills its Choices list with the values from ScoreCardFactory.Instance.SelectedMetrics. Typically you'd call this method during your Optimizer's Initialize method, and in ScoreCardChanged, to respond to a change in selected metrics.

Internal State

Your Optimizer might maintain some internal state as it processes Parameter values, zeroing in on profitable ranges. Since optimizations can be saved, paused, and resumed, you can use the following methods to cause the internal state of your optimizations to be saved inside the optimization save file.

If you don't utilize this method, WL8 will still employ some of its own optimizations to eliminate superfluous calls to ExecuteOptimzationRun. If WL8's StrategyOptimizer sees a result with the same Parameter values already in its result list, it will return that instance instead of executing another backtest run with the same set of parameters.

public virtual string GetInternalState()

Override this method to return a string that represents the complete internal state of your Optimizer.

public virtual void SetInternalState(string s, ParameterList pl)

Override this method to restore your Optimizer's internal state based on the string passed in the s parameter.

Parallel Processing Considerations

If your Optimizer will leverage multiple cores and execute code in a parallel processing mode, take particular care with the ParameterList instance that gets passed around in the pl parameter. The first thing you should do in a method that accepts a ParameterList instance is to use its Clone method to create a copy of it, and then work with that copy. Using this method will ensure that ParameterList instances don't get corrupted or mixed up when runs occur at the same time on multiple threads.

In the example code below, you'll see this as the first line in the ProcessParameter method. Since this example Optimizer is not multi-threaded, this Clone call is strictly not required, but we added it here as an example of best practice for parallel-processing Optimizers.

Example

Below is the source code for a minimal exhaustive Optimizer that runs on a single thread. This ExhaustiveNonParallel Optimizer is included in WL8 for comparison purposes. Try pitting it against the Exhaustive Optimizer, which leverages parallel processing to optimize on multiple cores, and see the difference in speed.

Make sure to rename the class before compiling:

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

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

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

        //execute the optimization
        public override void Optimize(ParameterList pl, bool resumePrevious)
        {
            totalRuns = GetTotalPermutations(pl);
            runs = 0;
            ProcessParameter(pl, 0);
        }

        //private members
        private int totalRuns;
        private int runs;

        //iterate through parameter values of the specified parameter (recursive)
        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;
            }

            //are we optimizing this parameter?
            if (myPL[depth].IsChecked)
            {
                //yes, create list of step values
                List<double> values = new List<double>(myPL[depth]);

                //and optimize in parallel
                //for (int index = 0; index < values.Count; index++)
                foreach (double myValue in values)
                {
                    if (!IsCancelled)
                    {
                        //set value
                        ParameterList runPL = myPL.Clone();
                        runPL[depth].SetNumericValue(myValue);

                        //continue to inner parameter
                        ProcessParameter(runPL, depth + 1);
                    }
                }
            }
            else
            {
                //no, use established value
                if (!IsCancelled)
                {
                    ProcessParameter(myPL, depth + 1);
                }
            }
        }
    }
}