runBacktest
Syntax
Description
runs the backtest over the timetable of adjusted asset price data.backtester
= runBacktest(backtester
,pricesTT
)
runBacktest
initializes each strategy previously defined using
backtestStrategy
to the
InitialPortfolioValue
and then begins processing the timetable of
price data (pricesTT
) as follows:
At each time step, the
runBacktest
function applies the asset returns to the strategy portfolio positions.The
runBacktest
function determines which strategies to rebalance based on theRebalanceFrequency
property of thebacktestStrategy
objects.For strategies that need rebalancing, the
runBacktest
function calls their rebalance functions with a rolling window of asset price data based on theLookbackWindow
property of eachbacktestStrategy
.Transaction costs are calculated and charged based on the changes in asset positions and the
TransactionCosts
property of eachbacktestStrategy
object.After the backtest is complete, the results are stored in several properties of the
backtestEngine
object.
run the backtest using the adjusted asset price data and signal data. When you specify the
signal data timetable (backtester
= runBacktest(backtester
,pricesTT
,signalTT
)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.
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(___,Name,Value
)backtester =
runBacktest(backtester,assetPrices,'Start',50,'End',100)
.
Examples
Run Backtests
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 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
Warm Starting Backtests
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 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
backtester
— Backtesting engine
backtestEngine
object
Backtesting engine, specified as a backtestEngine
object. Use
backtestEngine
to create the
backtester
object.
Data Types: object
pricesTT
— Asset prices
timetable
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
signalTT
— Signal data
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)
Start
— Time step to start backtest
1
(default) | integer | datetime
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
End
— Time step to end backtest
Last row in pricesTT
timetable (default) | integer | 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
CashAssets
— Specify items in pricesTT
timetable as cash assets
"" (default) | string | string array
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:
If specified, the assets in the
CashAssets
list, in order until allCashAssets
are exhausted.If specified, the first asset listed in the
DebtAssets
list incurs all remaining expenses. If noDebtAssets
is specified, then the finalCashAssets
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
DebtAssets
— Specify items in pricesTT
timetable as debt assets
"" (default) | string | string array
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:
If specified, the assets in the
CashAssets
list, in order until allCashAssets
are exhausted.If specified, the first asset listed in the
DebtAssets
list incurs all remaining expenses. If noDebtAssets
is specified, then the finalCashAssets
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
backtester
— Backtesting engine
backtestEngine
object
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.
Version History
Introduced in R2020bR2023b: Supports for CashAssets
and DebtAssets
name-value arguments
runBacktest
supports name-value arguments for
CashAssets
and DebtAssets
. When you specify a
CashAssets
or DebtAssets
using
runBacktest
, you can use the backtestStrategy
EngineDataList
name-value argument to capture this backtest state data
that a strategy needs in the backtestStrategy
rebalanceFcn
input argument.
Open Example
You have a modified version of this example. Do you want to open this example with your edits?
MATLAB Command
You clicked a link that corresponds to this MATLAB command:
Run the command by entering it in the MATLAB Command Window. Web browsers do not support MATLAB commands.
Select a Web Site
Choose a web site to get translated content where available and see local events and offers. Based on your location, we recommend that you select: .
You can also select a web site from the following list:
How to Get Best Site Performance
Select the China site (in Chinese or English) for best site performance. Other bat365 country sites are not optimized for visits from your location.
Americas
- América Latina (Español)
- Canada (English)
- United States (English)
Europe
- Belgium (English)
- Denmark (English)
- Deutschland (Deutsch)
- España (Español)
- Finland (English)
- France (Français)
- Ireland (English)
- Italia (Italiano)
- Luxembourg (English)
- Netherlands (English)
- Norway (English)
- Österreich (Deutsch)
- Portugal (English)
- Sweden (English)
- Switzerland
- United Kingdom (English)