Backtest with Brinson Attribution to Evaluate Portfolio Performance
This example shows how to compute Brinson attribution using the output of the MATLAB® backtest framework. The backtest framework allows you to build custom trading strategies and then backtest them against historical or simulated market data. The example uses the Brinson attribution function (brinsonAttribution
) to explain a portfolio's performance versus a benchmark.
Load Data
Brinson attribution requires that a category is assigned to each asset. Load the dowPortfolio.xlsx
data set, then assign each of the 30 stocks to a category.
% Read a table of daily adjusted close prices for 2006 DJIA stocks. pricesTT = readtimetable('dowPortfolio.xlsx'); % Remove the index from the dataset. pricesTT= removevars(pricesTT,'DJI'); num_assets = width(pricesTT); % Set the sectors for each stock. asset_categories = categorical(["Materials";"Financials";"Financials"; ... "Industrials";"Financials";"Industrials";"Materials"; ... "Communication Services";"Industrials";"Consumer Discretionary"; ... "Consumer Discretionary";"Industrials";"Information Technology"; ... "Information Technology";"Information Technology";"Health Care"; ... "Financials";"Consumer Staples";"Consumer Discretionary"; ... "Industrials";"Consumer Staples";"Health Care"; ... "Information Technology";"Health Care";"Consumer Staples"; ... "Communication Services";"Industrials";"Communication Services"; ... "Consumer Staples";"Energy"]); % Display the stock categories. Symbol = pricesTT.Properties.VariableNames(:); table(Symbol,asset_categories)
ans=30×2 table
Symbol asset_categories
________ ______________________
{'AA' } Materials
{'AIG' } Financials
{'AXP' } Financials
{'BA' } Industrials
{'C' } Financials
{'CAT' } Industrials
{'DD' } Materials
{'DIS' } Communication Services
{'GE' } Industrials
{'GM' } Consumer Discretionary
{'HD' } Consumer Discretionary
{'HON' } Industrials
{'HPQ' } Information Technology
{'IBM' } Information Technology
{'INTC'} Information Technology
{'JNJ' } Health Care
⋮
Define and Create Backtest Strategies
Multiperiod Brinson attribution expects fixed, regular time periods. In this case, the backtest strategy rebalance schedules will match the Brinson periods. You can set the rebalance dates to be monthly starting from January first.
% Rebalance on the first trading day of each month. rebalance_schedule = datetime(2006,1,1):calmonths(1):datetime(2006,12,1); for idx = 1:numel(rebalance_schedule) priceIdx = find(rebalance_schedule(idx) <= pricesTT.Dates,1,'first'); rebalance_schedule(idx) = pricesTT.Dates(priceIdx); end
The risk budgeting strategy needs historical data in order to set initial weights. You can use the first month of price data as a warm-up period to set the initial weights. The backtest begins at the end of the warm-up period.
% Use first month as warm-up period for the risk budgeting strategy. start_date = rebalance_schedule(2); % Get the first month of prices to initialize the risk budgeting strategy. initial_prices = pricesTT(pricesTT.Dates <= start_date,:);
Brinson attribution measures portfolio manager performance against some benchmark. For this example, use a simple equal-weighted strategy as the benchmark. This example examines the performance of the following two strategies relative to this benchmark:
Equal Category — This strategy allocates resources equally per category, and then allocates equal weights for stocks within each category.
Risk Budgeting — This strategy sets weights for stocks with the goal of minimizing each asset's risk contribution.
Create a backtestStrategy
object for the benchmark and the two candidate strategies. Define each of these strategies to use the same rebalance schedule and assign the appropriate initial weights.
% Define the Benchmark strategy. benchmark = backtestStrategy('Benchmark',@equalWeightRebalanceFcn, ... 'RebalanceFrequency',rebalance_schedule, ... 'InitialWeights',computeEqualWeights(num_assets), ... 'UserData',struct()); % Define the equal category strategy. equalCategory = backtestStrategy('EqualCategory',@equalCategoryRebalanceFcn, ... 'RebalanceFrequency',rebalance_schedule, ... 'InitialWeights',computeEqualCategory(asset_categories), ... 'UserData',struct('Categories',asset_categories)); % Define the risk budgeting strategy. riskBudgeting = backtestStrategy('RiskBudgeting',@riskBudgetingRebalanceFcn, ... 'RebalanceFrequency',rebalance_schedule, ... 'InitialWeights',computeRiskBudgeting(initial_prices));
Use backtestEngine
to create a backtestEngine
object for the strategies and then use runBacktest
to run the backtest. Display the equity curve using equityCurve
and then use summary
to display the backtest summary table. Both the equal category and risk budgeting strategies outperform the benchmark.
strategies = [benchmark,equalCategory,riskBudgeting];
bt = backtestEngine(strategies);
bt = runBacktest(bt,pricesTT,'Start',start_date);
equityCurve(bt)
summary(bt)
ans=9×3 table
Benchmark EqualCategory RiskBudgeting
__________ _____________ _____________
TotalReturn 0.17811 0.19249 0.18065
SharpeRatio 0.11671 0.12645 0.12393
Volatility 0.0062905 0.0062211 0.0059849
AverageTurnover 0.00073556 0.00076049 0.0015965
MaxTurnover 0.025681 0.028611 0.080538
AverageReturn 0.00073259 0.00078493 0.00074008
MaxDrawdown 0.07502 0.078323 0.071364
AverageBuyCost 0 0 0
AverageSellCost 0 0 0
Prepare Backtest Results for Brinson Attribution
Set the strategy name of the benchmark.
benchmark_name = 'Benchmark';
Define Brinson periods by the rebalance dates and the backtest end date.
period_dates = [rebalance_schedule(2:end), pricesTT.Dates(end)];
Compute Brinson Attribution for Backtested Investment Strategy
Select whether to analyze the equal category or risk budgeting strategy. Then, use the backtest2brinson
function in Local Functions to compute asset_table
that is formatted for use with the brinsonAttribution
function. For more information on the format of the asset_table
, see the description for the AssetTable
input argument in the brinsonAttribution
function.
% Select strategy to analyze strategy_name = "EqualCategory"; asset_table = backtest2brinson(bt,strategy_name,benchmark_name,pricesTT,asset_categories,period_dates); head(asset_table)
Period Name Return Category PortfolioWeight BenchmarkWeight ______ _____ _________ ______________________ _______________ _______________ 1 "AA" -0.050283 Materials 0.055556 0.033333 1 "AIG" 0.0015356 Financials 0.027778 0.033333 1 "AXP" 0.028439 Financials 0.027778 0.033333 1 "BA" 0.022929 Industrials 0.018519 0.033333 1 "C" 0.015045 Financials 0.027778 0.033333 1 "CAT" 0.074416 Industrials 0.018519 0.033333 1 "DD" 0.037791 Materials 0.055556 0.033333 1 "DIS" 0.11182 Communication Services 0.037037 0.033333
% Compute Brinson attribution
brinson = brinsonAttribution(asset_table);
summary(brinson)
ans=11×1 table
Brinson Attribution Summary
___________________________
Total Number of Assets 30
Number of Assets in Portfolio 30
Number of Assets in Benchmark 30
Number of Periods 11
Number of Categories 9
Portfolio Return 0.19249
Benchmark Return 0.17811
Active Return 0.014376
Allocation Effect 0.014376
Selection Effect 8.5567e-18
Interaction Effect -1.1701e-19
Generate the attribution chart using attributionsChart
with the brinson
object. The attributions chart creates a horizontal bar chart of portfolio performance attributions by category, aggregated over all time periods.
attributionsChart(brinson)
Generate a category returns chart using categoryReturnsChart
with the brinson
object. The category returns chart creates a horizontal bar chart of portfolio and benchmark category returns, aggregated over all time periods.
categoryReturnsChart(brinson)
Generate a category weights chart using categoryWeightsChart
with the brinson
object. The category weights chart creates a horizontal bar chart of portfolio, benchmark, and active weights by category, averaged over all time periods.
categoryWeightsChart(brinson)
The equal category strategy, despite having different category weights from the benchmark, has the same equal-weighted asset selection process within each category. Therefore, the equal category strategy has no selection or interaction effect (relative to the benchmark). Meanwhile, the risk budgeting strategy disregards the asset categories when setting portfolio weights, so it has selection and interaction effects.
Local Functions
Both the equal asset weight and equal category weight rebalance functions compute a fixed weight that is assigned at each rebalance date. In order to avoid calculating the weights each time, the weights are calculated once and then the result is saved in the UserData
struct for the backtestStrategy
object. For more information on the UserData
struct, see the rebalanceFcn input argument for backtestStrategy
.
function [new_weights,user_data] = equalWeightRebalanceFcn(~,pricesTT,user_data) % Equal asset weight rebalance function if ~isfield(user_data,'FixedAllocation') % If this is the first call to the rebalance function, calculate the % desired fixed allocation and save it. user_data.FixedAllocation = computeEqualWeights(width(pricesTT)); end new_weights = user_data.FixedAllocation; end function weights = computeEqualWeights(num_assets) % Equal asset weight portfolio allocation weights = ones(1,num_assets) / num_assets; end function [new_weights,user_data] = equalCategoryRebalanceFcn(~, ~, user_data) % Equal category weight rebalance function if ~isfield(user_data,'FixedAllocation') % If this is the first call to the rebalance function, calculate the % desired fixed allocation and save it. user_data.FixedAllocation = computeEqualCategory(user_data.Categories); end new_weights = user_data.FixedAllocation; end function weights = computeEqualCategory(asset_categories) % Equal category weight portfolio allocation weights = zeros(1,numel(asset_categories)); unique_categories = unique(asset_categories); category_weight = 1 / numel(unique_categories); for i = 1:numel(unique_categories) category_mask = asset_categories == unique_categories(i); weights(category_mask) = category_weight / sum(category_mask); end end function new_weights = riskBudgetingRebalanceFcn(~,pricesTT) % Risk budgeting rebalance function new_weights = computeRiskBudgeting(pricesTT); end function new_weights = computeRiskBudgeting(pricesTT) % Risk budgeting portfolio allocation asset_returns = tick2ret(pricesTT); asset_cov = cov(asset_returns{:,:}); new_weights = riskBudgetingPortfolio(asset_cov); end function asset_table = backtest2brinson(bt,strategy_name,benchmark_name,pricesTT,asset_categories,period_dates) % Build Brinson attribution input asset table based on the results of the % completed backtest. % Compute asset returns per period asset_returns = tick2ret(pricesTT(period_dates,:)); num_periods = height(asset_returns); % Brinson Return input Return = asset_returns.Variables'; % Brinson Category input Category = repmat(asset_categories,1,num_periods); % Brinson Period input num_assets = width(pricesTT); Period = (1:num_periods) .* ones(num_assets,1); % Brinson Name input Name = repmat(string(pricesTT.Properties.VariableNames(:)),1,num_periods); % Benchmark weights benchmark_positions = bt.Positions.(benchmark_name){period_dates(1:end-1),2:end}'; BenchmarkWeight = benchmark_positions ./ sum(benchmark_positions); % Strategy weights portfolio_positions = bt.Positions.(strategy_name){period_dates(1:end-1),2:end}'; PortfolioWeight = portfolio_positions ./ sum(portfolio_positions); % Aggregate the inputs into the Brinson asset table asset_table = table(Period(:),Name(:),Return(:),Category(:), ... PortfolioWeight(:),BenchmarkWeight(:),... VariableNames=["Period","Name","Return","Category", ... "PortfolioWeight","BenchmarkWeight"]); end
See Also
brinsonAttribution
| categoryAttribution
| categoryReturns
| categoryWeights
| totalAttribution
| summary