Этот пример показывает, как выполнить обратную проверку портфельных стратегий, которые включают инвестиционные сигналы в свою торговую стратегию. Сигналы термина включают любую информацию, которую автор стратегии должен принять в отношении торговых решений за пределами ценовой истории активов. Такая информация может включать технические показатели, выходы моделей машинного обучения, данные о настроениях, макроэкономические данные и так далее. Этот пример использует три простые инвестиционные стратегии, основанные на данных производного сигнала:
Кроссоверы скользящего среднего
Сходимость/расхождение скользящего среднего
Относительный индекс прочности
В этом примере можно запустить бэктест с помощью этих стратегий в течение одного года данных запаса. Затем анализируются результаты для сравнения эффективности каждой стратегии.
Даже при том, что технические индикаторы обычно не используются в качестве автономных торговых стратегий, этот пример использует эти стратегии, чтобы продемонстрировать, как создать инвестиционные стратегии на основе сигнальных данных при использовании backtestEngine
объект в MATLAB ®.
Загрузить скорректированные данные о ценах по 15 запасам за 2006 год. Этот пример использует небольшой набор инвестиционных активов для читабельности.
Ознакомьтесь с таблицей ежедневных скорректированных цен на акции DJIA 2006 года.
T = readtable('dowPortfolio.xlsx');
Для удобства чтения используйте только 15 из 30 запасов компонентов DJI.
symbols = ["AA","CAT","DIS","GM","HPQ","JNJ","MCD","MMM","MO","MRK","MSFT","PFE","PG","T","XOM"];
Обрезать таблицу, чтобы сохранить только даты и выбранные запасы.
timeColumn = "Dates";
T = T(:,[timeColumn symbols]);
Преобразуйте данные в расписание.
pricesTT = table2timetable(T,'RowTimes','Dates');
Просмотрите структуру расписания цен.
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
Визуализируйте корреляцию и общий возврат каждого запаса в наборе данных.
% Visualize the correlation between the 15 stocks. returns = tick2ret(pricesTT); stockCorr = corr(returns.Variables); heatmap(symbols,symbols,stockCorr,'Colormap',parula);
% Visualize the performance of each stock over the range of price data. totalRet = ret2tick(returns); plot(totalRet.Dates,totalRet.Variables); legend(symbols,'Location','NW'); title('Growth of $1 for Each Stock') ylabel('$')
% Get the total return of each stock for the duration of the data set.
totalRet(end,:)
ans=1×15 timetable
Dates AA CAT DIS GM HPQ JNJ MCD MMM MO MRK MSFT PFE PG T XOM
___________ ______ ______ ______ ______ ______ ______ ______ ______ ______ ______ ______ ______ ______ ______ _____
29-Dec-2006 1.0254 1.0781 1.4173 1.6852 1.4451 1.0965 1.3548 1.0087 1.1946 1.3856 1.1287 1.1304 1.1164 1.5181 1.336
В дополнение к историческим скорректированным ценам активов среда обратного тестирования позволяет опционально задавать сигнальные данные при запуске бэктеста. Задайте данные сигнала так же, как и цены, с помощью MATLAB ® timetable
. Измерение «время» расписания сигнала должно совпадать с размерностью расписания цен - то есть строки каждой таблицы должны иметь совпадающие значения datetime для Time
столбец.
Этот пример строит сигнальное расписание, чтобы поддержать каждую из трех инвестиционных стратегий:
Стратегия простого кроссовера скользящего среднего (SMA)
Стратегия сходимости/расхождения скользящего среднего (MACD)
Стратегия индекса относительной прочности (RSI)
Каждая стратегия имеет расписание сигналов, которые предварительно сжаты. Прежде чем вы запустите backtest, вы объединяете три отдельных расписания сигналов в одно совокупное расписание сигналов, которое используется для backtest.
Индикатор SMA использует 5-дневные и 20-дневные простые скользящие средние значения для принятия решений о покупке и продаже. Когда 5-дневный SMA пересекает 20-дневный SMA (движение вверх), запас покупается. Когда 5-дневный SMA переходит ниже 20-дневного SMA, акции продаются.
% Create SMA timetables using the movavg function. sma5 = movavg(pricesTT,'simple',5); sma20 = movavg(pricesTT,'simple',20);
Составьте расписание сигнала индикатора SMA.
smaSignalNameEnding = '_SMA5over20'; smaSignal = timetable; for i = 1:numel(symbols) symi = symbols(i); % Build a timetable for each symbol, then aggregate them together. smaSignali = timetable(pricesTT.Dates,... double(sma5.(symi) > sma20.(symi)),... 'VariableNames',{sprintf('%s%s',symi,smaSignalNameEnding)}); % Use the synchronize function to merge the timetables together. smaSignal = synchronize(smaSignal,smaSignali); end
Расписание сигнала SMA содержит индикатор со значением 1
когда 5-дневное скользящее среднее значение выше 20-дневного скользящего среднего значения для каждого актива, и a 0
в противном случае. Имена столбцов для каждого индикатора запаса являются [символом запаса] SMA5over20
. The backtestStrategy
объект принимает торговые решения на основе этих событий кроссовера.
Просмотрите структуру расписания сигнала SMA.
head(smaSignal)
ans=8×15 timetable
Time AA_SMA5over20 CAT_SMA5over20 DIS_SMA5over20 GM_SMA5over20 HPQ_SMA5over20 JNJ_SMA5over20 MCD_SMA5over20 MMM_SMA5over20 MO_SMA5over20 MRK_SMA5over20 MSFT_SMA5over20 PFE_SMA5over20 PG_SMA5over20 T_SMA5over20 XOM_SMA5over20
___________ _____________ ______________ ______________ _____________ ______________ ______________ ______________ ______________ _____________ ______________ _______________ ______________ _____________ ____________ ______________
03-Jan-2006 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
04-Jan-2006 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
05-Jan-2006 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
06-Jan-2006 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
09-Jan-2006 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0
10-Jan-2006 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1
11-Jan-2006 0 1 1 1 1 1 1 0 1 1 1 1 1 1 1
12-Jan-2006 0 1 1 1 1 1 1 0 1 1 1 1 1 1 1
Постройте график сигнала для одного актива, чтобы просмотреть торговую частоту.
plot(smaSignal.Time,smaSignal.CAT_SMA5over20); ylim([-0.5, 1.5]); ylabel('SMA 5 > SMA 20'); title(sprintf('SMA 5 over 20 for CAT'));
Метрику MACD можно использовать различными способами. Часто MACD сравнивается с собственным экспоненциальным скользящим средним значением, но для этого примера MACD служит триггером для сигнала покупки, когда MACD поднимается выше 0
. Позиция продается, когда MACD падает назад ниже 0
.
% Create a timetable of the MACD metric using the MACD function.
macdTT = macd(pricesTT);
Составьте расписание сигнала индикации MACD.
macdSignalNameEnding = '_MACD'; macdSignal = timetable; for i = 1:numel(symbols) symi = symbols(i); % Build a timetable for each symbol, then aggregate the symbols together. macdSignali = timetable(pricesTT.Dates,... double(macdTT.(symi) > 0),... 'VariableNames',{sprintf('%s%s',symi,macdSignalNameEnding)}); macdSignal = synchronize(macdSignal,macdSignali); end
Таблица сигналов MACD содержит столбец для каждого основного средства с именем [символ запаса] MACD
. Каждый сигнал имеет значение 1
когда MACD запаса выше 0
. Сигнал имеет значение 0
когда MACD запаса падает ниже 0
.
head(macdSignal)
ans=8×15 timetable
Time AA_MACD CAT_MACD DIS_MACD GM_MACD HPQ_MACD JNJ_MACD MCD_MACD MMM_MACD MO_MACD MRK_MACD MSFT_MACD PFE_MACD PG_MACD T_MACD XOM_MACD
___________ _______ ________ ________ _______ ________ ________ ________ ________ _______ ________ _________ ________ _______ ______ ________
03-Jan-2006 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
04-Jan-2006 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
05-Jan-2006 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
06-Jan-2006 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
09-Jan-2006 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
10-Jan-2006 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
11-Jan-2006 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
12-Jan-2006 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Подобно SMA, постройте график сигнала для одного актива, чтобы просмотреть торговую частоту.
plot(macdSignal.Time,macdSignal.CAT_MACD) ylim([-0.5, 1.5]); ylabel('MACD > 0'); title(sprintf('MACD > 0 for CAT'));
RSI является метрикой для захвата импульса. Распространенным эвристическим является покупка, когда RSI падает ниже 30
и продавать, когда RSI поднимается выше 70
.
rsiSignalNameEnding = '_RSI'; rsiSignal = timetable; for i = 1:numel(symbols) symi = symbols(i); rsiValues = rsindex(pricesTT.(symi)); rsiBuySell = zeros(size(rsiValues)); rsiBuySell(rsiValues < 30) = 1; rsiBuySell(rsiValues > 70) = -1; % Build a timetable for each symbol, then aggregate the symbols together. rsiSignali = timetable(pricesTT.Dates,... rsiBuySell,... 'VariableNames',{sprintf('%s%s',symi,rsiSignalNameEnding)}); rsiSignal = synchronize(rsiSignal,rsiSignali); end
Сигнал RSI принимает значение 1
(указывает на сигнал покупки), когда значение RSI для акции падает ниже 30
. Сигнал принимает значение -1
(что указывает на сигнал продажи), когда RSI для акции поднимается выше 70
. В противном случае сигнал принимает значение 0
, что указывает на отсутствие действия.
Постройте график сигнала для одного актива, чтобы просмотреть торговую частоту.
plot(rsiSignal.Time,rsiSignal.CAT_RSI) ylim([-1.5, 1.5]); ylabel('RSI Buy/Sell Signal'); title(sprintf('RSI Buy/Sell Signal for CAT'));
Создайте стратегии для backtestStrategy
объект с использованием функций ребаланса, определенных в разделе Локальные функции. Каждая стратегия использует функцию ребаланса для принятия торговых решений на основе соответствующих сигналов.
Сигналы требуют достаточных конечных данных для вычисления торговых сигналов (для примера, вычисления SMA20
для дня X требуются цены от 20 дней до дня X). Все конечные данные регистрируются в предварительно рассчитанных торговых сигналах. Таким образом, фактическим стратегиям нужно только 2-дневное окно поиска, чтобы принять торговые решения, чтобы оценить, когда сигналы пересекают пороги торговли.
Все стратегии оплачивают 25 базисные точки транзакционных издержек при покупке и продаже.
Начальные веса вычисляются на основе значений сигналов через 20 торговых дней. Бэктест начинается после этого 20-дневного периода инициализации.
tradingCosts = 0.0025; % Use the crossoverRebalanceFunction for both the SMA % strategy as well as the MACD strategy. This is because they both trade % on their respective signals in the same way (buy when signal goes from % 0->1, sell when signal goes from 1->0). Build an anonymous % function for the rebalance functions of the strategies that calls the % shared crossoverRebalanceFcn() with the appropriate signal name string % for each strategy. % Each anonymous function takes the current weights (w), prices (p), % and signal (s) data from the backtest engine and passes it to the % crossoverRebalanceFcn function with the signal name string. smaInitWeights = computeInitialWeights(smaSignal(20,:)); smaRebalanceFcn = @(w,p,s) crossoverRebalanceFcn(w,p,s,smaSignalNameEnding); smaStrategy = backtestStrategy('SMA',smaRebalanceFcn,... 'TransactionCosts',tradingCosts,... 'LookbackWindow',2,... 'InitialWeights',smaInitWeights); macdInitWeights = computeInitialWeights(macdSignal(20,:)); macdRebalanceFcn = @(w,p,s) crossoverRebalanceFcn(w,p,s,macdSignalNameEnding); macdStrategy = backtestStrategy('MACD',macdRebalanceFcn,... 'TransactionCosts',tradingCosts,... 'LookbackWindow',2,... 'InitialWeights',macdInitWeights); % The RSI strategy uses its signal differently, buying on a 0->1 % transition and selling on a 0->-1 transition. This logic is captured in % the rsiRebalanceFcn dunction defined in the Local Functions section. rsiInitWeights = computeInitialWeights(rsiSignal(20,:)); rsiStrategy = backtestStrategy('RSI',@rsiRebalanceFcn,... 'TransactionCosts',tradingCosts,... 'LookbackWindow',2,... 'InitialWeights',rsiInitWeights);
В качестве бенчмарка в этом примере также запускается простая равновзвешенная стратегия, чтобы определить, дают ли торговые сигналы ценное представление о будущих возвратах активов. Эталонная стратегия восстанавливается каждые четыре недели.
% The equal weight strategy requires no history, so set LookbackWindow to 0. benchmarkStrategy = backtestStrategy('Benchmark',@equalWeightFcn,... 'TransactionCosts',tradingCosts,... 'RebalanceFrequency',20,... 'LookbackWindow',0);
Агрегируйте каждое из отдельных расписаний сигналов в одно расписание обратного сигнала.
% Combine the three signal timetables.
signalTT = timetable;
signalTT = synchronize(signalTT, smaSignal);
signalTT = synchronize(signalTT, macdSignal);
signalTT = synchronize(signalTT, rsiSignal);
Использование backtestEngine
чтобы создать механизм обратного тестирования и затем использовать runBacktest
чтобы запустить бэктест. Безрисковая ставка, заработанная на неинвестированных денежных средствах, составляет 1% в годовом исчислении.
% Put the benchmark strategy and three signal strategies into an array. strategies = [benchmarkStrategy smaStrategy macdStrategy rsiStrategy]; % Create the backtesting engine. bt = backtestEngine(strategies,'RiskFreeRate',0.01)
bt = backtestEngine with properties: Strategies: [1x4 backtestStrategy] RiskFreeRate: 0.0100 CashBorrowRate: 0 RatesConvention: "Annualized" Basis: 0 InitialPortfolioValue: 10000 NumAssets: [] Returns: [] Positions: [] Turnover: [] BuyCost: [] SellCost: []
% Start with the end of the initial weights calculation warm-up period. startIdx = 20; % Run the backtest. bt = runBacktest(bt,pricesTT,signalTT,'Start',startIdx);
Использование equityCurve
для построения кривых справедливости стратегии, чтобы визуализировать их эффективность на заднем тесте.
equityCurve(bt)
Как упоминалось ранее, эти стратегии обычно не используются в качестве автономных торговых сигналов. На самом деле эти три стратегии работают хуже, чем простая эталонная стратегия на период 2006 года. Можно визуализировать изменение присвоения стратегии с течением времени с помощью территориального графика ежедневных позиций основных средств. Для этого используйте assetAreaPlot
вспомогательная функция, заданная в разделе «Локальные функции».
strategyName = 'Benchmark';
assetAreaPlot (bt, strategyName)
Во второй половине 2006 года на широком рынке акций было очень бычье 6 месяцев, и все три из этих стратегий не смогли полностью отразить этот рост, оставив слишком много капитала в наличных средствах. Хотя ни одна из этих стратегий не показала хороших результатов сама по себе, этот пример демонстрирует, как можно создавать основанные на сигналах торговые стратегии и проверять их на эффективность.
Следующая функция вычисления начального веса, а также функции ребалансировки стратегии.
function initial_weights = computeInitialWeights(signals) % Compute initial weights based on most recent signal. nAssets = size(signals,2); final_signal = signals{end,:}; buys = final_signal == 1; initial_weights = zeros(1,nAssets); initial_weights(buys) = 1 / nAssets; end
function new_weights = crossoverRebalanceFcn(current_weights, pricesTT, signalTT, signalNameEnding) % Signal crossover rebalance function. % Build cell array of signal names that correspond to the crossover signals. symbols = pricesTT.Properties.VariableNames; signalNames = cellfun(@(s) sprintf('%s%s',s,signalNameEnding), symbols, 'UniformOutput', false); % Pull out the relevant signal data for the strategy. crossoverSignals = signalTT(:,signalNames); % Start with our current weights. new_weights = current_weights; % Sell any existing long position where the signal has turned to 0. idx = crossoverSignals{end,:} == 0; new_weights(idx) = 0; % Find the new crossovers (signal changed from 0 to 1). idx = crossoverSignals{end,:} == 1 & crossoverSignals{end-1,:} == 0; % Bet sizing, split available capital across all remaining assets, and then % invest only in the new positive crossover assets. This leaves some % proportional amount of capital uninvested for future investments into the % zero-weight assets. availableCapital = 1 - sum(new_weights); uninvestedAssets = sum(new_weights == 0); new_weights(idx) = availableCapital / uninvestedAssets; end
function new_weights = rsiRebalanceFcn(current_weights, pricesTT, signalTT) % Buy and sell on 1 and -1 rebalance function. signalNameEnding = '_RSI'; % Build cell array of signal names that correspond to the crossover signals. symbols = pricesTT.Properties.VariableNames; signalNames = cellfun(@(s) sprintf('%s%s',s,signalNameEnding), symbols, 'UniformOutput', false); % Pull out the relevant signal data for the strategy. buySellSignals = signalTT(:,signalNames); % Start with the current weights. new_weights = current_weights; % Sell any existing long position where the signal has turned to -1. idx = buySellSignals{end,:} == -1; new_weights(idx) = 0; % Find the new buys (signal is 1 and weights are currently 0). idx = new_weights == 0 & buySellSignals{end,:} == 1; % Bet sizing, split available capital across all remaining assets, and then % invest only in the new positive crossover assets. This leaves some % proportional amount of capital uninvested for future investments into the % zero-weight assets. availableCapital = 1 - sum(new_weights); uninvestedAssets = sum(new_weights == 0); new_weights(idx) = availableCapital / uninvestedAssets; end
function new_weights = equalWeightFcn(current_weights,~) % Equal-weighted portfolio allocation. nAssets = numel(current_weights); new_weights = ones(1,nAssets); new_weights = new_weights / sum(new_weights); 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