The request
- "Is there a way just to retrieve the current Equity or Drawdown within a script?"
- "How one can write a Strategy that modifies entry/exit points based on portfolio equity?"
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.
Legacy versions of Wealth-Lab (v2.1-4.0) had the ability to retrieve the current equity within a script and use it to apply the so called "trading the equity curve" approach. In pre-.NET versions, it took coding your own portfolio level processing within a standalone ChartScript. In version 6 (.NET), things noticeably changed and this is no longer possible.
Since this change was by design, 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 );
HideVolume();
}
}
}
Step in on any symbol of the DataSet to see the equity. 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
}
Given that this technique was created before PosSizers appeared (in Version 5.6), we highly suggest using a native PosSizer solution like this for "trading the equity curve":
Notes and Limitations
- Does not work with Combination Strategies (Wealth-Lab 6.2+).
- 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.
- 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.)
Credits
Many thanks to
Aleksey who provided the insight and many solutions.