Main Content

Backtest Investment Strategies Using datetime and calendarDuration

This example shows how to use datetime inputs in the backtesting workflow. Backtesting is a useful tool to compare how investment strategies perform over historical or simulated market data. Using datetime inputs allows you to accurately specify the exact days that you can then use during the investment period to obtain initial weights from a warm up period, to select a start date to begin the backtest, and to establish a rebalancing frequency.

Load Data

The backtesting framework requires adjusted asset prices, meaning prices adjusted for dividends, splits, or other events. The prices must be stored in a MATLAB® timetable with each column holding a time series of asset prices for an investable asset.

In this example, you use the closing prices of the Dow Jones Industrial Average for 2006.

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

% For readability, use only 15 of the 30 DJI component stocks.
assetSymbols = ["AA","CAT","DIS","GM","HPQ","JNJ","MCD","MMM","MO","MRK","MSFT","PFE","PG","T","XOM"];

% Prune the table to hold only the dates and selected stocks.
timeColumn = "Dates";
T = T(:,[timeColumn assetSymbols]);

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

% View the structure of the prices timetable.
head(pricesTT)
       Dates        AA       CAT      DIS      GM       HPQ      JNJ      MCD      MMM      MO       MRK     MSFT      PFE      PG        T       XOM 
    ___________    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____    _____

    03-Jan-2006    28.72    55.86    24.18    17.82    28.35    59.08    32.72    75.93    52.27    30.73    26.19    22.16    56.38     22.7    56.64
    04-Jan-2006    28.89    57.29    23.77     18.3    29.18    59.99    33.01    75.54    52.65    31.08    26.32    22.88    56.48    22.87    56.74
    05-Jan-2006    29.12    57.29    24.19    19.34    28.97    59.74    33.05    74.85    52.52    31.13    26.34     22.9     56.3    22.92    56.45
    06-Jan-2006    29.02    58.43    24.52    19.61     29.8    60.01    33.25    75.47    52.95    31.08    26.26    23.16    56.24    23.21    57.57
    09-Jan-2006    29.37    59.49    24.78    21.12    30.17    60.38    33.88    75.84    53.11    31.58    26.21    23.16    56.67     23.3    57.54
    10-Jan-2006    28.44    59.25    25.09    20.79    30.33    60.49    33.91    75.37    53.04    31.27    26.35    22.77    56.45    23.16    57.99
    11-Jan-2006    28.05    59.28    25.33    20.61    30.88    59.91     34.5    75.22    53.31    31.39    26.63    23.06    56.65    23.34    58.38
    12-Jan-2006    27.68    60.13    25.41    19.76    30.57    59.63    33.96    74.57    53.23    31.41    26.48     22.9    56.02    23.24    57.77

Define the Strategies

You use backtestStrategy to define investment strategies that capture the logic used to make asset allocation decisions while a backtest is running. Each strategy is periodically given the opportunity to update its portfolio allocation. When you use backtestStrategy, you specify the allocation frequency with the RebalanceFrequency name-value argument. This argument can be an integer, a duration object, a calendarDuration object, or a datetime object.

This example uses two backtesting strategies:

  • Equal-weighted portfolio

wEW=(w1,w2,,wN),  wi=1N

  • Maximum Sharpe ratio portfolio

wSR=argmax{μTwwTQw|w0,i=1Nwi=1}, where μ an N×1 vector of returns and Q is the N×N covariance matrix.

Compute Initial Weights

By default, the initial weights allocate all capital to cash. To avoid this, the first two months (January and February) of the data are used to estimate the initial weights for the different strategies.

t0 = datetime('01-Jan-2006','InputFormat','dd-MMM-uuuu');
tEnd = datetime('28-Feb-2006','InputFormat','dd-MMM-uuuu');
warmupPeriod = t0:tEnd;

You calculate the initial weights by calling the backtestStrategy rebalance functions with only the dates in January and February.

% No current weights (100% cash position).
numAssets = size(pricesTT,2);
current_weights = zeros(1,numAssets);

% Warm-up partition of data set timetable.
warmupTT = pricesTT(warmupPeriod,:);

% Compute the initial portfolio weights for each strategy.
equalWeight_initial     = equalWeightFcn(current_weights,warmupTT);
maxSharpeRatio_initial  = maxSharpeRatioFcn(current_weights,warmupTT);

Visualize the initial weight allocations from the strategies.

strategyNames = {'Equally Weighted', 'Max Sharpe Ratio'};
assetSymbols = pricesTT.Properties.VariableNames;
initialWeights = [equalWeight_initial(:), maxSharpeRatio_initial(:)];
bar(assetSymbols,initialWeights);
legend(strategyNames);
title('Initial Asset Allocations');

Figure contains an axes object. The axes object with title Initial Asset Allocations contains 2 objects of type bar. These objects represent Equally Weighted, Max Sharpe Ratio.

Create Backtest Strategies

In this example the rebalancing frequency of the strategies is set to the first available day of each month.

% Rebalance at the beginning of the month
tEnd = datetime('31-Dec-2006','InputFormat','dd-MMM-uuuu');
rebalFreq = t0:calmonths(1):tEnd;

The lookback window defines the minimum and maximum amount of data to consider when rebalancing. In this example, the rebalance occurs if the backtest start time is at least 2 months prior to the rebalancing time, and it will not include prices older than 6 calendar months.

% Set the rolling lookback window to be at least 2 calendar months and at
% most 6 calendar months.
minLookback = calmonths(2);
maxLookback = calmonths(6);
lookback  = [minLookback maxLookback];

% Use a fixed transaction cost (buy and sell costs are both 0.5% of amount
% traded).
transactionsFixed = 0.005;

% Define backtest strategies
strat1 = backtestStrategy('Equal Weighted', @equalWeightFcn, ...
    'RebalanceFrequency', rebalFreq, ...
    'LookbackWindow', lookback, ...
    'TransactionCosts', transactionsFixed, ...
    'InitialWeights', equalWeight_initial);

strat2 = backtestStrategy('Max Sharpe Ratio', @maxSharpeRatioFcn, ...
    'RebalanceFrequency', rebalFreq, ...
    'LookbackWindow', lookback, ...
    'TransactionCosts', transactionsFixed, ...
    'InitialWeights', maxSharpeRatio_initial);

% Aggregate the strategy objects into an array.
strategies = [strat1, strat2];

Run the Backtest

Set an annual risk-free rate of 1%.

% 1% annual risk-free rate
annualRiskFreeRate = 0.01;

Create the backtestEngine object using the two previously defined strategies. If the rebalancing date is missing in the prices timetable (this usually happens when the day falls on a weekend or a holiday), rebalance on the next available date.

% Create the backtesting engine object
backtester = backtestEngine(strategies,'RiskFreeRate',annualRiskFreeRate,...
    'DateAdjustment','Next');

Use runBacktest to run the backtest. Since this example uses the first two months of data to find the initial weights, use the 'Start' name-value argument to initiate the backtesting after the warm-up period. This is done to avoid the look-ahead bias (that is, "seeing the future").

backtester = runBacktest(backtester,pricesTT,'Start',warmupPeriod(end));

Examine Backtest Results

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

summaryByStrategies = summary(backtester)
summaryByStrategies=9×2 table
                       Equal_Weighted    Max_Sharpe_Ratio
                       ______________    ________________

    TotalReturn             0.19615           0.097125   
    SharpeRatio             0.13059            0.04967   
    Volatility            0.0063393          0.0088129   
    AverageTurnover      0.00076232           0.012092   
    MaxTurnover            0.030952            0.68245   
    AverageReturn        0.00086517         0.00047599   
    MaxDrawdown            0.072652            0.11764   
    AverageBuyCost          0.04131            0.63199   
    AverageSellCost         0.04131            0.63199   

Use equityCurve to plot the equity curve for the different investment strategies.

equityCurve(backtester)

Figure contains an axes object. The axes object with title Equity Curve, xlabel Time, ylabel Portfolio Value contains 2 objects of type line. These objects represent Equal Weighted, Max Sharpe Ratio.

You can visualize the change in the strategy allocations over time using an area chart of the daily asset positions. For information on the assetAreaPlot function, see the Local Functions section.

strategyName = 'Max_Sharpe_Ratio';
assetAreaPlot(backtester,strategyName)

Figure contains an axes object. The axes object with title Max Sharpe Ratio Positions, xlabel Date, ylabel Asset Positions contains 16 objects of type area. These objects represent Cash, AA, CAT, DIS, GM, HPQ, JNJ, MCD, MMM, MO, MRK, MSFT, PFE, PG, T, XOM.

Local Functions

function new_weights = equalWeightFcn(~,pricesTT)
% Equal-weighted portfolio allocation

nAssets = size(pricesTT, 2);
new_weights = ones(1,nAssets);
new_weights = new_weights / sum(new_weights);

end
function new_weights = maxSharpeRatioFcn(~,pricesTT)
% Max Sharpe ratio portfolio allocation

nAssets = size(pricesTT, 2);
assetReturns = tick2ret(pricesTT);
% Max 25% into a single asset (including cash)
p = Portfolio('NumAssets',nAssets,...
    'LowerBound',0,'Budget',1);
p = estimateAssetMoments(p,assetReturns{:,:});
new_weights = estimateMaxSharpeRatio(p);

end
function assetAreaPlot(backtester,strategyName)
% Plot the asset allocation as an area plot.

t = backtester.Positions.(strategyName).Time;
positions = backtester.Positions.(strategyName).Variables;
h = area(t,positions);
title(sprintf('%s Positions',strrep(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