В этом примере показано, как выполнить обратное тестирование стратегий портфеля с использованием структуры обратного тестирования, реализованной в MATLAB ®. Обратное тестирование является полезным инструментом для сравнения эффективности инвестиционных стратегий по сравнению с историческими или смоделированными рыночными данными. В этом примере разрабатываются пять различных инвестиционных стратегий, а затем сравниваются их результаты после выполнения в течение одного года исторических данных о запасах. Платформа обратного тестирования реализована в двух классах MATLAB ®:backtestStrategy и backtestEngine.
Загрузка данных скорректированной цены за один год для 30 акций. В рамках обратного тестирования требуются скорректированные цены активов, т.е. цены, скорректированные на дивиденды, разделения или другие события. Цены должны храниться в MATLAB ®timetable каждый столбец содержит временной ряд цен на активы для инвестируемого актива.
В этом примере следует использовать данные цены основного средства за один год из запасов компонентов в промышленном среднем Dow Jones.
% 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
Инвестиционные стратегии отражают логику, используемую для принятия решений о распределении активов во время выполнения бэктеста. По мере выполнения бэктеста каждой стратегии периодически предоставляется возможность обновлять распределение портфеля на основе конечных рыночных условий, что она и делает, устанавливая вектор весов активов. Веса основных средств представляют собой процент доступного капитала, вложенного в каждое основное средство, причем каждый элемент вектора весов соответствует соответствующему столбцу основного средства. pricesTT расписание. Если сумма весов вектора равна 1, то портфель полностью инвестирован.
В этом примере существует пять стратегий обратного тестирования. Стратегии обратного тестирования присваивают веса основных средств с использованием следующих критериев:
Равновзвешенные
starti = 1N
Максимизация коэффициента Шарпа
возвращений, а Q - ковариационная матрица возвращаемых активов.
Обратная дисперсия
startN∑i=1Nσii-1, где startii - диагональные элементы ковариационной матрицы возврата актива.
Оптимизация портфеля Markowitz (максимизация доходности и минимизация риска с фиксированным коэффициентом неприятия риска)
}, где - коэффициент риска-неприятия.
Надежная оптимизация с неопределенностью ожидаемых результатов
Надежная стратегия оптимизации портфеля, в отличие от детерминированной формулировки Марковица, учитывает неопределенность ожидаемых доходностей активов и их отклонения и ковариации. Вместо моделирования неизвестных значений (например, ожидаемых возвращений) в виде одной точки, обычно представляемой средним значением, вычисленным из прошлого, неизвестные задаются как набор значений, которые содержат наиболее вероятные возможные реализации, r={r'r∈S (r0
В этом случае ожидаемая отдача определяется не детерминированным вектором , а областью ) вокруг вектора r0.
С учетом этого существует несколько способов переформулировать проблему оптимизации портфеля. Одним из наиболее часто используемых методов является формулирование проблемы как задачи нахождения максимума и минимума:
ω≥0,∑1Nωi=1,0≤ω≤0.1}
В этом примере область неопределенности ) задана как эллипсоид:
r-r0) ≤κ2}
В данном случае, - это коэффициент отклонения от неопределенности, который определяет, насколько широка область неопределенности, а является матрицей ошибок оценки в ожидаемых результатах .
С добавлением эллипсоидной неопределенности к модели Марковица задача надежной оптимизации переформулируется как:
}
Основная логика каждой стратегии реализуется в функции ребалансировки. Функция ребалансировки - это определяемая пользователем функция MATLAB ®, которая определяет, как стратегия распределяет капитал в портфеле. Функция ребалансировки является входным аргументом дляbacktestStrategy. Функция ребалансировки должна реализовывать следующую фиксированную сигнатуру:
function new_weights = allocationFunctionName(current_weights, pricesTimetable)
Эта фиксированная сигнатура представляет собой API-интерфейс, используемый платформой backtest при ребалансировке портфеля. По мере выполнения обратного теста механизм обратного тестирования вызывает функцию ребалансировки каждой стратегии, передавая следующие входные данные:
current_weights - Текущие веса портфеля перед перебалансировкой
pricesTimetable - Объект расписания MATLAB ®, содержащий скользящее окно цен основных средств.
backtestStrategy функция ребалансировки использует эту информацию для вычисления требуемых новых весов портфеля, которые возвращаются в механизм бэктестирования в выводе функции new_weights. См. разделы Локальные функции для функции ребалансировки для каждой из пяти стратегий.
Используйте функции перебалансировки стратегии для вычисления начальных весов для каждой стратегии. Установка начальных весов важна, потому что в противном случае стратегии начинают обратный тест со 100% наличными, зарабатывая безрисковую ставку, до первой даты ребаланса.
В этом примере для инициализации стратегий используются первые 40 дней набора данных (около 2 месяцев). Затем выполняется обратный тест оставшихся данных (около 10 месяцев).
warmupPeriod = 40;
Начальные веса вычисляются путем вызова backtestStrategy функция ребалансировки таким же образом, как ее вызовет механизм обратного тестирования. Для этого передайте вектор текущих весов (все нули, то есть 100% наличные), а также окно данных о ценах, которые стратегии будут использовать для установки требуемых весов (раздел данных прогрева). Использование функций ребалансировки для вычисления начальных весов таким образом не требуется. Начальные веса являются вектором начальных весов портфеля и могут быть установлены на любое соответствующее значение. Функции ребалансировки в этом примере приближаются к состоянию, в котором находились бы стратегии, если бы они уже выполнялись в начале бэктеста.
% 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);
Для использования стратегий в инфраструктуре бэктестинга необходимо создать backtestStrategy объекты, по одному для каждой стратегии. backtestStrategy функция принимает в качестве входных данных имя стратегии и функцию ребалансировки для каждой стратегии. Кроме того, backtestStrategy может использовать различные аргументы пары «имя-значение» для указания различных опций. Дополнительные сведения о создании стратегий обратного тестирования см. в разделе backtestStrategy.
Установите частоту перебалансировки и размер окна обратного просмотра в виде количества временных шагов (то есть строк pricesTT расписание). Поскольку данные являются ежедневными данными о ценах, укажите частоту перебалансировки и скользящее окно обратного просмотра в днях.
% 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];
Используйте следующий workflow-процесс для обратного тестирования стратегий с помощью backtestEngine.
backtestEngine функция принимает в качестве входных данных массив backtestStrategy объекты. Кроме того, при использовании backtestEngine, можно задать несколько вариантов, таких как безрисковая ставка и начальная стоимость портфеля. Когда безрисковая ставка указана в годовом выражении, backtestEngine использование Basis для установки соглашения о количестве дней. Дополнительные сведения о создании механизмов обратного тестирования см. в разделе 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 для выполнения обратного теста с использованием раздела тестовых данных. Используйте runBacktest аргумент пары имя-значение 'Start'чтобы избежать предвзятого взгляда (то есть «видеть будущее»). Начало бэктеста в конце периода «разогрева». Выполнение команды backtest заполняет пустые поля backtestEngine объект с результатами ежедневного бэктеста.
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 для создания таблицы результатов выполнения стратегии для бэктеста.
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
Подробные результаты бэктеста, включая дневную доходность, позиции активов и оборот, хранятся в свойствах 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 см. раздел Локальные функции.
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