Main Content

runBacktest

Run backtest on one or more strategies

Since R2020b

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. For more information, see runBacktest Processing Steps.

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)
       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 to 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]
             ManagementFee: 0
     ManagementFeeSchedule: 1y
            PerformanceFee: 0
    PerformanceFeeSchedule: 1y
         PerformanceHurdle: 0
                  UserData: [0x0 struct]
            EngineDataList: [0x0 string]

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
           DateAdjustment: "Previous"
      PayExpensesFromCash: 0
                NumAssets: []
                  Returns: []
                Positions: []
                 Turnover: []
                  BuyCost: []
                 SellCost: []
         TransactionCosts: []
                     Fees: []

% 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
           DateAdjustment: "Previous"
      PayExpensesFromCash: 0
                NumAssets: 7
                  Returns: [250x1 timetable]
                Positions: [1x1 struct]
                 Turnover: [250x1 timetable]
                  BuyCost: [250x1 timetable]
                 SellCost: [250x1 timetable]
         TransactionCosts: [1x1 struct]
                     Fees: [1x1 struct]

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)
       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]
             ManagementFee: 0
     ManagementFeeSchedule: 1y
            PerformanceFee: 0
    PerformanceFeeSchedule: 1y
         PerformanceHurdle: 0
                  UserData: [0x0 struct]
            EngineDataList: [0x0 string]

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")

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")

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")

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 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 Arguments

Specify optional pairs of arguments as Name1=Value1,...,NameN=ValueN, where Name is the argument name and Value is the corresponding value. Name-value arguments must appear after other arguments, but the order of the pairs does not matter.

Before R2021a, use commas to separate each name and value, and enclose Name in quotes.

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 pricesTT 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, the backtest will end on the last time in the pricesTT timetable that occurs on or before the 'End' parameter.

Data Types: double | datetime

Since R2023b

Specify items in the pricesTT timetable as cash assets, specified as the comma-separated pair consisting of 'CashAssets' and a scalar string or a string array where each element in the list must correspond to a column in the pricesTT timetable.

Columns that are indicated in the CashAssets list are designated as cash assets. These assets behave like regular assets, except that they do not contribute to transaction costs or portfolio turnover.

When the backtestEngine name-value argument PayExpensesFromCash is set to true, and if either CashAssets or DebtAssets are specified, the expenses encountered in the backtest (transaction costs and other fees) are paid in the following order:

  1. If specified, the assets in the CashAssets list, in order until all CashAssets are exhausted.

  2. If specified, the first asset listed in the DebtAssets list incurs all remaining expenses. If no DebtAssets is specified, then the final CashAssets balance goes negative to pay any remaining expense.

Note

The CashAssets are added to the available engine data for the backtestStrategy name-value argument for EnginedataList.

Data Types: string

Since R2023b

Specify items in prices timetable as debt assets, specified as the comma-separated pair consisting of 'DebtAssets' and a scalar string or a string array where each element in the list must correspond to a column in the pricesTT timetable.

Columns that are indicated in the DebtAssets list are designated as debt assets. These assets behave like regular assets, except that they do not contribute to transaction costs or portfolio turnover.

When the backtestEngine name-value argument PayExpensesFromCash is set to true, and if either CashAssets or DebtAssets are specified, the expenses encountered in the backtest (transaction costs and other fees) are paid in the following order:

  1. If specified, the assets in the CashAssets list, in order until all CashAssets are exhausted.

  2. If specified, the first asset listed in the DebtAssets list incurs all remaining expenses. If no DebtAssets is specified, then the final CashAssets balance goes negative to pay any remaining expense.

Note

The DebtAssets are added to the available engine data for the backtestStrategy name-value argument for EnginedataList.

Data Types: string

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.

More About

collapse all

runBacktest Processing Steps

runBacktest works in a walk-forward process.

The steps for runBacktest processing are:

  1. runBacktest identifies the rebalancing time periods by using the RebalanceFrequency property of the backtestStrategy object. For more information, see Defining Schedules for Backtest Strategies.

  2. At each rebalancing period, runBacktest decides if there is enough past data to perform the rebalancing using the LookbackWindow property of the backtestStrategy object. For example, if you are at rebalancing period T, the prices that are used are those at times t <= T, as long as there is enough price data before time period T to satisfy the minimum value in the LookbackWindow property. runBacktest also verifies that you are not using older data than that defined by the maximum value in the LookbackWindow property.

  3. The backtest information is computed at time T using the rebalanced weights and the prices at time T.

  4. Then runBacktest moves to time period T+1, where there are two options:

    1. If T+1 is not a rebalancing time period, then the backtest information is computed using the rebalancing at time T with the prices at time T+1.

    2. If time T+1 is a rebalancing time period, then runBacktest proceeds to step 2 in this process.

  5. runBacktest continues running this process until the end of the backtest.

Version History

Introduced in R2020b

expand all