Инвестиционные стратегии Backtest

В этом примере показано, как выполнить обратное тестирование стратегий портфеля с помощью среды обратного тестирования, реализованной в MATLAB ®. Backtesting является полезным инструментом для сравнения того, как инвестиционные стратегии работают по сравнению с историческими или моделируемыми рыночными данными. В этом примере разрабатываются пять различных инвестиционных стратегий, а затем сравниваются их эффективность после прогона за один год исторических данных о запасах. Среда обратного тестирования реализована в двух классах MATLAB ®: backtestStrategy и backtestEngine.

Загрузка данных

Загрузите один год скорректированных ценовых данных для 30 запасов. Среды обратных тестов требуются скорректированные цены активов, т.е. цены, скорректированные для дивидендов, разделений или других событий. Цены должны храниться в timetable MATLAB ® каждый столбец содержит временные ряды цен активов для инвестиционного актива.

В данном примере используется один год данных цены актива из запасов компонентов Industrial Average 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    

Определите стратегии

Инвестиционные стратегии захватывают логику, используемую для принятия решений о распределении активов во время бэктеста. При запусках backtest каждой стратегии периодически предоставляется возможность обновить распределение портфеля на основе конечных рыночных условий, что она делает путем установки вектора весов активов. Веса активов представляют собой процент доступного капитала, вложенного в каждый актив, причем каждый элемент в векторе весов соответствует соответствующему столбцу в активе pricesTT timetable. Если сумма вектора весов 1, затем портфель полностью инвестируется.

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

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

ωEW=(ω1,ω2,...,ωN),ωi=1N

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

ωSR=argmax                  ω{rωωQω|ω0,1Nωi=1,0ω0.1}, где r является вектором ожидаемых возвратов и Q - ковариационная матрица возвратов активов.

  • Обратное отклонение

ωIV=(ω1,ω2,...,ωN),ωi=(σii-1)i=1Nσii-1, где σii являются диагональными элементами ковариационной матрицы возврата активов.

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

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

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

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

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

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

Rrobust=maxωminrS(r0){rω-λωQω|ω0,1Nωi=1,0ω0.1}

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

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

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

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

Rrobust=maxω{rω-λωQω-kz|ω0,z0,ωΣrω-z20,1Nωi=1,0ω0.1}

Реализация функций перерасчета стратегии

Основная логика каждой стратегии реализована в функции ребаланса. Функция ребаланса является пользовательской функцией MATLAB ®, которая определяет, как стратегия распределяет капитал в портфеле. Функция ребаланса является входным параметром к backtestStrategy. Функция ребаланса должна реализовать следующую фиксированную сигнатуру:

function new_weights = allocationFunctionName(current_weights, pricesTimetable)

Эта фиксированная сигнатура является API, который backtest framework использует при ребалансировке портфеля. Когда бэктест запускается, механизм обратного тестирования вызывает функцию ребаланса каждой стратегии, передая в этих входах:

  • current_weights - Текущие веса портфеля перед пересчетом баланса

  • pricesTimetable - объект расписания MATLAB ®, содержащий скользящее окно цен на активы.

The 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.

Создайте стратегии Backtest

Чтобы использовать стратегии в среде обратного тестирования, необходимо создать backtestStrategy объекты, по одному для каждой стратегии. The backtestStrategy функция принимает в качестве входов имя стратегии и функцию ребалансирования для каждой стратегии. Кроме того, backtestStrategy может принимать различные аргументы пары "имя-значение", чтобы задать различные опции. Для получения дополнительной информации о создании стратегий backtest смотрите backtestStrategy.

Установите частоту ребаланса и размер окна поиска установлены в терминах количества временных шагов (то есть строк pricesTT timetable). Поскольку данные являются данными дневной цены, задайте частоту ребаланса и поисковое окно качения в днях.

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

Задайте Backtesting Engine

The backtestEngine функция принимает как вход массив backtestStrategy объекты. Кроме того, при использовании backtestEngineможно задать несколько опций, таких как безрисковая ставка и начальное значение портфеля. Когда безрисковая ставка указывается в годовом исчислении, backtestEngine использует Basis свойство, чтобы задать значение параметра day count. Для получения дополнительной информации о создании узлов обратного тестирования смотрите 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 заполняет пустые поля 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]

Исследуйте результаты Backtest

Используйте 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 (backter, 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

См. также

| | |

Похожие темы