The request
- "Is there a way just to retrieve the current Equity or Drawdown within a strategy?"
- "How one can code a Strategy that modifies entry/exit points based on portfolio equity?"
TL;DR
Given that this technique had been created before PosSizers appeared (in v5.6), if you just need to "trade the equity curve" we highly suggest that you simply use the native PosSizer solution:
If you need plotting the equity and much more control then read on!
The approach
Normally the Portfolio Backtest mode is used to perform portfolio level testing. But this type of backtest applies position sizing rules to a set of fixed trading system signals, and cannot interact dynamically with the evolving portfolio level equity.
There's more complexity than meets the eye in this problem. Our solution is based on an undocumented, internal Wealth-Lab class designed to execute trading Strategies (i.e. WealthScript objects) on both individual symbols (Single Symbol mode) and on a group of symbols (Multi-Symbol portfolio backtest). On the inside the source code is quite complex, so the business logic functions are now included in the
Community.Components to make the resulting Strategy code trader-friendly. Please
install the Extension before proceeding.
But in the end, the technique enables you to execute a WealthScript Strategy "on-the-fly" as well to execute a regular, saved Strategy (using its XML file). See code examples below.
Run a Strategy on-the-fly and display the portfolio equity curve
All position sizing modes are fully supported including (but not limited to)
WealthScript Override and
PosSizers.
Let's paste code below in a new Strategy window, compile and execute in Portfolio Backtest mode, then step in on any of the symbols to see the resulting portfolio equity plot:
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using WealthLab;
using WealthLab.Indicators;
using Community.Components;
namespace WealthLab.Strategies
{
/* Show portfolio equity */
public class OnTheFly : WealthScript
{
protected override void Execute()
{
Utility u = new Utility(this);
string firstRun = ""; string results = "";
SetGlobal( firstRun, Bars.Symbol );
if( (string)GetGlobal( firstRun ) == DataSetSymbols[0] )
{
RemoveGlobal( results + "_PerformanceResults" );
/* Option 1: run a strategy on-the-fly (see the "Donor" class below) */
SystemPerformance sp = u.runDonor( new Donor() );
SetGlobal( results + "_PerformanceResults", sp.Results );
}
/* Retrieve and plot the portfolio equity from Wealth-Lab's global memory */
SystemResults sr = (SystemResults)GetGlobal( results + "_PerformanceResults" );
DataSeries globalEquity = sr.EquityCurve; globalEquity.Description = "Donor system Portfolio Equity";
globalEquity = Synchronize( globalEquity );
ChartPane gEqPane = CreatePane( 40, false, true );
PlotSeries( gEqPane, globalEquity, Color.DarkGreen, LineStyle.Histogram, 2 );
HideVolume();
}
}
// Your optional "donor" strategy
public class Donor : WealthScript
{
protected override void Execute()
{
for(int bar = 21; bar < Bars.Count; bar++)
{
if (IsLastPositionActive)
{
if ( Close[bar] < Close[bar-20] )
SellAtMarket( bar+1, LastPosition );
}
else
{
if ( Close[bar] > Close[bar-20] )
if( BuyAtMarket( bar+1 ) != null )
LastPosition.Priority = Close[bar];
}
}
}
}
}
Let's analyze step by step what does the code do? A complete trading strategy is included in the WealthScript class "Donor".
// Invoke an instance of the helper class from Community.Components, passing it a WealthScript object as "this"
Utility u = new Utility(this);
// Use this trick to execute some code only once; see this Knowledge Base
if( (string)GetGlobal( firstRun ) == DataSetSymbols[0] )
// Make Wealth-Lab run your strategy on-the-fly
SystemPerformance sp = u.runDonor( new Donor() );
// Store in global memory
SetGlobal( results + "_PerformanceResults", sp.Results );
// Retrieve and plot the portfolio equity from Wealth-Lab's global memory
SystemResults sr = (SystemResults)GetGlobal( results + "_PerformanceResults" );
DataSeries globalEquity = sr.EquityCurve;
Run an existing Strategy from disk and display its portfolio equity curve
Code below will execute the "Neo Master" Strategy that comes pre-installed with Wealth-Lab 6. It's essentially the same as above. Notice the
LoadStrategyFromDisk method from
Community.Components that is self-descriptive. Run it in multi-symbol portfolio backtest mode on a DataSet like "Dow 30", selecting 10% of equity:
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using WealthLab;
using WealthLab.Indicators;
using Community.Components;
namespace WealthLab.Strategies
{
/* Show portfolio equity */
public class FromDiskFile : WealthScript
{
protected override void Execute()
{
/* Specify a Strategy: */
string myStrategyName = "Neo Master";
Utility u = new Utility(this);
string firstRun = ""; string results = "";
SetGlobal( firstRun, Bars.Symbol );
if( (string)GetGlobal( firstRun ) == DataSetSymbols[0] )
{
RemoveGlobal( results + "_PerformanceResults" );
/* Option 2: Load an existing Strategy from its XML file on your disk */
WealthScript myStrategy = u.LoadStrategyFromDisk( myStrategyName );
SystemPerformance sp = u.runDonor( myStrategy );
SetGlobal( results + "_PerformanceResults", sp.Results );
}
/* Retrieve and plot the portfolio equity from Wealth-Lab's global memory */
SystemResults sr = (SystemResults)GetGlobal( results + "_PerformanceResults" );
DataSeries globalEquity = sr.EquityCurve; globalEquity.Description = myStrategyName + " Portfolio Equity";
globalEquity = Synchronize( globalEquity );
ChartPane gEqPane = CreatePane( 40, false, true );
PlotSeries( gEqPane, globalEquity, Color.DarkGreen, LineStyle.Histogram, 2 );
/* Build drawdown series */
double drawdownPct = 0, newHigh = 0;
var DrawdownSeriesPct = new DataSeries(Bars,"Drawdown %");
for(int bar = 0; bar < Bars.Count; bar++)
{
if (globalEquity[bar] > newHigh)
newHigh = globalEquity[bar];
if (globalEquity[bar] < newHigh)
drawdownPct = (100 * (globalEquity[bar] / newHigh - 1));
DrawdownSeriesPct[bar] = drawdownPct;
}
ChartPane gDDPane = CreatePane( 40, false, true );
PlotSeries( gDDPane, DrawdownSeriesPct, Color.Red, LineStyle.Histogram, 2 );
HideVolume();
}
}
}
Step in on any symbol of the DataSet to see the portfolio equity and percent drawdown output. Now run the original "Neo Master" side by side on the same DataSet with the same position sizing and data loading settings. Most likely you'll get different values on every run.
What happened? The answer is found in
this FAQ and ultimately in the
WealthScript Programming Guide where you can find a detailed explanation. Now how do we work around this? There are two possible options:
- In Wealth-Lab's Preferences dialog, "Backtest Settings", check Use Worst Trades in Portfolio Simulation.
- Modify the Strategy code: assign a priority to each created Position in the original Strategy code and save it.
"Trading the equity curve"
Below is an example of how interacting dynamically with equity curve can be accomplished in a WealthScript Strategy:
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using WealthLab;
using WealthLab.Indicators;
using Community.Components;
namespace WealthLab.Strategies
{
/* Interacting with portfolio level equity */
public class InteractingWithPortfolioEquity : WealthScript
{
protected override void Execute()
{
// Create an instance of the Utility class from Community.Components, pass WealthScript as "this"
Utility u = new Utility(this);
// Save current symbol in Wealth-Lab's global memory
string firstRun = ""; string results = "";
SetGlobal( firstRun, Bars.Symbol );
#region User-configurable: specify Strategy name
string myStrategyName = "Neo Master";
//string myStrategyName = "Bandwagon Trade";
#endregion User-configurable: specify Strategy name
/* Here we kick in: first symbol detected */
if( (string)GetGlobal( firstRun ) == DataSetSymbols[0] )
{
RemoveGlobal( results + "_PerformanceResults" );
#region User-configurable: Load Donor strategy
// Loading an existing strategy from disk:
WealthScript myStrategy = u.LoadStrategyFromDisk( myStrategyName );
SystemPerformance sp = u.runDonor( myStrategy );
// As an alternative, you can load a strategy on-the-fly (see the "Donor" class below):
//SystemPerformance sp = u.runDonor( new Donor() );
#endregion User-configurable: Load Donor strategy
SetGlobal( results + "_PerformanceResults", sp.Results );
}
/* Simulation already performed, now run using Wealth-Lab's global memory */
// Retrieve system results from the GOP and plot portfolio equity
SystemResults sr = (SystemResults)GetGlobal( results + "_PerformanceResults" );
DataSeries globalEquity = sr.EquityCurve; globalEquity.Description = "Portfolio Equity";
globalEquity = Synchronize( globalEquity );
ChartPane gEqPane = CreatePane( 40, false, true );
PlotSeries( gEqPane, globalEquity, Color.DarkGreen, LineStyle.Histogram, 2 );
HideVolume();
/* Make sure to start your trading loop on a bar after
which the portfolio equity curve produces valid values! */
for(int bar = Bars.FirstActualBar; bar < Bars.Count; bar++)
{
foreach (Position p in sr.Positions )
{
if(( p.EntryDate == Bars.Date[bar] ) & ( p.Bars.Symbol == Bars.Symbol ))
{
#region User-configurable: Interacting with Portfolio Equity
/* Your equity interaction rule goes here: */
if( globalEquity[bar] > globalEquity[bar-100] )
{
Position pos = u.entry( p, bar, p.EntrySignal, p.BasisPrice );
if( pos != null )
{
pos.Priority = p.Priority;
pos.EntrySignal = p.EntrySignal; //pos.Equals(p);
}
}
#endregion User-configurable: Interacting with Portfolio Equity
/* Alternative:
u.entry( p, bar, p.EntrySignal, p.EntryPrice );
if( LastPosition != null )
LastPosition.Priority = p.Priority;*/
}
if( ActivePositions.Count > 0 )
{
for( int pos = ActivePositions.Count - 1; pos > -1 ; pos-- )
{
Position ap = ActivePositions[pos];
if(( p.ExitBar == bar ) & ( p.Bars.Symbol == Bars.Symbol ))
//if( p.EntryBar == ap.EntryBar )
//if( p.EntryPrice == ap.EntryPrice )
if( p.Priority == ap.Priority )
u.exit( p, ap, bar, p.ExitSignal, p.ExitPrice );
}
}
}
}
}
}
#region User-configurable: your "Donor" strategy
// This strategy is optional
public class Donor : WealthScript
{
protected override void Execute()
{
for(int bar = 101; bar < Bars.Count; bar++)
{
if (IsLastPositionActive)
{
if ( Close[bar] < Close[bar-20] )
SellAtMarket( bar+1, LastPosition );
}
else
{
if ( Close[bar] > Close[bar-20] )
if( BuyAtMarket( bar+1 ) != null )
LastPosition.Priority = Close[bar];
}
}
}
}
#endregion User-configurable: your "Donor" strategy
}
Notes and Limitations
- Alerts are not available. (By design - Wealth-Lab presents all positions created by the "donor" system, including those open on the Trades list, as closed.)
- SetContext is not supported in donor scripts; GetExternalSymbol doesn't work too but is replaceable with GetAllDataForSymbol.
- Update your DataSet before proceeding.
- Currently, if a Commission plan is active in Wealth-Lab's Preferences, "interacting with equity curve" will use its default settings. Customized Commission settings are not supported, so to get correct results when comparing, please run your original Strategy with the selected Commission plan's settings reset to default.
- The code that has "equity-trading" rules will run somewhat slower than the original Strategy. This is by design, as the Strategy has to execute another portfolio backtest.
- Does not work with Combination Strategies
Credits
Many thanks to
Aleksey who provided the insight and many solutions.