В этом примере показано, как выполнить обратную проверку портфельных стратегий, которые включают инвестиционные сигналы в свою торговую стратегию. Термин «сигналы» включает любую информацию, которую автор стратегии должен принять в отношении торговых решений вне ценовой истории активов. Такая информация может включать технические показатели, результаты моделей машинного обучения, данные о настроениях, макроэкономические данные и т.д. В этом примере используются три простые инвестиционные стратегии, основанные на данных производного сигнала:
Кроссоверы скользящей средней
Скользящая средняя сходимость/дивергенция
Индекс относительной прочности
В этом примере можно выполнить обратный тест с использованием этих стратегий в течение одного года данных запаса. Затем выполняется анализ результатов для сравнения эффективности каждой стратегии.
Несмотря на то, что технические индикаторы обычно не используются в качестве самостоятельных торговых стратегий, в этом примере используются эти стратегии для демонстрации того, как строить инвестиционные стратегии на основе данных сигналов при использовании 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)
Каждая стратегия имеет расписание сигналов, которые предварительно вычисляются. Прежде чем выполнить обратный тест, необходимо объединить три отдельных расписания сигналов в одно совокупное расписание сигналов, чтобы использовать для обратного теста.
Индикатор 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-дневного скользящего среднего для каждого актива, и 0 в противном случае. Имена столбцов для каждого индикатора запаса - [символ запаса].SMA5over20. 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). Все конечные данные фиксируются в предварительно вычисленных торговых сигналах. Таким образом, фактические стратегии нуждаются только в двухдневном окне обратного обзора для принятия торговых решений, чтобы оценить, когда сигналы пересекают торговые пороги.
Все стратегии оплачивают транзакционные издержки в 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)
Широкий рынок акций имел очень бычий 6 месяцев во второй половине 2006 года, и все три эти стратегии не смогли полностью отразить этот рост, оставив слишком много капитала в наличных. Хотя ни одна из этих стратегий не работала хорошо самостоятельно, этот пример демонстрирует, как можно построить торговые стратегии на основе сигналов и провести их обратную проверку для оценки их эффективности.
Далее следует функция расчета начального веса, а также функции перебалансировки стратегии.
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