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

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

  • Кроссоверы скользящего среднего

  • Сходимость/расхождение скользящего среднего

  • Относительный индекс прочности

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

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

Figure contains an object of type heatmap.

% 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('$')

Figure contains an axes. The axes with title Growth of $1 for Each Stock contains 15 objects of type line. These objects represent AA, CAT, DIS, GM, HPQ, JNJ, MCD, MMM, MO, MRK, MSFT, PFE, PG, T, XOM.

% 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: Простой скользящий средний кроссовер

Индикатор 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'));

Figure contains an axes. The axes with title SMA 5 over 20 for CAT contains an object of type line.

MACD: сходимость скользящего среднего/расхождение

Метрику 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'));

Figure contains an axes. The axes with title MACD > 0 for CAT contains an object of type line.

RSI: Относительный индекс прочности

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'));

Figure contains an axes. The axes with title RSI Buy/Sell Signal for CAT contains an object of type line.

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

Создайте стратегии для 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: []

Стратегии Backtest

% 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

Использование equityCurve для построения кривых справедливости стратегии, чтобы визуализировать их эффективность на заднем тесте.

equityCurve(bt)

Figure contains an axes. The axes with title Equity Curve contains 4 objects of type line. These objects represent Benchmark, SMA, MACD, RSI.

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

strategyName = 'Benchmark';
assetAreaPlot (bt, strategyName)

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

Заключение

Во второй половине 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

См. также

| | |

Похожие темы