exponenta event banner

Обратные инвестиционные стратегии

В этом примере показано, как выполнить обратное тестирование стратегий портфеля с использованием структуры обратного тестирования, реализованной в 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, то портфель полностью инвестирован.

В этом примере существует пять стратегий обратного тестирования. Стратегии обратного тестирования присваивают веса основных средств с использованием следующих критериев:

  • Равновзвешенные

startEW = (start1, start2,..., startN), starti = 1N

  • Максимизация коэффициента Шарпа

λ SR = argmax ω{r′ωω′Qω'ω≥0,∑1Nωi=1,0≤ω≤0.1}, где r - вектор ожидаемых возвращений, а Q - ковариационная матрица возвращаемых активов.

  • Обратная дисперсия

startIV = (start1, start2,..., startN), starti = (startii-1) ∑i=1Nσii-1, где startii - диагональные элементы ковариационной матрицы возврата актива.

  • Оптимизация портфеля Markowitz (максимизация доходности и минимизация риска с фиксированным коэффициентом неприятия риска)

RMkwtz=maxω{r′ω - λω′Qω | ω≥0,∑1Nωi=1,0≤ω≤0.1}, где λ - коэффициент риска-неприятия.

  • Надежная оптимизация с неопределенностью ожидаемых результатов

  • Надежная стратегия оптимизации портфеля, в отличие от детерминированной формулировки Марковица, учитывает неопределенность ожидаемых доходностей активов и их отклонения и ковариации. Вместо моделирования неизвестных значений (например, ожидаемых возвращений) в виде одной точки, обычно представляемой средним значением, вычисленным из прошлого, неизвестные задаются как набор значений, которые содержат наиболее вероятные возможные реализации, r={r'r∈S (r0)}.

В этом случае ожидаемая отдача определяется не детерминированным вектором r0, а областью S (r0) вокруг вектора r0.

С учетом этого существует несколько способов переформулировать проблему оптимизации портфеля. Одним из наиболее часто используемых методов является формулирование проблемы как задачи нахождения максимума и минимума:

Rrobust=maxωminr∈S (r0) {r′ω - λω′Qω | ω≥0,∑1Nωi=1,0≤ω≤0.1}

В этом примере область неопределенности S (r0) задана как эллипсоид:

S (r0) = {r | (r-r0) Startr-1 (r-r0) ≤κ2}

В данном случае, λ - это коэффициент отклонения от неопределенности, который определяет, насколько широка область неопределенности, а Startr является матрицей ошибок оценки в ожидаемых результатах r.

С добавлением эллипсоидной неопределенности к модели Марковица задача надежной оптимизации переформулируется как:

Rrobust=maxω{r′ω - λω′Qω-kz | ω≥0, z≥0, ω′Σrω-z2≤0, ∑1Nωi=1,0≤ω≤0.1}

Внедрение функций перебалансировки стратегии

Основная логика каждой стратегии реализуется в функции ребалансировки. Функция ребалансировки - это определяемая пользователем функция 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);

Figure contains an object of type heatmap. The chart of type heatmap has title Initial Asset Allocations.

Создание стратегий бэктеста

Для использования стратегий в инфраструктуре бэктестинга необходимо создать 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)

Figure contains an axes. The axes with title Equity Curve contains 5 objects of type line. These objects represent Equal Weighted, Max Sharpe Ratio, Inverse Variance, Markowitz Optimization, Robust Optimization.

Перенос сводной таблицы для построения графиков определенных метрик может оказаться полезным.

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

Figure contains an axes. The axes with title Average Turnover contains an object of type bar.

Можно визуализировать изменения в распределениях стратегии с течением времени с помощью диаграммы областей ежедневных позиций основных средств. Для получения информации о assetAreaPlot см. раздел Локальные функции.

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

Figure contains an axes. The axes with title Max Sharpe Ratio 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.

Локальные функции

Далее следуют функции перебалансировки стратегии, а также функция переменных операционных затрат.

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

См. также

| | |

Связанные темы