В этом примере показано, как выполнить backtesting стратегий портфеля с помощью backtesting среды, реализованной в MATLAB®. Backtesting является полезным инструментом, чтобы выдержать сравнение, как инвестиционные стратегии выполняют по историческим или симулированным данным о рынке. Этот пример разрабатывает пять различных инвестиционных стратегий и затем сравнивает их эффективность после работания на основе однолетнего периода исторических данных о запасе. backtesting среда реализована в двух классах MATLAB®: backtestStrategy
и backtestEngine
.
Загрузите один год настроенных ценовых данных для 30 запасов. backtesting среды требуют настроенных цен активов, означая цены, настроенные для дивидендов, разделений или других событий. Цены должны храниться в MATLAB® timetable
с каждым столбцом, содержащим временные ряды цен активов на подходящий для инвестирования актив.
В данном примере используйте один год данных цен активов из запасов компонента промышленного индекса Доу-Джонса.
% 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)
ans=8×15 timetable
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
% View the size of the asset price data set.
numSample = size(pricesTT.Variables, 1);
numAssets = size(pricesTT.Variables, 2);
table(numSample, numAssets)
ans=1×2 table
numSample numAssets
_________ _________
251 15
Инвестиционные стратегии получают логику, используемую, чтобы принять решения распределения активов, в то время как backtest запускается. Как запуски backtest, каждой стратегии периодически дают возможность обновить ее выделение портфеля на основе запаздывающего состояния рынка, которое это делает путем установки вектора из весов актива. Веса актива представляют процент ликвидного капитала, инвестированного в каждый актив с каждым элементом в соответствии вектора весов соответствующему столбцу в активе pricesTT
расписание. Если суммой вектора весов является 1
, затем портфель полностью инвестируют.
В этом примере существует пять backtest стратегий. backtest стратегии присваивают веса актива с помощью следующего crieria:
Равно взвешенный
Максимизация отношения Шарпа
, где вектор из ожидаемых доходов и ковариационная матрица актива, возвращается.
Обратное отклонение
, где диагональные элементы актива, возвращают ковариационную матрицу.
Оптимизация портфеля Markowitz (максимизация возвращаются и минимизирующий риск с фиксированным коэффициентом нерасположенности к риску),
, где коэффициент нерасположенности к риску.
Устойчивая оптимизация с неопределенностью в ожидаемых доходах
Устойчивая стратегия оптимизации портфеля, в отличие от детерминированной формулировки Markowitz, учитывает ожидаемые доходы неопределенности активов и их отклонений и ковариаций. Вместо того, чтобы моделировать неизвестные значения (например, ожидаемые доходы) как одна точка, обычно представленная средним значением, вычисленным от прошлого, неизвестные заданы в виде набора значений, которые содержат наиболее вероятную реализацию, .
В этом случае ожидаемый доход задан не детерминированным вектором но областью вокруг вектора .
При принятии этого во внимание существует несколько способов переформулировать задачу оптимизации портфеля. Один из наиболее часто используемых методов должен сформулировать проблему как задачу нахождения максимум и минимум:
В этом примере, области неопределенности задан как эллипсоид:
Здесь, - коэффициент отвращения неопределенности, который задает, насколько широкий область неопределенности, и матрица ошибок расчета в ожидаемых доходах .
Со сложением неопределенности эллипсоида к модели Markowitz устойчивая задача оптимизации переформулирована как:
Логика ядра каждой стратегии реализована в функции восстановления равновесия. Функция восстановления равновесия является пользовательской функцией MATLAB®, которая задает, как стратегия выделяет капитал в портфеле. Функция восстановления равновесия является входным параметром к backtestStrategy
. Функция восстановления равновесия должна реализовать следующую фиксированную подпись:
function
new_weights = allocationFunctionName
(
current_weights,
pricesTimetable
)
Эта фиксированная подпись является API, который backtest среда использует при изменении баланса портфеля. Как запуски backtest, backtesting механизм вызывает функцию восстановления равновесия каждой стратегии, передающей в этих входных параметрах:
current_weights
— Текущие веса портфеля перед изменением баланса
pricesTimetable
— Объект расписания MATLAB®, содержащий прокручивающееся окно цен активов.
backtestStrategy
изменяйте баланс функция использует эту информацию, чтобы вычислить желаемые новые веса портфеля, которые возвращены в backtesting механизм в функциональном выходе new_weights
. Смотрите разделы Локальных функций для функции восстановления равновесия для каждой из этих пяти стратегий.
Используйте функции восстановления равновесия стратегии, чтобы вычислить начальные веса для каждой стратегии. Установка начальных весов важна, потому что в противном случае стратегии начинают backtest с 100% наличными, зарабатывая безрисковый уровень, до первой даты восстановления равновесия.
Этот пример использует первые 40 дней набора данных (приблизительно 2 месяца), чтобы инициализировать стратегии. backtest затем запущен по остающимся данным (приблизительно 10 месяцев).
warmupPeriod = 40;
Начальные веса вычисляются путем вызова backtestStrategy
восстановите равновесие функции таким же образом, что backtesting механизм вызовет ее. Для этого передайте в векторе из текущих весов (все нули, который является 100%-ми наличными деньгами), а также окно ценовых данных, которые стратегии будут использовать, чтобы установить желаемые веса (раздел данных прогрева). Используя функции восстановления равновесия, чтобы вычислить начальные веса таким образом не требуется. Начальные веса являются вектором из начальных весов портфеля и могут быть установлены в любое соответствующее значение. Функции восстановления равновесия в этом примере аппроксимируют состояние, в котором были бы стратегии, имел их уже выполнение в начале backtest.
% No current weights (100% cash position). current_weights = zeros(1,numAssets); % Warm-up partition of data set timetable. warmupTT = pricesTT(1:warmupPeriod,:); % Compute the initial portfolio weights for each strategy. equalWeight_initial = equalWeightFcn(current_weights,warmupTT); maxSharpeRatio_initial = maxSharpeRatioFcn(current_weights,warmupTT); inverseVariance_initial = inverseVarianceFcn(current_weights,warmupTT); markowitz_initial = markowitzFcn(current_weights,warmupTT); robustOptim_initial = robustOptimFcn(current_weights,warmupTT);
Визуализируйте начальные выделения веса из стратегий.
strategyNames = {'Equal Weighted', 'Max Sharpe Ratio', 'Inverse Variance', 'Markowitz Optimization','Robust Optimization'}; assetSymbols = pricesTT.Properties.VariableNames; initialWeights = [equalWeight_initial(:), maxSharpeRatio_initial(:), inverseVariance_initial(:), markowitz_initial(:), robustOptim_initial(:)]; heatmap(strategyNames, assetSymbols, initialWeights, 'title','Initial Asset Allocations','Colormap', parula);
Чтобы использовать стратегии в backtesting среде, необходимо создать backtestStrategy
объекты, один для каждой стратегии. backtestStrategy
функционируйте берет в качестве входа имя стратегии и восстанавливающий равновесие функции для каждой стратегии. Кроме того, backtestStrategy
может взять множество аргументов пары "имя-значение", чтобы задать различные варианты. Для получения дополнительной информации о создании backtest стратегии, смотрите backtestStrategy
.
Установите частоту восстановления равновесия, и lookback размер окна установлены в терминах количества временных шагов (то есть, строки pricesTT
расписание. Поскольку данные являются ежедневными ценовыми данными, задайте частоту восстановления равновесия и прокручивающийся lookback окно в днях.
% Rebalance approximately every 1 month (252 / 12 = 21). rebalFreq = 21; % Set the rolling lookback window to be at least 40 days and at most 126 % days (about 6 months). lookback = [40 126]; % Use a fixed transaction cost (buy and sell costs are both 0.5% of amount % traded). transactionsFixed = 0.005; % Customize the transaction costs using a function. See the % variableTransactionCosts function below for an example. transactionsVariable = @variableTransactionCosts; % The first two strategies use fixed transaction costs. The equal-weighted % strategy does not require a lookback window of trailing data, as its % allocation is fixed. strat1 = backtestStrategy('Equal Weighted', @equalWeightFcn,... 'RebalanceFrequency', rebalFreq,... 'LookbackWindow', 0,... 'TransactionCosts', transactionsFixed,... 'InitialWeights', equalWeight_initial); strat2 = backtestStrategy('Max Sharpe Ratio', @maxSharpeRatioFcn,... 'RebalanceFrequency', rebalFreq,... 'LookbackWindow', lookback,... 'TransactionCosts', transactionsFixed,... 'InitialWeights', maxSharpeRatio_initial); % Use variable transaction costs for the remaining strategies. strat3 = backtestStrategy('Inverse Variance', @inverseVarianceFcn,... 'RebalanceFrequency', rebalFreq,... 'LookbackWindow', lookback,... 'TransactionCosts', @variableTransactionCosts,... 'InitialWeights', inverseVariance_initial); strat4 = backtestStrategy('Markowitz Optimization', @markowitzFcn,... 'RebalanceFrequency', rebalFreq,... 'LookbackWindow', lookback,... 'TransactionCosts', transactionsFixed,... 'InitialWeights', markowitz_initial); strat5 = backtestStrategy('Robust Optimization', @robustOptimFcn,... 'RebalanceFrequency', rebalFreq,... 'LookbackWindow', lookback,... 'TransactionCosts', transactionsFixed,... 'InitialWeights', robustOptim_initial); % Aggregate the strategy objects into an array. strategies = [strat1, strat2, strat3, strat4, strat5];
Используйте следующий рабочий процесс для backtest стратегии с backtestEngine
.
backtestEngine
функционируйте берет в качестве входа массив backtestStrategy
объекты. Кроме того, при использовании backtestEngine
, можно установить несколько опций, таких как безрисковый уровень и начальная стоимость портфеля. Когда безрисковый уровень задан в пересчитанных на год терминах, backtestEngine
использование Basis
свойство установить базу ежедневного расчета процентов. Для получения дополнительной информации о создании backtesting механизмы, смотрите backtestEngine
.
% Risk-free rate is 1% annualized annualRiskFreeRate = 0.01; % Create the backtesting engine object backtester = backtestEngine(strategies, 'RiskFreeRate', annualRiskFreeRate)
backtester = backtestEngine with properties: Strategies: [1x5 backtestStrategy] RiskFreeRate: 0.0100 CashBorrowRate: 0 RatesConvention: "Annualized" Basis: 0 InitialPortfolioValue: 10000 NumAssets: [] Returns: [] Positions: [] Turnover: [] BuyCost: [] SellCost: []
Используйте runBacktest
запускать backtest использование раздела тестовых данных. Используйте runBacktest
аргумент пары "имя-значение" 'Start'
избегать предварительного смещения (то есть, "видя будущее"). Начните backtest в конце периода "прогрева". Выполнение backtest заполняет пустые поля backtestEngine
объект каждодневными результатами backtest.
backtester = runBacktest(backtester, pricesTT, 'Start', warmupPeriod)
backtester = backtestEngine with properties: Strategies: [1x5 backtestStrategy] RiskFreeRate: 0.0100 CashBorrowRate: 0 RatesConvention: "Annualized" Basis: 0 InitialPortfolioValue: 10000 NumAssets: 15 Returns: [211x5 timetable] Positions: [1x1 struct] Turnover: [211x5 timetable] BuyCost: [211x5 timetable] SellCost: [211x5 timetable]
Используйте summary
функция, чтобы сгенерировать таблицу эффективности стратегии заканчивается для backtest.
summaryByStrategies = summary(backtester)
summaryByStrategies=9×5 table
Equal_Weighted Max_Sharpe_Ratio Inverse_Variance Markowitz_Optimization Robust_Optimization
______________ ________________ ________________ ______________________ ___________________
TotalReturn 0.18745 0.14991 0.15906 0.17404 0.15655
SharpeRatio 0.12559 0.092456 0.12179 0.10339 0.11442
Volatility 0.0063474 0.0070186 0.0055626 0.0072466 0.0058447
AverageTurnover 0.00087623 0.0065762 0.0028666 0.0058268 0.0025172
MaxTurnover 0.031251 0.239 0.09114 0.21873 0.073746
AverageReturn 0.00083462 0.00068672 0.0007152 0.00078682 0.00070651
MaxDrawdown 0.072392 0.084768 0.054344 0.085544 0.064904
AverageBuyCost 0.047298 0.3449 0.15228 0.3155 0.1328
AverageSellCost 0.047298 0.3449 0.22842 0.3155 0.1328
Подробные результаты backtest, включая ежедневную газету возвращается, положения актива и оборот, хранятся в свойствах backtestEngine
объект.
Используйте equityCurve
построить кривую акции для пяти различных инвестиционных стратегий.
equityCurve(backtester)
Перемещение сводной таблицы, чтобы сделать графики определенных метрик может быть полезным.
% Transpose the summary table to plot the metrics. summaryByMetrics = rows2vars(summaryByStrategies); summaryByMetrics.Properties.VariableNames{1} = 'Strategy'
summaryByMetrics=5×10 table
Strategy TotalReturn SharpeRatio Volatility AverageTurnover MaxTurnover AverageReturn MaxDrawdown AverageBuyCost AverageSellCost
__________________________ ___________ ___________ __________ _______________ ___________ _____________ ___________ ______________ _______________
{'Equal_Weighted' } 0.18745 0.12559 0.0063474 0.00087623 0.031251 0.00083462 0.072392 0.047298 0.047298
{'Max_Sharpe_Ratio' } 0.14991 0.092456 0.0070186 0.0065762 0.239 0.00068672 0.084768 0.3449 0.3449
{'Inverse_Variance' } 0.15906 0.12179 0.0055626 0.0028666 0.09114 0.0007152 0.054344 0.15228 0.22842
{'Markowitz_Optimization'} 0.17404 0.10339 0.0072466 0.0058268 0.21873 0.00078682 0.085544 0.3155 0.3155
{'Robust_Optimization' } 0.15655 0.11442 0.0058447 0.0025172 0.073746 0.00070651 0.064904 0.1328 0.1328
% Compare the strategy turnover. names = [backtester.Strategies.Name]; nameLabels = strrep(names,'_',' '); bar(summaryByMetrics.AverageTurnover) title('Average Turnover') ylabel('Daily Turnover (%)') set(gca,'xticklabel',nameLabels)
Можно визуализировать изменение в выделениях стратегии в зависимости от времени с помощью диаграммы областей ежедневных положений актива. Для получения информации о assetAreaPlot
функционируйте, смотрите раздел Local Functions.
strategyName = 'Max_Sharpe_Ratio';
assetAreaPlot (backtester, strategyName)
Стратегия, восстанавливающая равновесие функций, а также переменной функции операционных издержек, следует.
function new_weights = equalWeightFcn(current_weights, 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(current_weights, pricesTT) % Mean-variance portfolio allocation nAssets = size(pricesTT, 2); assetReturns = tick2ret(pricesTT); % Max 25% into a single asset (including cash) p = Portfolio('NumAssets',nAssets,... 'LowerBound',0,'UpperBound',0.1,... 'LowerBudget',1,'UpperBudget',1); p = estimateAssetMoments(p, assetReturns{:,:}); new_weights = estimateMaxSharpeRatio(p); end
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
function new_weights = robustOptimFcn(current_weights, pricesTT) % Robust portfolio allocation nAssets = size(pricesTT, 2); assetReturns = tick2ret(pricesTT); Q = cov(table2array(assetReturns)); SIGMAx = diag(diag(Q)); % Robust aversion coefficient k = 1.1; % Robust aversion coefficient lambda = 0.05; rPortfolio = mean(table2array(assetReturns))'; % Create the optimization problem pRobust = optimproblem('Description','Robust Portfolio'); % Define the variables % xRobust - x allocation vector xRobust = optimvar('x',nAssets,1,'Type','continuous','LowerBound',0.0,'UpperBound',0.1); zRobust = optimvar('z','LowerBound',0); % Define the budget constraint pRobust.Constraints.budget = sum(xRobust) == 1; % Define the robust constraint pRobust.Constraints.robust = xRobust'*SIGMAx*xRobust - zRobust*zRobust <=0; pRobust.Objective = -rPortfolio'*xRobust + k*zRobust + lambda*xRobust'*Q*xRobust; x0.x = zeros(nAssets,1); x0.z = 0; opt = optimoptions('fmincon','Display','off'); [solRobust,~,~] = solve(pRobust,x0,'Options',opt); new_weights = solRobust.x; end
function new_weights = markowitzFcn(current_weights, pricesTT) % Robust portfolio allocation nAssets = size(pricesTT, 2); assetReturns = tick2ret(pricesTT); Q = cov(table2array(assetReturns)); % Risk aversion coefficient lambda = 0.05; rPortfolio = mean(table2array(assetReturns))'; % Create the optimization problem pMrkwtz = optimproblem('Description','Markowitz Mean Variance Portfolio '); % Define the variables % xRobust - x allocation vector xMrkwtz = optimvar('x',nAssets,1,'Type','continuous','LowerBound',0.0,'UpperBound',0.1); % Define the budget constraint pMrkwtz.Constraints.budget = sum(xMrkwtz) == 1; % Define the Markowitz objective pMrkwtz.Objective = -rPortfolio'*xMrkwtz + lambda*xMrkwtz'*Q*xMrkwtz; x0.x = zeros(nAssets,1); opt = optimoptions('quadprog','Display','off'); [solMrkwtz,~,~] = solve(pMrkwtz,x0,'Options',opt); new_weights = solMrkwtz.x; end
function [buy, sell] = variableTransactionCosts(deltaPositions) % Variable transaction cost function % % This function is an example of how to compute variable transaction costs. % % Compute scaled transaction costs based on the change in market value of % each asset after a rebalance. Costs are computed at the following rates: % % Buys: % $0-$10,000 : 0.5% % $10,000+ : 0.35% % Sells: % $0-$1,000 : 0.75% % $1,000+ : 0.5% buy = zeros(1,numel(deltaPositions)); sell = zeros(1,numel(deltaPositions)); % Buys idx = 0 < deltaPositions & deltaPositions < 1e4; buy(idx) = 0.005 * deltaPositions(idx); % 50 basis points idx = 1e4 <= deltaPositions; buy(idx) = 0.0035 * deltaPositions(idx); % 35 basis ponits buy = sum(buy); % Sells idx = -1e3 < deltaPositions & deltaPositions < 0; sell(idx) = 0.0075 * -deltaPositions(idx); % 75 basis points idx = deltaPositions <= -1e3; sell(idx) = 0.005 * -deltaPositions(idx); % 50 basis points sell = sum(sell); 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
backtestEngine
| backtestStrategy
| runBacktest
| summary