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

Strategy Gene Extension API

This document details the API for building Strategy Genes for Wealth-Lab 8 Strategy Genetic Evolver. The Genetic Evolver uses these Genes to randomly generate and mutate trading Strategies over a number of generations.

Build Environment

You can create a Strategy Gene 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 Strategy Gene will be a class in this library that descends from StrategyGeneBase, 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 Strategy Gene, 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;

Gene Name and Code

public abstract string Name

Override the Name property to return a descriptive name for your Gene.

public abstract string ShortCode

Return a short string code to identify the Gene. The Evolver displays all of a Genetic Strategy's Genes as a concatenation of their Codes.

Gene Parameters

public virtual string GeneData

You can use standard .NET class properties to store the values of your Gene's parameters. Override the GeneData property getter and setter method to package your Gene's properties into a single string. WL8 uses this string to persist the Gene's information.

Example

If your Gene has two properties Value (double) and Lookback (int), your GeneData implementation might look like this:

public override string GeneData
{
    get
    {
        return Value.ToString(CultureInfo.InvariantCulture) + ";" + Lookback.ToString();
    }
    set
    {
        string[] tokens = value.Split(';');
        Value = Double.Parse(tokens[0], CultureInfo.InvariantCulture);
        Lookback = Int32.Parse(tokens[1]);
    }
}

Gene's Relation to Building Block

public abstract BuildingBlockBase GetBuildingBlock()

Each Strategy Gene will ultimately inject a single Building Block into the Genetic Strategy. The injected Building Blocks can be either Entry/Exit or Condition Blocks. The Building Block inserted is determined by the return value of GetBuildingBlock.

The requirement to create and return an instance of a class derived from BuildingBlockBase here implies you have a certain familiarity with the Building Block classes available in WL8, and how their Parameters are laid out. Generally, you can load a Building Block in the Strategy Designer and determine their Parameter layout by observing the sequence of parameters in the Designer.

After determining which Building Block to inject, create the instance of the Building Block, and then assign values to its Parameters based on the parameter properties contained in your Strategy Gene.

Instead of injecting one of WL8's Building Blocks, you can also inject one of your own custom Building Blocks. A typical implmentation will create an extension library containing both a StrategyGene class as well as a BuildingBlockBase derived class.

Example

In the example below, the WL8 SellAfterNBars Gene injects a WL8 SellAfterNBars Building Block:

public override BuildingBlockBase GetBuildingBlock()
{
    SellAfterNBars bb = new SellAfterNBars(); //This is the WL8 Building Block
    bb.Parameters[0].Value = NumBars;
    AddConditionsBlocks(bb); //See Entry and Exit Genes below for explanation
    return bb;
}

Randomization and Mutation

public virtual void Initialize()

WL8 calls this method when the Gene is being initialized in a new Genetic Strategy. You can perform any initialization required here.

public virtual void Randomize

Override this method to completely randomize the parameter property values of your Gene.

public virtual void Mutate(GeneticStrategy gs)

Override this method to mutate the Gene, meaning to change one (or at least a small number) of its property parameter values. If your Gene is an Entry/Exit Gene, call the base.Mutate method, which causes the Condition Genes contained within the Entry/Exit to also Mutate.

protected internal static Random RNG

A static instance of the .NET Random class that you can employ to more easily generate random numbers for your randomization and mutation.

Example

In the example below, the WL8 SellAfterNBars Gene mutates the property parameter that determines how many bars to sell the position. Note the RandomizeValue method is described in Helper Methods, below.

public override void Mutate(GeneticStrategy gs)
{
    NumBars = RandomizeValue(NumBars);
}

Entry and Exit Genes

public virtual bool IsEntry
public virtual bool IsExit

Override one of these properties and return true to indicate that your Gene is an Entry or Exit, respectively.

public bool IsEntryExit 

This property resolves to true if either IsEntry or IsExit returns true.

public virtual PositionType PositionType

Override this property to return the PositionType that your Entry/Exit handles. Possible values are PositionType.Long or PositionType.Short.

protected void AddConditionsBlocks(BuildingBlockBase bb)

In your implementation of GetBuildingBlock, call this method, passing the BuildingBlockBase instance that you created, to cause randomized Condition Genes to be injected.

public virtual bool CanAddExit(List<StrategyGeneBase> exits)

For Exit Blocks, optionally override this method to return false if the Exit Gene should not be added to the Genetic Strategy. You are passed a list of the already injected Exit Genes in the exits parameter.

Example

In the example below, the SellAtStopLoss Gene returns false in CanAddExit if there is already at least one other SellAtStopLoss Gene already added to the Genetic Strategy. It also checks to ensure that there is at least one other exit in place, to avoid having a stop loss be the only exit possibility.

public override bool CanAddExit(List<StrategyGeneBase> exits)
{
    if (exits.Count == 0)
        return false;
    foreach (StrategyGeneBase exit in exits)
        if (exit is SellAtStopLossGene)
            return false;
    return true;
}

Inserting Conditions

protected void InsertConditions(int max, StrategyGeneBase parentGene, StrategyGeneConditionTypes conditionType)

Call this method in your Randomize implementation to cause one or more random Condition Genes to be added to your Entry/Exit. You specify the maximum number of Condition Genes to inject with the max parameter, and the type(s) of Condition Genes desired (see below) in the conditionType parameter. Pass this as the value for the parentGene parameter.

Condition Genes

public bool IsCondition

If you did not return true in either IsEntry or IsExit, your Strategy Gene will be considered a Condition Gene, and this property will resolve to true.

public virtual StrategyGeneConditionTypes ConditionType

The Evolver recognizes two types of Condition Genes, Signal and Filter. Override this property to indicate which type your Gene represents. Possible values are:

  • StrategyGeneConditionTypes.Signal - Use this value for Condition Genes that represent a primary signal, such as a moving average crossover, or an oscillator crossing into oversold/overbought territory. When the Evolver first adds a Condition Gene to an Entry/Exit Gene, it will pick a Signal Condition.
  • StrategyGeneConditionTypes.Filter - Use this value for Condition Genes that can be used as filters for other signals, such as an indicator being above or below another indicator or value. The Evolver can add Filter Condition Genes to an Entry/Exit Gene that already has a Signal Condition.
  • StrategyGeneConditionTypes.Both - Use this value if your Condition Gene could be considered either a Signal or a Filter.
public virtual bool CanIncludeCondition

In some situations, it does not make sense to include a Condition Gene. For example, we would not want to inject the Chart Patterns Condition Gene if the user has somehow deleted all of their Chart Patterns. You can optionally override this method to return true if your Condition Gene should not be injected for any reason.

Restricting Genes

public virtual bool IsValid(GeneticStrategy gs, StrategyGeneBase parent)

Override the IsValid method to indicate whether the Strategy Gene would be valid to insert given the current state of the Genetic Strategy (passed in the gs parameter.) If it is a Condition Gene that is being queried, the parent parameter will contain the Strategy Gene instance that corresponds to the parent Entry/Exit Gene.

Working with Indicator Parameters

Many Building Blocks use Indicator Parameters to allow the user to specify particular Indicators to base their rules on. Rather than setting the Value of the Building Block Parameter that represents an Indicator, you'll need to set the Parameter's IndicatorAbbreviation and IndicatorParameters properties.

Example

In this example, the WL8 OscillatorGene has a string property called OscillatorAbbreviation that contains the abbreviation of the Indicator used, and a ParameterList property called OscillatorParameters that contains its parameters. In its GetBuildingBlock implementation, it assigns the values to the Building Block's Indicator parameter like this:

//Create an instance of the Building Block
//IndicatorValue is the class the implements the Indicator Compare to Value BB
IndicatorValue bb = new IndicatorValue();

//parameter 0 - indicator
Parameter p = bb.Parameters[0];
p.IndicatorAbbreviation = OscillatorAbbreviation;
p.IndicatorParameters = OscillatorParameters;

You can use helper methods in the IndicatorFactory class to get random Indicator instances to assign in your Randomize method. Use the IndicatorFactory.Instance to gain access to these properties. The properties return an instance of a RandomList<IndicatorBase> object, and you can use its RandomValue property to pull a random Indicator.

  • FastIndicators - Returns a RandomList of Indicators that are appropriate to use in the Evolver, filtering out the ones that have been flagged as slow performers.
  • Smoothers - Returns a RandomList of "Smoother" Indicators. Smoothers are indicators like SMA and EMA that take another TimeSeries as source input and smooth the source data.
  • Oscillators - Returns a RandomList of "Oscillator" Indicators. Oscillators have defined OverboughtLevel and OversoldLevel property values.

After obtaining an IndicatorBase instance from the IndicatorFactory, you can assign its Abbreviation and its Parameters to local properties, and ultimately to the IndicatorAbbreviation and IndicatorParameters properties of the Building Block Parameter instance that represents an Indicator.

When storing the Indicator Parameters, use the Clone method to make a copy of the ParameterList instead of assigning the actual instance from the IndicatorBase returned from the IndicatorFactory. See how this is done in the example code at the end of this document.

Coordinating Genes

Using the classes and properties described below you can coordinate Genes to accomplish things like ensuring the same Oscillator is used for both an oversold Entry Gene and an overbought Exit Gene.

public GeneticStrategy ParentStrategy

The ParentStrategy property returns an instance of the GeneticStrategy class that represents the Genetic Strategy currently being constructed. The GeneticStrategy class has a few notable properties/methods useful for coordinating Genes:

  • Cache - Returns a Dictionary<string, object> that you can employ to track information during the construction of the Genetic Strategy
  • EntryExits - Returns a List containing the Strategy Genes for the Entries and Exits already added to the Strategy
  • Entries - Returns a List containing the Strategy Genes for the Entries already added to the Strategy
  • GetExits(StrategyGeneBase entry) - Returns a List containing the Strategy Genes for the Exits that correspond to the Entry passed in the entry parameter
  • MostRecentEntryExit - Returns the most recent Entry/Exit Gene that was added to the Strategy
  • MostRecentEntry - Returns the most recent Entry Gene that was added to the Strategy
public StrategyGeneBase ParentGene

For Condition Genes, will return the instance of the Gene that represents this Condition's parent Entry/Exit.

public List<StrategyGeneBase> ChildGenes

Returns a List containing the instances of the Genes that represent the Conditions already added to an Entry/Exit Gene.

Example

The OscillatorGene in WL8 uses these coordination techniques to try and match the Indicator used in an Exit Gene with the same Indicator used in a previously added Condition of an Entry Gene.

public override void Randomize()
    {
        //if this is an exit condition, attempt to pair a previous entry oscillator
        if (RNG.NextDouble() > 0.5 && ParentGene.IsExit)
        {
            StrategyGeneBase recentEntry = ParentStrategy.MostRecentEntry;
            if (recentEntry != null)
            {
                if (ParentStrategy.Cache.ContainsKey(Name))
                {
                    OscillatorGene entryOscGene = (OscillatorGene)ParentStrategy.Cache[Name];
                    OscillatorAbbreviation = entryOscGene.OscillatorAbbreviation;
                    OscillatorParameters = entryOscGene.OscillatorParameters.Clone();
                    AboveOrBelow = entryOscGene.AboveOrBelow == AboveBelow.Above ? AboveBelow.Below : AboveBelow.Above;
                    IndicatorBase ind = IndicatorFactory.Instance.Find(OscillatorAbbreviation);
                    Value = ind.ReverseTargetValue(entryOscGene.Value);
                    return;
                }
            }
        }

        //don't match with the entry oscillator
        IndicatorBase osc = IndicatorFactory.Instance.Oscillators.RandomValue;
        OscillatorAbbreviation = osc.Abbreviation;
        OscillatorParameters = osc.Parameters;
        RandomizeParameters(OscillatorParameters);
        AboveOrBelow = RNG.NextDouble() > 0.5 ? AboveBelow.Above : AboveBelow.Below;
        if (RNG.NextDouble() > 0.6)
        {
            Value = osc.MidpointLevel;
            double range = Math.Abs(osc.OverboughtLevel - osc.OversoldLevel);
            range *= 0.2;
            double adjust = RNG.NextDouble() * range - (range / 2.0);
            Value += adjust;
        }
        else
        {
            Value = RNG.NextDouble() > 0.5 ? osc.OversoldLevel : osc.OverboughtLevel;
            Value = RandomizeValue(Value);
        }

        //small chance it should use an external index symbol
        if (RNG.NextDouble() > 0.9)
            IndexSymbol = RandomIndexSymbol;

        //if this is an entry, cache the indicator info for future pairing with an exit
        if (ParentGene.IsEntry)
            ParentStrategy.Cache[Name] = this;
    }

Associating a Building Block with a Gene

public virtual StrategyGeneBase ConvertGeneFromBB(ConditionBuildingBlock bb)

WL8 can construct Genetic Strategies from Building Block Strategies that users drag and drop into slots 6 - 10 of the Genetic Evolver Preferences page. For each Building Block, WL8 calls its GetGene method to see if a Gene is available from the Building Block. If the Building Block returns a Gene instance, WL8 then calls ConvertGeneFromBB on this instance to obtain a Gene populated with the current parameter values of the Building Block.

If you want your custom Gene to integrate with this process, you'll need a corresponding custom Building Block that implements GetGene, returning an instance of your Gene. You'll also need to implement ConvertGeneFromBB in your Gene, populating your Gene's property parameters based on the values of the incoming Building Block in the bb parameter.

Helper Properties and Methods

public static double RandomizeValue(double value, double pct = 10.0)

Returns a randomized double value based on the source value, with a maximum amount of randomization controlled by the pct parameter.

public static int RandomizeValue(int value)

Returns a randomized int value based on the source value, with a maximum amount of 10% randomization.

public static void RandomizeParameters(ParameterList pl)

Randomizes the values of the Parameter instances in the ParameterList passed in pl. Will randomize Parameter instances of types PriceComponent, Int32, Double, and StringChoice.

public static PriceComponent RandomPriceComponent

Returns a random PriceComponent, possible values include Open, High, Low, Close, and some of the various "averaging" PriceComponents available in the WL8 framework. Gives highest weight to Close, followed by Open/High/Low.

public string RandomIndexSymbol

Returns a random symbol appropriate for use as a market index or secondary symbol.

Example

Below is the full source code for the WL8 PriceCompareIndicatorGene class. This Gene injects the Indicator Compare to Indicator Building Block and compares one of the BarHistory's Price Components (OHLC) to another Price Component, or to a randomly selected Indicator that plots on the Price pane.

using WealthLab.Core;
using WealthLab.Indicators;

namespace WealthLab.Backtest
{
    //This gene compares price to another price, or an indicator that is plottable on the price pane
    public class PriceCompareIndicatorGene : StrategyGeneBase
    {
        //gene name
        public override string Name => "Price Comparisons";

        //Short Code
        public override string ShortCode => "E";

        //filter, not signal
        public override StrategyGeneConditionTypes ConditionType => StrategyGeneConditionTypes.Filter;

        //price component to compare
        public PriceComponent Price { get; set; }

        //price component to compare against
        public PriceComponent PriceCompareTo { get; set; }

        //above or below?
        public AboveBelow AboveOrBelow { get; set; }

        //indicator to compare against
        public string IndicatorAbbreviation { get; set; } = "";
        public ParameterList IndicatorParameters { get; set; } = new ParameterList();

        //bars ago?
        public int BarsAgo
        {
            get
            {
                return _barsAgo;
            }
            set
            {
                if (value < 0)
                    value = 0;
                _barsAgo = value;
            }
        }

        //randomize
        public override void Randomize()
        {
            Price = RandomPriceComponent;
            AboveOrBelow = RNG.NextDouble() > 0.5 ? AboveBelow.Above : AboveBelow.Below;
            PriceCompareTo = RandomPriceComponent;
            while (PriceCompareTo == Price)
                PriceCompareTo = RandomPriceComponent;
            if (RNG.NextDouble() > 0.5)
            {
                IndicatorBase ind = IndicatorFactory.Instance.FastIndicators.RandomValue;
                while (ind.PaneTag != "Price")
                    ind = IndicatorFactory.Instance.FastIndicators.RandomValue;
                IndicatorAbbreviation = ind.Abbreviation;
                IndicatorParameters = ind.Parameters.Clone();
                RandomizeParameters(IndicatorParameters);
            }
            BarsAgo = RNG.Next(5) + 1;
            if (IndicatorAbbreviation != "")
                BarsAgo--;
        }

        //mutate
        public override void Mutate(GeneticStrategy gs)
        {
            int num = RNG.Next(4);
            if (num == 0)
            {
                Price = RandomPriceComponent;
                while (Price == PriceCompareTo)
                    Price = RandomPriceComponent;
            }
            else if (num == 1)
            {
                if (IndicatorAbbreviation == "")
                {
                    PriceCompareTo = RandomPriceComponent;
                    while (Price == PriceCompareTo)
                        PriceCompareTo = RandomPriceComponent;
                }
                else
                    RandomizeParameters(IndicatorParameters);
            }
            else if (num == 2)
                BarsAgo = RandomizeValue(BarsAgo);
            else
                AboveOrBelow = AboveOrBelow.Opposite();
        }

        //create the building block
        public override BuildingBlockBase GetBuildingBlock()
        {
            IndicatorCompareIndicator ici = new IndicatorCompareIndicator();
            ici.Parameters[0].Value = Price.ToString();
            ici.Parameters[0].IndicatorAbbreviation = Price.ToString();
            ici.Parameters[1].Value = AboveOrBelow.AsLessThanGreaterThan();
            if (IndicatorAbbreviation != "")
            {
                ici.Parameters[2].Value = IndicatorAbbreviation;
                ici.Parameters[2].IndicatorAbbreviation = IndicatorAbbreviation;
                ici.Parameters[2].IndicatorParameters = IndicatorParameters;
            }
            else
            {
                ici.Parameters[2].Value = PriceCompareTo.ToString();
                ici.Parameters[2].IndicatorAbbreviation = PriceCompareTo.ToString();
            }
            ici.Parameters[3].SetNumericValue(BarsAgo);
            return ici;
        }

        //create a gene (if possible) from a condition building block
        public override StrategyGeneBase ConvertGeneFromBB(ConditionBuildingBlock bb)
        {
            if (bb is IndicatorCompareIndicator)
            {
                IndicatorCompareIndicator ici = bb as IndicatorCompareIndicator;
                if (ici.Qualifier != null)
                    return null;
                if (!PriceComponentExtensions.IsPriceComponent(ici.Parameters[0].IndicatorAbbreviation))
                    return null;
                PriceCompareIndicatorGene pci = new PriceCompareIndicatorGene();
                pci.Price = PriceComponentExtensions.StringToPriceComponent(ici.Parameters[0].IndicatorAbbreviation);
                pci.AboveOrBelow = ici.Parameters[1].AsString == "greater than" ? AboveBelow.Above : AboveBelow.Below;
                if (PriceComponentExtensions.IsPriceComponent(ici.Parameters[2].IndicatorAbbreviation))
                    pci.PriceCompareTo = PriceComponentExtensions.StringToPriceComponent(ici.Parameters[2].IndicatorAbbreviation);
                else
                {
                    pci.IndicatorAbbreviation = ici.Parameters[2].IndicatorAbbreviation;
                    pci.IndicatorParameters = ici.Parameters[2].IndicatorParameters;
                }
                pci.BarsAgo = ici.Parameters[3].AsInt;
                return pci;
            }
            return null;
        }

        //private members
        private int _barsAgo = 1;
    }
}