В этом примере показано, как выполнить backtesting стратегий портфеля, которые включают инвестиционные сигналы в их торговую стратегию. Термин сигналы включает любую информацию, которую автор стратегии должен сделать относительно торговых решений за пределами ценовой истории активов. Такая информация может включать технические индикаторы, выходные параметры моделей машинного обучения, данных о чувстве, макроэкономических данных, и так далее. Этот пример использует три простых инвестиционных стратегии на основе производных данных сигнала:
Перекрестные соединения скользящего среднего значения
Сходимость/расхождение скользящего среднего значения
Относительный индекс силы
В этом примере можно запустить backtest, использующий эти стратегии более чем один год данных о запасе. Вы затем анализируете результаты сравнить эффективность каждой стратегии.
Даже при том, что технические индикаторы обычно не используются в качестве автономных торговых стратегий, этот пример использует эти стратегии продемонстрировать, как создать инвестиционные стратегии на основе данных сигнала, когда вы используете backtestEngine
объект в MATLAB®.
Загрузите настроенные ценовые данные для 15 запасов в течение года 2006. Этот пример использует маленький набор подходящих для инвестирования активов для удобочитаемости.
Считайте таблицу ежедневных настроенных окончательных цен для 2006 запасов DJIA.
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
В дополнение к историческим настроенным ценам активов backtesting среда позволяет вам опционально задавать данные сигнала при выполнении backtest. Задайте данные сигнала похожим способом как цены при помощи 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-дневного скользящего среднего значения для каждого актива и 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 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 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
объект с помощью функций, определяемых восстановления равновесия в разделе Local Functions. Каждая стратегия использует функцию восстановления равновесия, чтобы принять торговые решения на основе соответствующих сигналов.
Сигналы требуют, чтобы достаточные запаздывающие данные вычислили торговые сигналы (например, вычисляя SMA20
в течение дня X требует цен с этих 20 дней до дня X). Все запаздывающие данные собраны в предварительно вычисленных торговых сигналах. Таким образом, для фактических стратегий нужно только 2-дневное lookback окно, чтобы принять торговые решения оценить когда крест сигналов торговые пороги.
Все стратегии платят операционные издержки на 25 пунктов на покупках, и продает.
Начальные веса вычисляются на основе значений сигналов после 20 торговых дней. backtest начинается после этого 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);
Агрегат каждое из отдельных расписаний сигнала в один backtest сигнализирует о расписании.
% Combine the three signal timetables.
signalTT = timetable;
signalTT = synchronize(signalTT, smaSignal);
signalTT = synchronize(signalTT, macdSignal);
signalTT = synchronize(signalTT, rsiSignal);
Используйте backtestEngine
создать backtesting механизм и затем использовать runBacktest
запускать backtest. Безрисковый уровень, заработанный на неинвестированных наличных деньгах, составляет пересчитанный на год 1% (который является 0.01/252 для ежедневных данных).
% 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 / 252)
bt = backtestEngine with properties: Strategies: [1x4 backtestStrategy] RiskFreeRate: 3.9683e-05 CashBorrowRate: 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);
Постройте кривые акции стратегии, чтобы визуализировать их эффективность по backtest.
% The plot equity curve helper function is implemented in the Local Functions section.
plotEquityCurve(bt);
Как упомянуто ранее, эти стратегии обычно не используются в качестве автономных торговых сигналов. На самом деле эти три стратегии выполняют хуже, чем простая стратегия сравнительного теста в течение 2 006 периодов времени. Можно визуализировать, как выделения стратегии изменяют в зависимости от времени использование диаграммы областей ежедневных положений актива. Для этого используйте assetAreaPlot
функция помощника, заданная в разделе Local Functions.
strategyName = 'Benchmark';
assetAreaPlot (купленный, strategyName)
Широкий фондовый рынок имел очень бычьи 6 месяцы во второй половине из 2 006 и всех трех из этих стратегий, отказавших, чтобы полностью получить тот рост путем отъезда слишком большого количества капитала наличными. В то время как ни одна из этих стратегий, выполняемых хорошо самостоятельно, этот пример, не демонстрирует, как можно создать основанные на сигнале торговые стратегии и backtest их, чтобы оценить их эффективность.
Начальная функция вычисления веса, а также стратегия, восстанавливающая равновесие функций, следует.
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 plotEquityCurve(bt) % Get total portfolio values using ret2tick. dailyReturns = bt.Returns; portfolioValues = ret2tick(dailyReturns,'StartPrice',bt.InitialPortfolioValue,'method','simple'); plot(portfolioValues.Time, portfolioValues.Variables); datetick('x','mm-yy','keeplimits','keepticks'); xlabel('Time'); ylabel('Portfolio Value ($)'); title('Equity Curves') % Remove the underscores in the strategy names for labeling. names = {bt.Strategies.Name}; nameLabels = cellfun(@(n) strrep(n,'_',' '),names,'UniformOutput',false); legend(nameLabels,'Location','best') grid on 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