exponenta event banner

Бэктест инвестиционных стратегий с торговыми сигналами

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

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

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

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

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

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

Каждая стратегия имеет расписание сигналов, которые предварительно вычисляются. Прежде чем выполнить обратный тест, необходимо объединить три отдельных расписания сигналов в одно совокупное расписание сигналов, чтобы использовать для обратного теста.

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-дневного скользящего среднего для каждого актива, и 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'));

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

Все стратегии оплачивают транзакционные издержки в 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)

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.

Заключение

Широкий рынок акций имел очень бычий 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

См. также

| | |

Связанные темы