Anatomy of a Wealth-Lab 7 Strategy

Nov 22, 2021 by Glitch

A Strategy in Wealth-Lab 7 is best known for containing rules about when to buy or sell a stock, crypto, or some other financial instrument, based on its historical price data. You create a Strategy by either coding it yourself in the C# programming language, or by using some higher level editor such as our Building Block or Rotation Strategy Designers.

The WL7 framework is based on Microsoft's .NET Core framework, and technically-speaking, a WL7 Strategy is a .NET class that derives from the StrategyBase class, which is defined in the WealthLab.Backtest assembly.

The Strategies that you code up in WL7, or create using Building Blocks, are derived from a subclass of StrategyBase, named UserStrategyBase. UserStrategyBase adds extra fucntionality that allow Strategies to run more easily in a portfolio level backtest.

WL7 uses a different subclass of StrategyBase, named UserStrategyExecutor, to actually run your UserStrategyBase-derived Strategy. UserStrategyExecutor creates an instance of your Strategy for every symbol being backtested. In this way, each symbol of the backtest can maintain its own instance-level variables.

As a side note, a WL7 Rotation Strategy is actually a different class, named RotationStrategy, that descends directly from StrategyBase. It maintains its own synchronization of symbols and its own processing loop internally.

Backtester Class

The Backtester class is the WL7 workhorse, performing the backtests and collecting the results. It has a method RunBacktest which takes an instance of a StrategyBase derived class, and a list of historical data as parameters.

The Backtester synchronizes the data, calls methods in the StrategyBase class, processes the simulated trades, and compiles the equity and drawdown curves, along with a host of other performance metrics. It exposes a dynamic property Metrics that WL7 ScoreCards leverage to generate their performance metrics. All performance metrics are ultimately sourced from various properties of the Backtester class.

Backtesting Process Diagram

The diagram below shows the flow of a typical portfolio backtest in WL7. The green block shows the abstracted Backtester processes, and how it passes control to the various virtual methods in UserStrategyBase. In your Strategy class, you'll override some or all of the methods in this blue box to interact with the backtest process at various points.

enter image description here

BacktestBegin

The first point of execution in UserStrategyBase is the BacktestBegin method. WL7's Backtester calls this method only once at the start of the backtest process.

Initialize

WL7's Backtester calls the UserStrategyBase Initialize method right after BacktestBegin. It's called once for every symbol being backtested. Here's you typically create the instances of indicators or other objects that your Strategy intends to use later on in its Execute method. This is also a good place to Plot Indicators or TimeSeries.

Synchronization Loop Begins

At this point, the Backtester has synchronized the historical data for all of the symbols in the backtest. Each symbol might have a different start and end date, but ultimately there is one ordered list of DateTimes that represents the date range being backtested, called the master date list. The following three methods are called in turn for each DateTime in the master date list.

PreExecute

This method is passed a list of the symbols that contain data for the DateTime being processed. This is called the participants list.

Execute

Next, the Backtester iterates through each of the symbols in the participants list, and calls the UserStrategyBase Execute method, passing the symbol as well as the index into the symbol's history that corresponds to the DateTime being processed. It's here that you'll code the logic of your Strategy, checking Indicator values, issuing PlaceTrade calls, etc.

PostExecute

After all the calls to Execute are complete, the Backtester calls PostExecute, which operates exactly like the PreExecute method does above.

Synchronization Loop Ends

After the last DateTime in the master date list is processed, the synchronization loop ends. The following two methods complete the backtest process, outside of this loop.

Cleanup

Similar to Initialize that was called earlier, the Backtester calls Cleanup for each symbol in the backtest. This give you a chance to perform any required housekeeping or logging you might want to do on a per-symbol basis.

BacktestComplete

As a last call, the Backtester calls the BacktestComplete method. Here you can clean up any class-level static resources that you might have set up during BacktestBegin.

Scoping - Instance, Static, and Global

In programming terms, scoping refers to how accessible a variable is within your code. There are three levels of variable scoping you can employ in your WL7 Strategies:

  • Instance Level - these are standard .NET variables defined in your class. In the C# template code, there's a place for them at the bottom of the class. Each class instance gets its own copy of these variables. So they allow, for example, each symbol to have their own Indicator instances.
  • Static - these are .NET variables defined using the C# static keyword. All instances of the class share this same variable instance, but it's lifespan is confined to a particular backtest run. When you Run Backtest again, all static variables are re-initialized.
  • Global - these are objects that you save into WL7's Global Storage using the SetGlobal method. You can retrieve from Global Storage by calling GetGlobal. The values here are retained, even between backtest runs, and can even be shared amongst different Strategies. The are cleared only when WL7 is restarted.

Scoping Example

This sample Strategy creates three counter variables, one an instance variable, one a static, and the last one a Global. It then and increments each variable by 1 in the Initialize method. Running the Strategy on the WealthData Dow 30 for the past 20 years results in 48 symbols participating, and the following output:

Count=1
CountStatic=48
CountGlobal=48

The instance variable counter only reaches a value of 1, since each symbol has its own copy of the variable. In effect, 48 different copies of the variable were all incremented by 1. The shared static variable results in 48. Since the variable is shared among all 48 instances, each instance incremented the same variable one time. The Global variable also results in 48, but let's run the Strategy a second time and look at the output.

Count=1
CountStatic=48
CountGlobal=96

This time, the Global variable reached 96. Since Global variables aren't reset between backtest runs, the incrementing during this run just kept adding to the Global's previous value.

Conclusion

Armed with this knowledge, you can now code your Strategies with a clear idea of each overriden method's place in the processing flow. And, you can confidently choose a variable scoping strategy that is perfect for your needs.

Scoping Example Code

using WealthLab.Backtest;
using WealthLab.Core;

namespace WealthScript1 
{
    public class MyStrategy : UserStrategyBase
    {
        //increment each counter variable once in Initialize
        public override void Initialize(BarHistory bars)
        {
			Count++;
			CountStatic++;
			CountGlobal = HasGlobal("CountGlobal") ? (int)GetGlobal("CountGlobal") : 0;
			CountGlobal++;
			SetGlobal("CountGlobal", CountGlobal);
        }

        //just a demo, no Strategy rules to code here
        public override void Execute(BarHistory bars, int idx)
        {
        }

		//write the variables to the debug log
        public override void BacktestComplete()
        {
			WriteToDebugLog("Count=" + Count);
			WriteToDebugLog("CountStatic=" + CountStatic);
			WriteToDebugLog("CountGlobal=" + CountGlobal);
        }

		//declare variables below
		private int Count = 0;				//instance
        private static int CountStatic = 0;	//static
		private static int CountGlobal = 0;	//global
    }
}