Main Content

runBacktest

Run backtest on one or more strategies

Description

example

backtester = runBacktest(backtester,pricesTT) runs the backtest over the timetable of adjusted asset price data.

runBacktest initializes each strategy previously defined using backtestStrategy to the InitialPortfolioValue and then begins processing the timetable of price data (pricesTT) as follows:

  1. At each time step, the runBacktest function applies the asset returns to the strategy portfolio positions.

  2. The runBacktest function determines which strategies to rebalance based on the RebalanceFrequency property of the backtestStrategy objects.

  3. For strategies that need rebalancing, the runBacktest function calls their rebalance functions with a rolling window of asset price data based on the LookbackWindow property of each backtestStrategy.

  4. Transaction costs are calculated and charged based on the changes in asset positions and the TransactionCosts property of each backtestStrategy object.

  5. After the backtest is complete, the results are stored in several properties of the backtestEngine object.

example

backtester = runBacktest(backtester,pricesTT,signalTT) run the backtest using the adjusted asset price data and signal data. When you specify the signal data timetable (signalTT), then the runBacktest function runs the backtest and additionally passes a rolling window of signal data to the rebalance function of each strategy during the rebalance step.

example

backtester = runBacktest(___,Name,Value) specifies options using one or more optional name-value pair arguments in addition to the input arguments in the previous syntax. For example, backtester = runBacktest(backtester,assetPrices,'Start',50,'End',100).

Examples

collapse all

The MATLAB® backtesting engine runs backtests of portfolio investment strategies over timeseries of asset price data. After creating a set of backtest strategies using backtestStrategy and the backtest engine using backtestEngine, the runBacktest function executes the backtest. This example illustrates how to use the runBacktest function to test investment strategies.

Load Data

Load one year of stock price data. For readability, this example only uses a subset of the DJIA stocks.

% Read table of daily adjusted close prices for 2006 DJIA stocks
T = readtable('dowPortfolio.xlsx');

% Prune the table on only hold the dates and selected stocks
timeColumn = "Dates";
assetSymbols = ["BA", "CAT", "DIS", "GE", "IBM", "MCD", "MSFT"];
T = T(:,[timeColumn assetSymbols]);

% Convert to timetable
pricesTT = table2timetable(T,'RowTimes','Dates');

% View the final asset price timetable
head(pricesTT)
ans=8×7 timetable
       Dates        BA       CAT      DIS      GE       IBM      MCD     MSFT 
    ___________    _____    _____    _____    _____    _____    _____    _____

    03-Jan-2006    68.63    55.86    24.18     33.6    80.13    32.72    26.19
    04-Jan-2006    69.34    57.29    23.77    33.56    80.03    33.01    26.32
    05-Jan-2006    68.53    57.29    24.19    33.47    80.56    33.05    26.34
    06-Jan-2006    67.57    58.43    24.52     33.7    82.96    33.25    26.26
    09-Jan-2006    67.01    59.49    24.78    33.61    81.76    33.88    26.21
    10-Jan-2006    67.33    59.25    25.09    33.43     82.1    33.91    26.35
    11-Jan-2006     68.3    59.28    25.33    33.66    82.19     34.5    26.63
    12-Jan-2006     67.9    60.13    25.41    33.25    81.61    33.96    26.48

Create Strategy

In this introductory example, test an equal weighted investment strategy. This strategy invests an equal portion of the available capital into each asset. This example does describe the details about how create backtest strategies. For more information on creating backtest strategies, see backtestStrategy.

Set the RebalanceFrequency to rebalance the portfolio every 60 days. This example does not use a lookback window to rebalance.

% Create the strategy
numAssets = size(pricesTT,2);
equalWeightsVector = ones(1,numAssets) / numAssets;
equalWeightsRebalanceFcn = @(~,~) equalWeightsVector;

ewStrategy = backtestStrategy("EqualWeighted",equalWeightsRebalanceFcn,...
    'RebalanceFrequency',60,...
    'LookbackWindow',0,...
    'TransactionCosts',0.005,...
    'InitialWeights',equalWeightsVector)
ewStrategy = 
  backtestStrategy with properties:

                  Name: "EqualWeighted"
          RebalanceFcn: @(~,~)equalWeightsVector
    RebalanceFrequency: 60
      TransactionCosts: 0.0050
        LookbackWindow: 0
        InitialWeights: [0.1429 0.1429 0.1429 0.1429 0.1429 0.1429 0.1429]

Run Backtest

Create a backtesting engine and run a backtest over a year of stock data. For more information on creating backtest engines, see backtestEngine.

% Create the backtest engine. The backtest engine properties that hold the
% results are initialized to empty.
backtester = backtestEngine(ewStrategy)
backtester = 
  backtestEngine with properties:

               Strategies: [1x1 backtestStrategy]
             RiskFreeRate: 0
           CashBorrowRate: 0
          RatesConvention: "Annualized"
                    Basis: 0
    InitialPortfolioValue: 10000
                NumAssets: []
                  Returns: []
                Positions: []
                 Turnover: []
                  BuyCost: []
                 SellCost: []

% Run the backtest. The empty properties are now populated with
% timetables of detailed backtest results.
backtester = runBacktest(backtester,pricesTT)
backtester = 
  backtestEngine with properties:

               Strategies: [1x1 backtestStrategy]
             RiskFreeRate: 0
           CashBorrowRate: 0
          RatesConvention: "Annualized"
                    Basis: 0
    InitialPortfolioValue: 10000
                NumAssets: 7
                  Returns: [250x1 timetable]
                Positions: [1x1 struct]
                 Turnover: [250x1 timetable]
                  BuyCost: [250x1 timetable]
                 SellCost: [250x1 timetable]

Backtest Summary

Use the summary function to generate a summary table of backtest results.

% Examing results. The summary table shows several performance metrics.
summary(backtester)
ans=9×1 table
                       EqualWeighted
                       _____________

    TotalReturn            0.22943  
    SharpeRatio            0.11415  
    Volatility           0.0075013  
    AverageTurnover     0.00054232  
    MaxTurnover           0.038694  
    AverageReturn       0.00085456  
    MaxDrawdown           0.098905  
    AverageBuyCost        0.030193  
    AverageSellCost       0.030193  

When running a backtest in MATLAB®, you need to understand what the initial conditions are when the backtest begins. The initial weights for each strategy, the size of the strategy lookback window, and any potential split of the dataset into training and testing partitions affects the results of the backtest. This example shows how to use the runBacktest function with the 'Start' and 'End' name-value pair arguments that interact with the 'LookbackWindow' and 'RebalanceFrequency' properties of the backtestStrategy object to "warm start" a backtest.

Load Data

Load one year of stock price data. For readability, this example uses only a subset of the DJIA stocks.

% Read table of daily adjusted close prices for 2006 DJIA stocks.
T = readtable('dowPortfolio.xlsx');

% Prune the table to include only the dates and selected stocks.
timeColumn = "Dates";
assetSymbols = ["BA", "CAT", "DIS", "GE", "IBM", "MCD", "MSFT"];
T = T(:,[timeColumn assetSymbols]);

% Convert to timetable.
pricesTT = table2timetable(T,'RowTimes','Dates');

% View the final asset price timetable.
head(pricesTT)
ans=8×7 timetable
       Dates        BA       CAT      DIS      GE       IBM      MCD     MSFT 
    ___________    _____    _____    _____    _____    _____    _____    _____

    03-Jan-2006    68.63    55.86    24.18     33.6    80.13    32.72    26.19
    04-Jan-2006    69.34    57.29    23.77    33.56    80.03    33.01    26.32
    05-Jan-2006    68.53    57.29    24.19    33.47    80.56    33.05    26.34
    06-Jan-2006    67.57    58.43    24.52     33.7    82.96    33.25    26.26
    09-Jan-2006    67.01    59.49    24.78    33.61    81.76    33.88    26.21
    10-Jan-2006    67.33    59.25    25.09    33.43     82.1    33.91    26.35
    11-Jan-2006     68.3    59.28    25.33    33.66    82.19     34.5    26.63
    12-Jan-2006     67.9    60.13    25.41    33.25    81.61    33.96    26.48

Create Strategy

This example backtests an "inverse variance" strategy. The inverse variance rebalance function is implemeted in the Local Functions section. For more information on creating backtest strategies, see backtestStrategy. The inverse variance strategy uses the covariance of asset returns to make decisions about asset allocation. The LookbackWindow for this strategy must contain at least 30 days of trailing data (about 6 weeks), and at most, 60 days (about 12 weeks).

Set RebalanceFrequency for backtestStrategy to rebalance the portfolio every 25 days.

% Create the strategy
minLookback = 30;
maxLookback = 60;
ivStrategy = backtestStrategy("InverseVariance",@inverseVarianceFcn,...
    'RebalanceFrequency',25,...
    'LookbackWindow',[minLookback maxLookback],...
    'TransactionCosts',[0.0025 0.005])
ivStrategy = 
  backtestStrategy with properties:

                  Name: "InverseVariance"
          RebalanceFcn: @inverseVarianceFcn
    RebalanceFrequency: 25
      TransactionCosts: [0.0025 0.0050]
        LookbackWindow: [30 60]
        InitialWeights: [1x0 double]

Run Backtest and Examine Results

Create a backtesting engine and run a backtest over a year of stock data. For more information on creating backtest engines, see backtestEngine.

% Create the backtest engine.
backtester = backtestEngine(ivStrategy);

% Run the backtest.
backtester = runBacktest(backtester,pricesTT);

Use the assetAreaPlot helper function, defined in the Local Functions section of this example, to display the change in the asset allocation over the course of the backtest.

assetAreaPlot(backtester,"InverseVariance")

Figure contains an axes. The axes with title InverseVariance Positions contains 8 objects of type area. These objects represent Cash, BA, CAT, DIS, GE, IBM, MCD, MSFT.

Notice that the inverse variance strategy begins all in cash and remains in that state for about 2.5 months. This is because the backtestStrategy object does not have a specified set of initial weights, which you specify using the InitialPortfolioValue name-value pair argument. The inverse variance strategy requires 30 days of trailing asset price history before rebalancing. You can use the printRebalanceTable helper function, defined in the Local Functions section, to display the rebalance schedule.

printRebalanceTable(ivStrategy,pricesTT,minLookback);
    First Day of Data    Backtest Start Date    Minimum Days to Rebalance
    _________________    ___________________    _________________________

       03-Jan-2006           03-Jan-2006                   30            



    Rebalance Dates    Days of Available Price History    Enough Data to Rebalance
    _______________    _______________________________    ________________________

      08-Feb-2006                     26                           "No"           
      16-Mar-2006                     51                           "Yes"          
      21-Apr-2006                     76                           "Yes"          
      26-May-2006                    101                           "Yes"          
      03-Jul-2006                    126                           "Yes"          
      08-Aug-2006                    151                           "Yes"          
      13-Sep-2006                    176                           "Yes"          
      18-Oct-2006                    201                           "Yes"          
      22-Nov-2006                    226                           "Yes"          
      29-Dec-2006                    251                           "Yes"          

The first rebalance date comes on February 8 but the strategy does not have enough price history to fill out a valid lookback window (minimum is 30 days), so no rebalance occurs. The next rebalance date is on March 16, a full 50 days into the backtest.

This situation is not ideal as these 50 days sitting in an all-cash position represent approximately 20% of the total backtest. Consequently, when the backtesting engine reports on the performance of the strategy (that is, the total return, Sharpe ratio, volatility, and so on), the results do not reflect the "true" strategy performance because the strategy only began to make asset allocation decisions only about 20% into the backtest.

Warm Start Backtest

It is possible to "warm start" the backtest. A warm start means that the backtest results reflect the strategy performance in the market conditions reflected in the price timetable. To start, set the initial weights of the strategy to avoid starting all in cash.

The inverse variance strategy requires 30 days of price history to fill out a valid lookback window, so you can partition the price data set into two sections, a "warm-up" set and a "test" set.

warmupRange = 1:30;
% The 30th row is included in both ranges since the day 30 price is used
% to compute the day 31 returns.
testRange = 30:height(pricesTT);

Use the warm-up partition to set the initial weights of the inverse variance strategy. By doing so, you can begin the backtest with the strategy already "running" and avoid the initial weeks spent in the cash position.

% Use the rebalance function to set the initial weights. This might
% or might not be possible for other strategies depending on the details of
% the strategy logic.
initWeights = inverseVarianceFcn([],pricesTT(warmupRange,:));

Update the strategy and rerun the backtest. Since the warm-up range is used to initialize the inverse variance strategy, you must omit this data from the backtest to avoid a look-ahead bias, or "seeing the future," and to backtest only over the "test range."

% Set the initial weights on the strategy in the backtester. You can do this when you 
% create the strategy as well, using the 'InitialWeights' parameter.
backtester.Strategies(1).InitialWeights = initWeights;

% Rerun the backtest over the "test" range.
backtester = runBacktest(backtester,pricesTT(testRange,:));

When you generate the area plot, you can see that the issue where the strategy is in cash for the first portion of the backtest is avoided.

assetAreaPlot(backtester,"InverseVariance")

Figure contains an axes. The axes with title InverseVariance Positions contains 8 objects of type area. These objects represent Cash, BA, CAT, DIS, GE, IBM, MCD, MSFT.

However, if you look at the rebalance table, you can see that the strategy still "missed" the first rebalance date. When you run the backtest over the test range of the data set, the first rebalance date is on March 22. This is because the warm-up range is omitted from the price history and the strategy had only 26 days of history available on that date (less than the minimum 30 days required for the lookback window). Therefore, the March 22 rebalance is skipped.

To avoid backtesting over the warm-up range, the range was removed it from the data set. This means the new backtest start date and all subsequent rebalance dates are 30 days later. The price history data contained in the warm-up range was completely removed, so when the backtest engine hit the first rebalance date the price history was insufficient to rebalance.

printRebalanceTable(ivStrategy,pricesTT(testRange,:),minLookback);
    First Day of Data    Backtest Start Date    Minimum Days to Rebalance
    _________________    ___________________    _________________________

       14-Feb-2006           14-Feb-2006                   30            



    Rebalance Dates    Days of Available Price History    Enough Data to Rebalance
    _______________    _______________________________    ________________________

      22-Mar-2006                     26                           "No"           
      27-Apr-2006                     51                           "Yes"          
      02-Jun-2006                     76                           "Yes"          
      10-Jul-2006                    101                           "Yes"          
      14-Aug-2006                    126                           "Yes"          
      19-Sep-2006                    151                           "Yes"          
      24-Oct-2006                    176                           "Yes"          
      29-Nov-2006                    201                           "Yes"          

This scenario is also not correct since the original price timetable (warm-up and test partitions together) does have enough price history by March 22 to fill out a valid lookback window. However, the earlier data is not available to the backtest engine because the backtest was run using only the test partition.

Use Start and End Parameters for runBacktest

The ideal workflow in this situation is to both omit the warm-up data range from the backtest to avoid the look-ahead bias but include the warm-up data in the price history to be able to fill out the lookback window of the strategy with all available price history data. You can do so by using the 'Start' parameter for the runBacktest function.

The 'Start' and 'End' name-value pair arguments for runBacktest enable you to start and end the backtest on specific dates. You can specify 'Start' and 'End' as rows of the prices timetable or as datetime values (see the documentation for the runBacktest function for details). The 'Start' argument lets the backtest begin on a particular date while giving the backtest engine access to the full data set.

Rerun the backtest using the 'Start' name-value pair argument rather than only running on a partition of the original data set.

% Rerun the backtest starting on the last day of the warmup range.
startRow = warmupRange(end);
backtester = runBacktest(backtester,pricesTT,'Start',startRow);

Plot the new asset area plot.

assetAreaPlot(backtester,"InverseVariance")

Figure contains an axes. The axes with title InverseVariance Positions contains 8 objects of type area. These objects represent Cash, BA, CAT, DIS, GE, IBM, MCD, MSFT.

View the new rebalance table with the new 'Start' parameter.

printRebalanceTable(ivStrategy,pricesTT,minLookback,startRow);
    First Day of Data    Backtest Start Date    Minimum Days to Rebalance
    _________________    ___________________    _________________________

       03-Jan-2006           14-Feb-2006                   30            



    Rebalance Dates    Days of Available Price History    Enough Data to Rebalance
    _______________    _______________________________    ________________________

      22-Mar-2006                     55                           "Yes"          
      27-Apr-2006                     80                           "Yes"          
      02-Jun-2006                    105                           "Yes"          
      10-Jul-2006                    130                           "Yes"          
      14-Aug-2006                    155                           "Yes"          
      19-Sep-2006                    180                           "Yes"          
      24-Oct-2006                    205                           "Yes"          
      29-Nov-2006                    230                           "Yes"          

The inverse variance strategy now has enough data to rebalance on the first rebalance date (March 22) and the backtest is "warm started." By using the original data set, the first day of data remains January 3, and the 'Start' parameter allows you to move the backtest start date forward to avoid the warm-up range.

Even though the results are not dramatically different, this example illustrates the interaction between the LookbackWindow and RebalanceFrequency name-value pair arguments for a backtestStrategy object and the range of data used in the runBacktest when you evaluate the performance of a strategy in a backtest.

Local Functions

The strategy rebalance function is implemented as follows. For more information on creating strategies and writing rebalance functions, see backtestStrategy.

function new_weights = inverseVarianceFcn(current_weights, pricesTT) 
% Inverse-variance portfolio allocation.

assetReturns = tick2ret(pricesTT);
assetCov = cov(assetReturns{:,:});
new_weights = 1 ./ diag(assetCov);
new_weights = new_weights / sum(new_weights);

end

This helper function plots the asset allocation as an area plot.

function assetAreaPlot(backtester,strategyName)

t = backtester.Positions.(strategyName).Time;
positions = backtester.Positions.(strategyName).Variables;
h = area(t,positions);
title(sprintf('%s Positions',strategyName));
xlabel('Date');
ylabel('Asset Positions');
datetick('x','mm/dd','keepticks');
xlim([t(1) t(end)])
oldylim = ylim;
ylim([0 oldylim(2)]);
cm = parula(numel(h));
for i = 1:numel(h)
    set(h(i),'FaceColor',cm(i,:));
end
legend(backtester.Positions.(strategyName).Properties.VariableNames)

end

This helper function generates a table of rebalance dates along with the available price history at each date.

function printRebalanceTable(strategy,pricesTT,minLookback,startRow)

if nargin < 4
    startRow = 1;
end

allDates = pricesTT.(pricesTT.Properties.DimensionNames{1});
rebalanceDates = allDates(startRow:strategy.RebalanceFrequency:end);
[~,rebalanceIndices] = ismember(rebalanceDates,pricesTT.Dates);

disp(table(allDates(1),rebalanceDates(1),minLookback,'VariableNames',{'First Day of Data','Backtest Start Date','Minimum Days to Rebalance'}));
fprintf('\n\n');
numHistory = rebalanceIndices(2:end);
sufficient = repmat("No",size(numHistory));
sufficient(numHistory > minLookback) = "Yes";
disp(table(rebalanceDates(2:end),rebalanceIndices(2:end),sufficient,'VariableNames',{'Rebalance Dates','Days of Available Price History','Enough Data to Rebalance'}));

end

Input Arguments

collapse all

Backtesting engine, specified as a backtestEngine object. Use backtestEngine to create the backtester object.

Data Types: object

Asset prices, specified as a timetable of asset prices that the backtestEngine uses to backtest the strategies. Each column of the prices timetable must contain a timeseries of prices for an asset. Historical asset prices must be adjusted for splits and dividends.

Data Types: timetable

(Optional) Signal data, specified as a timetable of trading signals that the strategies use to make trading decisions. signalTT is optional. If provided, the backtestEngine calls the strategy rebalance functions with both asset price data and signal data. The signalTT timetable must have the same time dimension as the pricesTT timetable.

Data Types: timetable

Name-Value Pair Arguments

Specify optional comma-separated pairs of Name,Value arguments. Name is the argument name and Value is the corresponding value. Name must appear inside quotes. You can specify several name and value pair arguments in any order as Name1,Value1,...,NameN,ValueN.

Example: backtester = runBacktest(backtester,assetPrices,'Start',50,'End',100)

Time step to start the backtest, specified as the comma-separated pair consisting of 'Start' and a scalar integer or datetime.

If an integer, the Start time refers to the row in the pricesTT timetable where the backtest begins.

If a datetime object, the backtest will begin at the first time in the prices timetable that occurs on or after the 'Start' parameter. The backtest will end on the last time in the prices timetable that occurs on or before the 'End' parameter. The 'Start' and 'End' parameters set the boundary of the data that is included in the backtest.

Data Types: double | datetime

Time step to end the backtest, specified as the comma-separated pair consisting of 'End' and a scalar integer or datetime.

If an integer, the End time refers to the row in the pricesTT timetable where the backtest ends.

If a datetime object, TBD

Data Types: double | datetime

Output Arguments

collapse all

Backtesting engine, returned as an updated backtestEngine object. After backtesting is complete, runBacktest populates several properties in the backtestEngine object with the results of the backtest. You can summarize the results by using the summary function.

Introduced in R2020b