MetaTrader 5 позволяет разрабатывать и тестировать роботов, торгующих одновременно на нескольких инструментах.
Встроенный в платформу тестер стратегий автоматически скачивает с торгового сервера брокера тиковую историю и учитывает спецификацию контрактов — разработчику ничего не нужно делать руками.
Это позволяет легко и максимально достоверно воспроизводить все условия торгового окружения — вплоть до миллисекундных интервалов между поступлениями тиков на разных символах.
Сейчас мы покажем, как провести разработку и тестирование спредовой стратегии на двух фьючерсах Московской биржи.
На Московской бирже торгуются фьючерсы вида Si-M.Y и RTS-M.Y, которые достаточно тесно между собой связаны. Здесь M.Y обозначают дату истечения контракта:
Si — это фьючерсный контракт на курс доллар США/российский рубль, RTS — фьючерсный контракт на Индекс РТС, выраженный в долларах США. Так как в Индекс РТС входят акции российских компаний, цены на которые выражены в рублях, то колебания курса USD/RUR отражаются также и на колебаниях индекса, выраженного в долларах США.
На графиках этих инструментов видно, что при росте одного актива второй, как правило, падает.
Для лучшего представления мы наложили на оба графика канал стандартного отклонения.
Мы можем выразить связь между двумя инструментами с помощью уравнения линейной регрессии вида Y(X)=A(X)+B. Для проверки напишем скрипт CalcShowRegression_script.mq5, который берет два массива цен закрытия, вычисляет коэффициенты и выводит диаграмму распределения с линией регрессии прямо на график.
Для вычисления коэффициентов регрессии используется функция из библиотеки ALGLIB, отрисовка делается графическими классами Стандартной библиотеки.
Мы получили коэффициенты линейной регрессии, и теперь можем строить синтетический график вида Y(RTS) = A*RTS+B. Разницу между оригинальным активом и синтетическим рядом назовем спредом. Эта разница будет меняться на каждом баре, от положительных до отрицательных значений.
Для визуального представления спреда напишем индикатор TwoSymbolsSpread_Ind.mql5, который выводит в виде гистограммы спред на данных за последние 500 баров.
Положительные значения спреда рисуются голубым цветом, отрицательные — желтым:
Индикатор обновляет и пишет в журнал Эксперты коэффициенты линейной регрессии при открытии каждого нового бара. При этом он дожидается того момента, когда новая свеча откроется на обоих инструментах — Si и RTS.
Таким образом индикатор позволяет контролировать правильность и точность расчетов.
Индикатор спреда показывает, что разница между фьючерсом Si и синтетическим инструментом периодически меняется. Чтобы оценить текущую тенденцию, напишем индикатор SpreadRegression_Ind.mq5 (спред и линейная регрессия на нём), который строит трендовую линию на графике спреда. Параметры линии вычисляем опять же методом линейной регрессии.
Наложим оба индикатора на график для отладки:
Красная линия тренда меняет свой наклон в зависимости от значения спреда на последних 100 барах. В принципе, теперь у нас есть минимум необходимых данных, чтобы попробовать построить торговую систему.
Значения спреда в индикаторе TwoSymbolsSpread_Ind.mql5 считаются как разность между Si и Y(RTS)=A*RTS + B.
Вы можете легко проверить это, запустив индикатор под отладкой (кнопкой F5):
Сделаем простого советника, который отслеживает изменение угла наклона линейной регрессии, наложенной на график спреда. Угол наклона линии — это не что иное, как коэффициент A в уравнении Y=A*X+B.
При положительном тренде на графике спреда A>0, при отрицательном A<0. Линейную регрессию мы считаем на последних 100 значениях графика спреда.
Вот часть кода из советника Strategy1_AngleChange_EA.mq5.
#include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| тип стратегии спреда | //+------------------------------------------------------------------+ enum SPREAD_STRATEGY { BUY_AND_SELL_ON_UP, // Buy 1-st, Sell 2-nd SELL_AND_BUY_ON_UP, // Sell 1-st, Buy 2-nd }; //--- input int LR_length=100; // кол-во баров для регрессии на спреде input int Spread_length=500; // кол-во баров для расчета спреда input ENUM_TIMEFRAMES period=PERIOD_M5; // таймфрейм input string symbol1="Si-12.16"; // первый символ пары input string symbol2="RTS-12.16"; // второй символ пары input double profit_percent=10; // сколько процентов прибыли фиксируем input SPREAD_STRATEGY strategy=SELL_AND_BUY_ON_UP; // тип спредовой стратегии //--- хендлы индикаторов int ind_spreadLR,ind,ind_2_symbols; //--- класс для торговых операций CTrade trade; //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- коэффициент A наклона линейной регрессии на графике спреда Y(X)=A*X+B static double Spread_A_prev=0; if(isNewBar()) PrintFormat("Новый бар %s открылся в %s",_Symbol,TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS)); //--- дождемся, когда данные индикатора обновятся, так как он работает на двух символах if(BarsCalculated(ind_spreadLR)==Bars(_Symbol,_Period)) { //--- получим значения линейной регрессии на графике спреда для баров с индексами 1 и 2 ("вчера" и "позавчера") double LRvalues[]; double Spread_A_curr; int copied=CopyBuffer(ind_spreadLR,1,1,2,LRvalues); if(copied!=-1) { //--- коэффициент линейной регрессии на последнем завершенном ("вчерашнем") баре Spread_A_curr=LRvalues[1]-LRvalues[0]; //--- если наклон регрессии сменился, то произведение текущего и предыдущего меньше нуля if(Spread_A_curr*Spread_A_prev<0) { PrintFormat("Линия LR сменила наклон, Spread_A_curr=%.2f, Spread_A_prev=%.2f: %s", Spread_A_curr,Spread_A_prev,TimeToString(TimeCurrent(),TIME_SECONDS)); //--- если открытых позиций нет, то войдем в рынок по двум символам if(PositionsTotal()==0) DoTrades(Spread_A_curr-Spread_A_prev>0,strategy,symbol1,1,symbol2,1); //--- есть открытые позиции, делаем переворот else ReverseTrades(symbol1,symbol2); } //--- наклон регрессии не изменился, проверим плавающую прибыль - может пора закрывать? else { double profit=AccountInfoDouble(ACCOUNT_PROFIT); double balance=AccountInfoDouble(ACCOUNT_BALANCE); if(profit/balance*100>=profit_percent) { //--- достигнут нужный уровень плавающей прибыли - фиксируем trade.PositionClose(symbol1); trade.PositionClose(symbol2); } } //--- запомним направление тренда для сравнения на открытии нового бара Spread_A_prev=Spread_A_curr; } } }Чтобы не делать предположений, что нужно покупать, а что продавать при смене тренда, мы просто добавили в советника внешний параметр, который позволяет переворачивать торговые правила:
input SPREAD_STRATEGY strategy=SELL_AND_BUY_ON_UP; // тип спредовой стратегииТеперь мы можем запустить тестирование и отладку советника.
Для отладки новой стратегии лучше всего использовать режим визуального тестирования. Через меню Сервис-Настройки-Отладка задайте все необходимые данные:
Для биржевых инструментов рекомендуем выбирать "Каждый тик на основе реальных тиков". В этом случае тестирование будет идти на записанных исторических данных, и вы получите результаты, максимально приближенные к реальным торговым условиям.
Торговый сервер MetaTrader 5 автоматически накапливает все пришедшие с биржи тики у себя и выдает в терминал всю тиковую историю по первому запросу:
В этом режиме отладки вы сможете визуально пройти через всё тестирование и проверить значения любых переменных в нужных местах кода с помощью точек прерывания.
Используемые в торговом роботе индикаторы автоматически будут загружены на графики, самому вручную ничего делать не придётся.
После того, как код советника отлажен, можем провести оптимизацию параметров.
В советнике Strategy1_AngleChange_EA.mq5 есть несколько внешних параметров, которые можно подбирать путем оптимизации (выделено желтым):
input int LR_length=100; // кол-во баров для регрессии на спреде input int Spread_length=500; // кол-во баров для расчета спреда input double profit_percent=10; // сколько процентов прибыли фиксируем input SPREAD_STRATEGY strategy=SELL_AND_BUY_ON_UP; // тип спредовой стратегииНо мы данном случае будем оптимизировать только параметр profit_percent для двух вариантов стратегии, чтобы понять, есть ли между ними разница.
Для правила BUY_AND_SELL_ON_UP (покупаем первый актив, продаем второй), когда наклон линии меняется с отрицательного на положительный, оптимизация дает не очень хорошие результаты.
В целом такой способ входа в рынок не выглядит привлекательным, мы чаще получаем убыток при тестировании на интервале в 2 месяца.
При использовании правила SELL_AND_BUY_ON_UP (продаем первый актив, покупаем второй) результаты оптимизации лучше — 5 из 15 проходов оптимизации показывают какую-то прибыль.
Оптимизация проводилась на истории с 1 августа по 30 сентября 2016 (интервал в два месяца). В целом, оба варианта торговли не выглядят многообещающими.
Возможно, проблема в том, что для входов мы использовали достаточно запаздывающий показатель — наклон трендовой линии за последние 100 баров. Попробуем разработать второй вариант стратегии.
Во второй стратегии мы будем следить за изменениями знака спреда. Смотреть мы будем значения только на сформировавшихся барах, то есть на открытии нового «сегодняшнего» бара. Если спред на «позавчерашнем» баре был отрицательный, а на «вчерашнем» стал положительным, то мы считаем, что спред развернулся вверх (UP).
Опять-таки в коде оставлена возможность торговать смену спреда в любую сторону — мы можем разворачивать направления входа с помощью параметра strategy.
Вот блок кода советника Strategy2_SpreadSignChange_EA.mq5:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- предыдущее значение спреда как разности Symbol1 и Y(Symbol2)=A*Symbol2+B static double Spread_prev=0; if(isNewBar()) PrintFormat("Новый бар %s открылся в %s",_Symbol,TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS)); //--- дождемся, когда данные индикатора обновятся, так как он работает на двух символах if(BarsCalculated(ind_spreadLR)==Bars(_Symbol,_Period)) { //--- получим значения спреда для баров с индексами 1 и 2 ("вчера" и "позавчера") double SpreadValues[]; int copied=CopyBuffer(ind_spreadLR,0,1,2,SpreadValues); double Spread_curr=SpreadValues[1]; if(copied!=-1) { //--- если знак спреда изменился, то произведение текущего и предыдущего меньше нуля if(Spread_curr*Spread_prev<0) { PrintFormat("Спред изменил знак, Spread_curr=%.2f, Spread_prev=%.2f: %s", Spread_curr,Spread_prev,TimeToString(TimeCurrent(),TIME_SECONDS)); //--- если открытых позиций нет, то войдем в рынок по двум символам if(PositionsTotal()==0) DoTrades(Spread_curr>0,strategy,symbol1,1,symbol2,1); //--- есть открытые позиции, делаем переворот else ReverseTrades(symbol1,symbol2); } //--- знак спреда не изменился, проверим плавающую прибыль - может, пора закрывать? else { double profit=AccountInfoDouble(ACCOUNT_PROFIT); double balance=AccountInfoDouble(ACCOUNT_BALANCE); if(profit/balance*100>=profit_percent) { //--- достигнут нужный уровень плавающей прибыли - фиксируем trade.PositionClose(symbol1); trade.PositionClose(symbol2); } } //--- запомним значение спрреда для сравнения на открытии нового бара Spread_prev=Spread_curr; } } }
Как видим, для второй стратегии правило «Продаем первый актив и покупаем второй» даёт такие же неутешительные результаты тестирования, как и для первой.
При этом правило «Покупаем первый актив и продаем второй» на всех проходах даёт больше убытков.
Попробуем сделать третий вариант стратегии.
Две предыдущие стратегии работали только на открытии бара, то есть анализировали изменения только на полностью завершенных барах. Попробуем теперь работать внутри текущего бара.
Проанализируем изменения спреда на каждом тике, и если знаки спреда на предыдущем завершенном и текущем развивающемся бара отличаются, то мы решаем, что направление спреда изменилось.
Потребуем также, чтобы изменение знака спреда было устойчивым на последних N тиках — таким образом мы пытаемся избавиться от ложных сигналов. Для этого добавим в советника внешний параметр ticks_for_trade=10.
Если на последних 10 тиках знак спреда отрицательный, а на предыдущем баре положительный, то мы входим в рынок.
Вот так выглядит функция OnTick() советника Strategy3_SpreadSignOnTick_EA.mq5:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { if(isNewBar()) PrintFormat("Новый бар %s открылся в %s",_Symbol,TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS)); //--- дождемся, когда данные индикатора обновятся, так как он работает на двух символах if(BarsCalculated(ind_spreadLR)==Bars(_Symbol,_Period)) { //--- получим значения спреда на текущем ("сегодня") и предыдущем ("вчера") барах double SpreadValues[]; int copied=CopyBuffer(ind_spreadLR,0,0,2,SpreadValues); double Spread_curr=SpreadValues[1]; // спред на текущем баре развивающемся который double Spread_prev=SpreadValues[0]; // спред на преыдущем завершенном баре if(copied!=-1) { //--- если изменение знака спреда сохраняется на последних ticks_for_trade тиках if(SpreadSignChanged(Spread_curr,Spread_prev,ticks_for_trade)) { PrintFormat("Спред изменил знак, Spread_curr=%.2f, Spread_prev=%.2f: %s", Spread_curr,Spread_prev,TimeToString(TimeCurrent(),TIME_SECONDS)); //--- покажем на графике значения последних ticks_for_trade тиков по обоим символам ShowLastTicksComment(ticks_for_trade); //--- если открытых позиций нет, то войдем в рынок по двум символам if(PositionsTotal()==0) DoTrades(Spread_curr>0,strategy,symbol1,1,symbol2,1); //--- есть открытые позиции, делаем переворот else ReverseTrades(Spread_curr>0,positionstype,symbol1,symbol2); } //--- знак спреда не изменился, проверим плавающую прибыль - может, пора закрывать? else { double profit=AccountInfoDouble(ACCOUNT_PROFIT); double balance=AccountInfoDouble(ACCOUNT_BALANCE); if(profit/balance*100>=profit_percent) { //--- достигнут нужный уровень плавающей прибыли - фиксируем trade.PositionClose(symbol1); trade.PositionClose(symbol2); positionstype=0; } } } } }
Запускаем такие же варианты оптимизаций, как и в предыдущих стратегиях, и получаем следующие результаты:
«Покупаем 1-ый актив и продаем 2-ой»
«Продаем 1-ый актив и покупаем 2-ой»
Результаты при такой простой оптимизации не сильно улучшились.
Создадим четвертую, последнюю, стратегию для торговли спредом. Она будет такой же простой как три предыдущие, а именно — торговый сигнал возникает в тот момент, когда спред составляет от цены первого актива заданное значение в процентах — spread_delta.
Обработчик поступающих тиков OnInit() изменился незначительно, вот его код из советника Strategy4_SpreadDeltaPercent_EA.mq5:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { if(isNewBar()) PrintFormat("Новый бар %s открылся в %s",_Symbol,TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS)); //--- дождемся, когда данные индикатора обновятся, так как он работает на двух символах if(BarsCalculated(ind_spreadLR)==Bars(_Symbol,_Period)) { //--- получим значение спреда на текущем ("сегодня")баре double SpreadValues[]; int copied=CopyBuffer(ind_spreadLR,0,0,1,SpreadValues); double Spread_curr=SpreadValues[0]; // спред на текущем развивающемся баре if(copied!=-1) { MqlTick tick; SymbolInfoTick(symbol1,tick); double last=tick.last; double spread_percent=Spread_curr/last*100; //--- если спред в % достиг заданной величины spread_delta if(MathAbs(spread_percent)>=spread_delta) { PrintFormat("Спред достиг %.1f%% (%G) %s", spread_percent,TimeToString(TimeCurrent(),TIME_SECONDS), Spread_curr); //--- если открытых позиций нет, то войдем в рынок по двум символам if(PositionsTotal()==0) DoTrades(Spread_curr,strategy,symbol1,1,symbol2,1); //--- есть открытые позиции, делаем переворот else ReverseTrades(Spread_curr,positionstype,symbol1,symbol2); } //--- спред в пределах допустимого, проверим плавающую прибыль - может, пора закрывать? else { double profit=AccountInfoDouble(ACCOUNT_PROFIT); double balance=AccountInfoDouble(ACCOUNT_BALANCE); if(profit/balance*100>=profit_percent) { //--- достигнут нужный уровень плавающей прибыли - фиксируем trade.PositionClose(symbol1); trade.PositionClose(symbol2); positionstype=0; } } } } }
«Покупаем 1-ый актив и продаем 2-ой»
«Продаем 1-ый актив и покупаем 2-ой»
На этот раз первое правило «Покупаем 1-ый актив и продаем 2-ой» выглядит значительно лучше, чем второе. Оптимизацию по остальным параметрам вы можете сделать сами.
В этой статье мы рассмотрели 4 простые стратегии для торговли на спреде. Показанные этими системами результаты тестирования и оптимизации нельзя принимать как руководство к действию, так как они были получены на ограниченном интервале и могут быть в какой-то степени случайными.
В этой статье мы показали, насколько удобно в MetaTrader 5 проверять и отлаживать торговые идеи.
Для разработчиков автоматических торговых систем тестер в MetaTrader 5 предлагает следующие удобные возможности:
Тестер торговых стратегий в данной статье использовался как инструмент исследования и поиска правильного направления — это делалось в виде оптимизации по одному параметру, чтобы быстро получать качественные выводы.
Вы можете добавлять новые правила, модифицировать существующие и подвергнуть полученные советники тотальной оптимизации.
Для ускорения массовых тяжелых вычислений используйте глобальную вычислительную сеть MQL5 Cloud Network, которая создана специально для платформы MetaTrader 5
Обычно при поиске символов для составления спреда используют не абсолютные значения цены, а приращения. То есть вместо ряда Close[i] вычисляют Delta[i]=Close[i]-Close[i-1].
Для сбалансированной торговли необходимо подбирать объем для каждого символа спреда. В статье же при тестировании использовались только объемы в 1 лот на каждом символе.
При тестировании используются текущие настройки в спецификации контрактов Si и RTS. Необходимо отметить, что:
Методику расчета вы можете найти на сайте MOEX — http://fs.moex.com/files/3244. Поэтому нужно иметь в виду, что результаты оптимизации в тестере стратегий будут зависеть от курса доллара на момент проведения тестирования.
В статье использованы скриншоты с результатами оптимизации, проведенными 25 октября 2016 года.
Код написан для работы в условиях идеального исполнения — нет обработки результатов отправки торговых приказов, не обрабатываются ошибки, связанные с потерей связи, не учтены комиссии, нет проскальзываний.
Для фьючерсов ликвидность и заполненность графиков улучшаются ближе к моменту истечения контракта. В коде нет явной обработки ситуации, когда на одном символе котировки идут, а на второй пропущены целые бары (торговля в это время на бирже не велась по какой-либо причине).
Хотя сами индикаторы, использованные в советнике, гарантированно ждут синхронизации баров по обоим символам, чтобы рассчитать значение спреда, и пишут эти события в журнал.
Не затронут вопрос анализа статистики отклонений спреда от средних значений для создания более надежных торговых правил.
Не используется анализ стакана заявок, так как поток заявок в тестере MetaTrader 5 не эмулируется.
Индикаторы, используемые в данной статье, динамически пересчитывают коэффициенты линейной регрессии для создания графиков спреда и линии тренда. Поэтому по окончании тестирования вид графиков и значения индикаторов на текущий момент будут отличаться от тех, что были во время тестирования.
Запустите приложенные индикаторы или советники в режиме визуального тестирования, чтобы увидеть, как это происходит в режиме реального времени.
Статьи по теме:
Программы, используемые в статье:
Вы всегда в любой момент можете получить все тики, включая информационные bid/ask и торговые last на любую глубину.
Тестер использует реальные тики, в том числе в мультисимвольном режиме, моделируя развитие рынка с точностью до 1 мс.
Кроме того, вы можете прямо в тестере включить режим визуализации и глазами наблюдать, как развивается рынок и как идет торговля.
Универсальный индикатор на любой ТФ никак не сделать?
Full Order Log и очередь заявок в тестере/демо-режиме когда прикрутите? ;)
С ее помощью очень удобно моделировать проскальзывание.
Даже выставленная задержка в виде пинга до сервера(last ping to your server) позволит остудить некоторые пипсовочные стратегии.
У нас в тестере задаваемая задержка как раз позволяет прокручивать/моделировать весь мир во время этой задержки со всеми тиками и уже приходить к исполнению по конкретной рыночной ситуации в конкретную миллисекунду.
Это не тупая задержка, а очень умная и точная. Задав задержку в виде своего пинга, можно примлемо по рыночным условиям подходить к столу раздачи.
Задав задержку больше пинга, можно более пессимистично оценивать качество отработки своих стратегий.
Теперь и другие узнали, что у вас есть свой тестер. Только его не увидят, в отличие от нашего :)