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

%% 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])

%
%% 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);

% 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);

% 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 specify 'Start' and 'End' as rows of the prices timetable. 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);

% 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

% Copyright 2020 The MathWorks, Inc.