Использование индикаторов для RealTime оптимизации советников

Dmitriy Gizlyk | 4 сентября, 2018

Содержание

Введение

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

И тогда требуется повторная оптимизация советника. Этот цикл постоянен. Момент повторной оптимизации каждый пользователь выбирает сам. Но давайте подумаем, можно ли этот процесс автоматизировать? Какие существуют методы решения данного вопроса? Возможно, вы уже смотрели в сторону программного управления стандартным тестером стратегий через запуск терминала с собственным конфигурационным файлом. Я же хочу предложить несколько иной, нестандартный подход, и возложить роль тестера на индикатор.

1. Идея

Конечно, индикатор это совсем не тестер стратегий. И как же он может нам помочь в оптимизации советника? Моя идея заключается в том, чтобы прописать в индикаторе логику работы советника и отслеживать доходность совершения виртуальных сделок в реальном времени. При проведении оптимизации в тестере стратегий проводится серия тестов с перебором заданных параметров. Мы поступим также, запустим одновременно несколько экземпляров одного индикатора с разными параметрами, по аналогии с проходами тестера стратегий. В момент принятия решения, советник опросит запущенные индикаторы и выберет для исполнения лучшие показатели.

Вполне вероятен вопрос: зачем изобретать велосипед? Давайте рассмотрим положительные и отрицательные стороны такого решения. Несомненно, главным преимуществом такого подхода является оптимизация советника практически в условиях реального времени. Вторым преимуществом можно назвать проведение тестирования на реальных тиках именно Вашего брокера, но с другой стороны, тестирование в режиме реального времени является большим минусом, т.к. необходимо выждать время для сбора статистических данных. Положительным является и тот момент, что при движении во времени тестер-индикатор не будет пересчитывать всю историю, а только текущий тик, в то время, как тестер стратегий будет проводить прогон истории с самого начала. Такой подход дает более быструю оптимизацию в требуемый момент. Следовательно, мы можем проводить оптимизацию практически на каждом баре.

К минусам использования такого подхода можно отнести отсутствие тиковой истории для тестирования на истории. Конечно, можно воспользоваться CopyTicks или CopyTicksRange. Но загрузка тиковой истории потребует время, а затем и пересчет большого объема информации также потребует вычислительных мощностей компьютера и время. Не будем забывать, что мы используем индикаторы. А все индикаторы по одному инструменту в MetaTrader 5 работают в одном потоке. В этом кроется и еще одно ограничение — слишком большое количество индикаторов может привести к замедлению работы терминала.

Для минимизации рисков от описанных выше минусов примем следующие допущения:

  1. При инициализации тестер-индикатора история просчитывается по ценам М1 OHLC. В расчете прибыли/убытков по ордерам сначала проверяется стоп-лосс, а затем тейк-профит по High/Low (в зависимости от типа ордера).
  2. В связи с пунктом 1 ордера открываются только на открытии свечи.
  3. Для снижения общего количества запущенных тест-индикаторов применяем осмысленный подход к выбору применяемых в них параметров. Здесь можно добавить минимальный шаг, фильтрация параметров в соответствии с логикой индикатора. К примеру, при использовании MACD если диапазон параметров быстрой и медленной скользящей средней перекрываются, то для набора параметров где период медленной скользящей меньше или равен периоду быстрой скользящей средней тестер-индикатор не запускается, как противоречащей самой логике работы индикатора. Можно также добавить минимальное расхождение между периодами, изначально отбросив варианты с большим количеством ложных сигналов.

2. Стратегия торговли

Для тестирования метода воспользуемся простой стратегией основанной на 3-х классических индикаторах WPR, RSI и ADX. Сигналом на покупку будет пересечение WPR уровня перепроданости (уровня -80) снизу вверх. При этом контролируется, чтобы RSI не был в зоне перекупленности (выше уровня 70). Так как оба эти индикатора являются осцилляторами, то их применение оправданно во флетовых движениях. Наличие флета проверяется индикатором ADX, значение которого не должно превышать уровень 40.

Точка входа на покупку

Для продажи сигнал будет зеркальный. Индикатор WPR пересекает уровень перекупленности -20 сверху вниз, значение RSI должно быть выше зоны перепроданности 30. ADX контролирует наличие флета, как и при покупке.

Точка входа на продажу

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

С целью контроля убытков в рынке всегда будет не более одной позиции.

3. Подготовка тестер-индикатора

3.1. Класс виртуальных сделок

Определив стратегию торговли, пора приступить к написанию тестового индикатора. И первое, что мы должны сделать, это подготовить виртуальные ордера, исполнение которых мы будем отслеживать в индикаторе. Ранее в статье [1] уже был описан класс виртуального ордера. В нашей работе мы вполне можем воспользоваться этой разработкой с небольшим дополнением. Дело в том, что описанный ранее класс имеет метод Tick, который проверяет момент закрытия ордера по текущим ценам Ask и Bid. Такой подход вполне применим при работе только в реальном времени, и совсем не применим для проверки на исторических данных. Немного переделаем указанную функцию, добавив в ее параметры цену и спред. После выполнения операций метод вернет состояние ордера. В результате такого дополнения метод примет нижеследующий вид.

bool CDeal::Tick(double price, int spread)
  {
   if(d_ClosePrice>0)
      return true;
//---
   switch(e_Direct)
     {
      case POSITION_TYPE_BUY:
        if(d_SL_Price>0 && d_SL_Price>=price)
          {
           d_ClosePrice=price;
           i_Profit=(int)((d_ClosePrice-d_OpenPrice)/d_Point);
          }
        else
          {
           if(d_TP_Price>0 && d_TP_Price<=price)
             {
              d_ClosePrice=price;
              i_Profit=(int)((d_ClosePrice-d_OpenPrice)/d_Point);
             }
          }
        break;
      case POSITION_TYPE_SELL:
        price+=spread*d_Point;
        if(d_SL_Price>0 && d_SL_Price<=price)
          {
           d_ClosePrice=price;
           i_Profit=(int)((d_OpenPrice-d_ClosePrice)/d_Point);
          }
        else
          {
           if(d_TP_Price>0 && d_TP_Price>=price)
             {
              d_ClosePrice=price;
              i_Profit=(int)((d_OpenPrice-d_ClosePrice)/d_Point);
             }
          }
        break;
     }
   return IsClosed();
  }

С полным кодом класса можно ознакомиться во вложении.

3.2. Программируем индикатор

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

input int                  HistoryDepth      =  500;           //Depth of history(bars)
input int                  StopLoss          =  200;           //Stop Loss(points)
input int                  TakeProfit        =  600;           //Take Profit(points)
//--- Параметры индикатора RSI
input int                  RSIPeriod         =  28;            //RSI Period
input double               RSITradeZone      =  30;            //Overbaying/Overselling zone size
//--- Параметры индикатора WPR
input int                  WPRPeriod         =  7;             //Period WPR
input double               WPRTradeZone      =  30;            //Overbaying/Overselling zone size
//--- Параметры индикатора ADX
input int                  ADXPeriod         =  11;            //ADX Period
input int                  ADXLevel          =  40;            //Flat Level ADX
//---
input int                  Direction         =  -1;            //Trade direction "-1"-All, "0"-Buy, "1"-Sell
//---
input int                  AveragePeriod     =  10;            //Averaging Period

Для вычислений и обмена данными с советником создадим в нашем индикаторе 9 индикаторных буферов, содержащих следующую информацию:

1. Вероятность прибыльной сделки.

double      Buffer_Probability[];

2. Профит-фактор за протестированный период.

double      Buffer_ProfitFactor[];

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

double      Buffer_TakeProfit[];
double      Buffer_StopLoss[];

4. Буферы для подсчета за тестируемый период общего количества совершенных сделок и их прибыльного числа.

double      Buffer_ProfitCount[];
double      Buffer_DealsCount[];

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

double      Buffer_ProfitCountCurrent[];
double      Buffer_DealsCountCurrent[];

6. И последний по списку, но не по значению буфер, передающий в советник сигнал на совершение сделки.

double      Buffer_TradeSignal[];

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

CArrayObj   Deals;
datetime    last_deal;
int         wpr_handle,rsi_handle,adx_handle;
double      rsi[],adx[],wpr[];

В начале функции OnInit проведем инициализацию индикаторов.

int OnInit()
  {
//--- Получение хэндла индикатора RSI
   rsi_handle=iRSI(Symbol(),PERIOD_CURRENT,RSIPeriod,PRICE_CLOSE);
   if(rsi_handle==INVALID_HANDLE)
     {
      Print("Test Indicator",": Failed to get RSI handle");
      Print("Handle = ",rsi_handle,"  error = ",GetLastError());
      return(INIT_FAILED);
     }
//--- Получение хэндла индикатора WPR
   wpr_handle=iWPR(Symbol(),PERIOD_CURRENT,WPRPeriod);

   if(wpr_handle==INVALID_HANDLE)
     {
      Print("Test Indicator",": Failed to get WPR handle");
      Print("Handle = ",wpr_handle,"  error = ",GetLastError());
      return(INIT_FAILED);
     }
//--- Получение хэндла индикатора ADX
   adx_handle=iADX(Symbol(),PERIOD_CURRENT,ADXPeriod);
   if(adx_handle==INVALID_HANDLE)
     {
      Print("Test Indicator",": Failed to get ADX handle");
      Print("Handle = ",adx_handle,"  error = ",GetLastError());
      return(INIT_FAILED);
     }

Затем свяжем индикаторные буферы с динамическими массивами.

//--- indicator buffers mapping
   SetIndexBuffer(0,Buffer_Probability,INDICATOR_CALCULATIONS);
   SetIndexBuffer(1,Buffer_DealsCount,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,Buffer_TradeSignal,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,Buffer_ProfitFactor,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,Buffer_ProfitCount,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,Buffer_TakeProfit,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,Buffer_StopLoss,INDICATOR_CALCULATIONS);
   SetIndexBuffer(7,Buffer_DealsCountCurrent,INDICATOR_CALCULATIONS);
   SetIndexBuffer(8,Buffer_ProfitCountCurrent,INDICATOR_CALCULATIONS);

И присвоим всем массивам свойства таймсерий.

   ArraySetAsSeries(Buffer_Probability,true);
   ArraySetAsSeries(Buffer_ProfitFactor,true);
   ArraySetAsSeries(Buffer_TradeSignal,true);
   ArraySetAsSeries(Buffer_DealsCount,true);
   ArraySetAsSeries(Buffer_ProfitCount,true);
   ArraySetAsSeries(Buffer_TakeProfit,true);
   ArraySetAsSeries(Buffer_StopLoss,true);
   ArraySetAsSeries(Buffer_DealsCountCurrent,true);
   ArraySetAsSeries(Buffer_ProfitCountCurrent,true);
//--- 
   ArraySetAsSeries(rsi,true);
   ArraySetAsSeries(wpr,true);
   ArraySetAsSeries(adx,true);

В заключении функции обнулим массив сделок и дату последней сделки, а также присвоим имя нашему индикатору.

   Deals.Clear();
   last_deal=0;
//---
   IndicatorSetString(INDICATOR_SHORTNAME,"Test Indicator");
//---
   return(INIT_SUCCEEDED);
  }

Загрузку текущих данных индикаторов будем осуществлять в функции GetIndValue. На входе указанная функция будет получать требуемую глубину истории загружаемых данных, а на выходе функция вернет количество загруженных элементов. Сами данные индикаторов сохранятся в глобально объявленных массивах.

int GetIndValue(int depth)
  {
   if(CopyBuffer(wpr_handle,MAIN_LINE,0,depth,wpr)<=0 || CopyBuffer(adx_handle,MAIN_LINE,0,depth,adx)<=0 || CopyBuffer(rsi_handle,MAIN_LINE,0,depth,rsi)<=0)
      return -1;
   depth=MathMin(ArraySize(rsi),MathMin(ArraySize(wpr),ArraySize(adx)));
//---
   return depth;
  }

Для проверки сигналов на открытие сделки создадим зеркальные функции BuySignal и SellSignal. Детально с кодом функций можно ознакомиться во вложении.

Основной функционал, как и в любом индикаторе, будет сосредоточен в функции OnCalculate. Операции этой функции можно логически разделить на 2 потока:

  1. При пересчете более одного бара (первый запуск после инициализации или открытие нового бара). В этом потоке мы будем проверять сигналы на открытие сделок на каждом не пересчитанном баре и отработку стоп-приказов открытых сделок по историческим данным таймфрейма М1.
  2. На каждом новом тике, когда новый бар еще не сформирован, проверяем только срабатывание стоп-приказов открытых позиций.

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

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   int total=rates_total-prev_calculated;
   if(prev_calculated<=0)
     {
      total=fmin(total,HistoryDepth);
//---
      ArrayInitialize(Buffer_Probability,0);
      ArrayInitialize(Buffer_ProfitFactor,0);
      ArrayInitialize(Buffer_TradeSignal,0);
      ArrayInitialize(Buffer_DealsCount,0);
      ArrayInitialize(Buffer_ProfitCount,0);
      ArrayInitialize(Buffer_TakeProfit,TakeProfit*_Point);
      ArrayInitialize(Buffer_StopLoss,StopLoss*_Point);
      ArrayInitialize(Buffer_DealsCountCurrent,0);
      ArrayInitialize(Buffer_ProfitCountCurrent,0);
     }

Далее идут операции первого логического потока, когда есть открытие новой свечи. Сначала загрузим текущие данные используемых индикаторов. При ошибке загрузки данных выходим из функции до следующего тика, в ожидании пересчета индикаторов.

   if(total>0)
     {
      total=MathMin(GetIndValue(total+2),rates_total);
      if(total<=0)
         return prev_calculated;

Затем присвоим признак таймсерии входящим ценовым массивам.

      if(!ArraySetAsSeries(open,true) || !ArraySetAsSeries(high,true) || !ArraySetAsSeries(low,true) || !ArraySetAsSeries(close,true)
         || !ArraySetAsSeries(time,true) || !ArraySetAsSeries(spread,true))
         return prev_calculated;

Далее следует основной цикл пересчета каждого бара. В начале цикла, инициализируем индикаторные буферы для пересчитываемого бара.

      for(int i=total-3;i>=0;i--)
        {
         Buffer_TakeProfit[i]=TakeProfit*_Point;
         Buffer_StopLoss[i]=StopLoss*_Point;
         Buffer_DealsCount[i]=Buffer_DealsCountCurrent[i]=0;
         Buffer_ProfitCount[i]=Buffer_ProfitCountCurrent[i]=0;

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

         if(last_deal<time[i])
           {
            if(BuySignal(i))
              {
               double open_price=open[i]+spread[i]*_Point;
               double sl=open_price-StopLoss*_Point;
               double tp=open_price+TakeProfit*_Point;
               CDeal *temp=new CDeal(_Symbol,rates_total-i,POSITION_TYPE_BUY,time[i],open_price,sl,tp);
               if(temp!=NULL)
                  Deals.Add(temp);
               Buffer_TradeSignal[i]=1;
              }
            else /*BuySignal*/
            if(SellSignal(i))
              {
               double open_price=open[i];
               double sl=open_price+StopLoss*_Point;
               double tp=open_price-TakeProfit*_Point;
               CDeal *temp=new CDeal(_Symbol,rates_total-i,POSITION_TYPE_SELL,time[i],open_price,sl,tp);
               if(temp!=NULL)
                  Deals.Add(temp);
               Buffer_TradeSignal[i]=-1;
              }
            else /*SellSignal*/
               Buffer_TradeSignal[i]=0;
           }

Далее проводится работа с открытыми позициями. И первое, что мы делаем — проверяем текущий таймфрейм. Если индикатор работает на таймфрейме М1, то проверку срабатывания стоп-приказов открытых позиций будем осуществлять по данным теймсерий, полученных в параметрах функции OnCalculate. В противном случае нам придется подгрузить данные минутного таймфрейма.

         if(Deals.Total()>0)
           {
            if(PeriodSeconds()!=60)
              {
               MqlRates rates[];
               int rat=CopyRates(_Symbol,PERIOD_M1,time[i],(i>0 ? time[i-1] : TimeCurrent()),rates);

После загрузки котировок организовываем цикл по проверке отработки стоп-приказов открытых сделок на каждом минутном баре. Сумму закрытых и прибыльных сделок суммируем в соответствующие индикаторные буферы для пересчитываемого бара. Непосредственно обработка массива сделок осуществляется в функции CheckDeals, в параметрах которой передаем данные проверяемой минутной свечи. Алгоритм работы функции будет прокомментирован ниже.

               int closed=0, profit=0;
               for(int r=0;(r<rat && Deals.Total()>0);r++)
                 {
                  CheckDeals(rates[r].open,rates[r].high,rates[r].low,rates[r].close,rates[r].spread,rates[r].time,closed,profit);
                  if(closed>0)
                    {
                     Buffer_DealsCountCurrent[i]+=closed;
                     Buffer_ProfitCountCurrent[i]+=profit;
                    }
                 }

Далее следуют аналогичные альтернативные блоки, которые проверяют сделки по данным текущего таймфрейма в случаях ошибки загрузки минутных котировок или работы индикатора на таймфрейме М1.

               if(rat<0)
                 {
                  CheckDeals(open[i],high[i],low[i],close[i],spread[i],time[i],closed,profit);
                  Buffer_DealsCountCurrent[i]+=closed;
                  Buffer_ProfitCountCurrent[i]+=profit;
                 }
              }
            else /* PeriodSeconds()!=60 */
              {
               int closed=0, profit=0;
               CheckDeals(open[i],high[i],low[i],close[i],spread[i],time[i],closed,profit);
               Buffer_DealsCountCurrent[i]+=closed;
               Buffer_ProfitCountCurrent[i]+=profit;
              }
           } /* Deals.Total()>0 */

В заключение проделанной работы посчитаем статистику работы нашей стратегии. Посчитаем количество открытых за тестируемый период сделок и сколько из них закрылось с прибылью.

         Buffer_DealsCount[i+1]=NormalizeDouble(Buffer_DealsCount[i+2]+Buffer_DealsCountCurrent[i+1]-((i+HistoryDepth+1)<rates_total ? Buffer_DealsCountCurrent[i+HistoryDepth+1] : 0),0);
         Buffer_ProfitCount[i+1]=NormalizeDouble(Buffer_ProfitCount[i+2]+Buffer_ProfitCountCurrent[i+1]-((i+HistoryDepth+1)<rates_total ? Buffer_ProfitCountCurrent[i+HistoryDepth+1] : 0),0);
         Buffer_DealsCount[i]=NormalizeDouble(Buffer_DealsCount[i+1]+Buffer_DealsCountCurrent[i]-((i+HistoryDepth)<rates_total ? Buffer_DealsCountCurrent[i+HistoryDepth] : 0),0);
         Buffer_ProfitCount[i]=NormalizeDouble(Buffer_ProfitCount[i+1]+Buffer_ProfitCountCurrent[i]-((i+HistoryDepth)<rates_total ? Buffer_ProfitCountCurrent[i+HistoryDepth] : 0),0);

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

         if(Buffer_DealsCount[i]>0)
           {
            double pr=2.0/(AveragePeriod-1.0);
            Buffer_Probability[i]=((i+1)<rates_total && Buffer_Probability[i+1]>0 && Buffer_DealsCount[i+1]>=AveragePeriod ? Buffer_ProfitCount[i]/Buffer_DealsCount[i]*100*pr+Buffer_Probability[i+1]*(1-pr) : Buffer_ProfitCount[i]/Buffer_DealsCount[i]*100);
            if(Buffer_DealsCount[i]>Buffer_ProfitCount[i])
              {
               double temp=(Buffer_ProfitCount[i]*TakeProfit)/(StopLoss*(Buffer_DealsCount[i]-Buffer_ProfitCount[i]));
               Buffer_ProfitFactor[i]=((i+1)<rates_total && Buffer_ProfitFactor[i+1]>0 ? temp*pr+Buffer_ProfitFactor[i+1]*(1-pr) : temp);
              }
            else
               Buffer_ProfitFactor[i]=TakeProfit*Buffer_ProfitCount[i];
           }
        }
     }

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

Ранее я уже писал, что проверка срабатывания стоп-приказов действующих сделок осуществляется в функции CheckDeals. Давайте рассмотрим алгоритм ее работы. В параметрах функция получает котировки анализируемого бара и две ссылки на переменные для возврата количества закрытых и прибыльных сделок.

В начале функции обнуляем возвращаемые переменные и объявляем результирующую логическую переменную.

bool CheckDeals(double open,double high,double low,double close,int spread,datetime time,int &closed, int &profit)
  {
   closed=0;
   profit=0;
   bool result=true;

Далее в функции организовывается цикл по перебору всех сделок в массиве. В цикле поочередно из массива получаются указатели на объекты сделок. В случае ошибочного указателя на объект удаляем данную сделку из массива и переходим к следующей сделке. При наличии ошибки выполнения операций присваиваем результирующей переменной значение false.

   for(int i=0;i<Deals.Total();i++)
     {
      CDeal *deal=Deals.At(i);
      if(CheckPointer(deal)==POINTER_INVALID)
        {
         if(Deals.Delete(i))
            i--;
         else
            result=false;
         continue;
        }

Следующим шагом проверяем, была ли открыта сделка на момент открытия свечи. Если нет, то переходим к следующей сделке.

      if(deal.GetTime()>time)
         continue;

И наконец, проверяем срабатывание стоп-приказов сделки поочередно для цен открытия, максимальной, минимальной и закрытия вызовом метода Tick проверяемой сделки для каждой цены. Алгоритм работы данного метода описан выше в начале текущего раздела. При этом следует учесть, что для сделок на покупку и продажу последовательность проверки отличается. Сначала проверяется срабатывание стоп-лосса, а затем тейк-профита. Такой подход отчасти может занизить результативность торговли, но позволит снизить убытки в будущей торговле. При срабатывание какого-либо из стоп-приказов увеличивается количество закрытых сделок, а в случае получения прибыли увеличиваем и количество прибыльных сделок. После закрытия сделка удаляется из массива для предотвращения повторного пересчета.

      if(deal.Tick(open,spread))
        {
         closed++;
         if(deal.GetProfit()>0)
            profit++;
         if(Deals.Delete(i))
            i--;
         if(CheckPointer(deal)!=POINTER_INVALID)
            delete deal;
         continue;
        }
      switch(deal.Type())
        {
         case POSITION_TYPE_BUY:
            if(deal.Tick(low,spread))
              {
               closed++;
               if(deal.GetProfit()>0)
                  profit++;
               if(Deals.Delete(i))
                  i--;
               if(CheckPointer(deal)!=POINTER_INVALID)
                  delete deal;
               continue;
              }
            if(deal.Tick(high,spread))
              {
               closed++;
               if(deal.GetProfit()>0)
                  profit++;
               if(Deals.Delete(i))
                  i--;
               if(CheckPointer(deal)!=POINTER_INVALID)
                  delete deal;
               continue;
              }
           break;
         case POSITION_TYPE_SELL:
            if(deal.Tick(high,spread))
              {
               closed++;
               if(deal.GetProfit()>0)
                  profit++;
               if(Deals.Delete(i))
                  i--;
               if(CheckPointer(deal)!=POINTER_INVALID)
                  delete deal;
               continue;
              }
            if(deal.Tick(low,spread))
              {
               closed++;
               if(deal.GetProfit()>0)
                  profit++;
               if(Deals.Delete(i))
                  i--;
               if(CheckPointer(deal)!=POINTER_INVALID)
                  delete deal;
               continue;
              }
           break;
        }
     }
//---
   return result;
  }

С полным кодом индикатора и всех его функций можно ознакомиться во вложении.

4. Создаем советник

После создания тестер-индикатора пришло время приступить непосредственно к созданию нашего эксперта. В параметрах нашего советника мы укажем ряд статических переменных (общих для всех проходов) и, по аналогии с тестером стратегий, укажем начальные, конечные значения изменяемых параметров и величину шага изменения значений. Также в параметрах советника мы укажем критерии для отбора сигналов на вход в рынок — это минимальная вероятность получения прибыли и минимальный профит-фактор за тестируемый период. Кроме того, для соблюдения объективности полученных статистических данных укажем минимально-необходимое количество сделок за тестируемый период.

input double               Lot                     =  0.01;
input int                  HistoryDepth            =  500;           //Depth of history(bars)
//--- Параметры индикатора RSI
input int                  RSIPeriod_Start         =  5;             //RSI Period
input int                  RSIPeriod_Stop          =  30;            //RSI Period
input int                  RSIPeriod_Step          =  5;             //RSI Period
//---
input double               RSITradeZone_Start      =  30;            //Overbaying/Overselling zone size Start
input double               RSITradeZone_Stop       =  30;            //Overbaying/Overselling zone size Stop
input double               RSITradeZone_Step       =  5;             //Overbaying/Overselling zone size Step
//--- Параметры индикатора WPR
input int                  WPRPeriod_Start         =  5;             //Period WPR Start
input int                  WPRPeriod_Stop          =  30;            //Period WPR Stop
input int                  WPRPeriod_Step          =  5;             //Period WPR Step
//---
input double               WPRTradeZone_Start      =  20;            //Overbaying/Overselling zone size Start
input double               WPRTradeZone_Stop       =  20;            //Overbaying/Overselling zone size Stop
input double               WPRTradeZone_Step       =  5;             //Overbaying/Overselling zone size Step
//--- Параметры индикатора ADX
input int                  ADXPeriod_Start         =  5;             //ADX Period Start
input int                  ADXPeriod_Stop          =  30;            //ADX Period Stop
input int                  ADXPeriod_Step          =  5;             //ADX Period Step
//---
input int                  ADXTradeZone_Start      =  40;            //Flat Level ADX Start
input int                  ADXTradeZone_Stop       =  40;            //Flat Level ADX Stop
input int                  ADXTradeZone_Step       =  10;            //Flat Level ADX Step
//--- Deals Settings
input int                  TakeProfit_Start        =  600;           //TakeProfit Start
input int                  TakeProfit_Stop         =  600;           //TakeProfit Stop
input int                  TakeProfit_Step         =  100;           //TakeProfit Step
//---
input int                  StopLoss_Start          =  200;           //StopLoss Start
input int                  StopLoss_Stop           =  200;           //StopLoss Stop
input int                  StopLoss_Step           =  100;           //StopLoss Step
//---
input double               MinProbability          =  60.0;          //Minimal Probability
input double               MinProfitFactor         =  1.6;           //Minimal Profitfactor
input int                  MinOrders               =  10;            //Minimal number of deals in history

В глобальных переменных объявим массив для хранения хэндлов тестер-индикаторов и экземпляр класса торговых операций.

CArrayInt   ar_Handles;
CTrade      Trade;

В функции OnInit нашего эксперта организуем ряд вложенных циклов для перебора всех вариантов тестируемых параметров и добавим еще отдельное тестирование сделок на покупку и продажу. Такой подход позволит учесть влияние глобальных трендов, не отслеживаемых тестируемой стратегией. Внутри циклов будем инициализировать тестер-индикаторы. При ошибке загрузке индикатора выходим из функции с результатом INIT_FAILED. В случае успешной загрузки индикатора добавляем его хэндл в массив.

int OnInit()
  {
//---
   for(int rsi=RSIPeriod_Start;rsi<=RSIPeriod_Stop;rsi+=RSIPeriod_Step)
      for(double rsi_tz=RSITradeZone_Start;rsi_tz<=RSITradeZone_Stop;rsi_tz+=RSITradeZone_Step)
         for(int wpr=WPRPeriod_Start;wpr<=WPRPeriod_Stop;wpr+=WPRPeriod_Step)
            for(double wpr_tz=WPRTradeZone_Start;wpr_tz<=WPRTradeZone_Stop;wpr_tz+=WPRTradeZone_Step)
               for(int adx=ADXPeriod_Start;adx<=ADXPeriod_Stop;adx+=ADXPeriod_Step)
                  for(double adx_tz=ADXTradeZone_Start;adx_tz<=ADXTradeZone_Stop;adx_tz+=ADXTradeZone_Step)
                     for(int tp=TakeProfit_Start;tp<=TakeProfit_Stop;tp+=TakeProfit_Step)
                        for(int sl=StopLoss_Start;sl<=StopLoss_Stop;sl+=StopLoss_Step)
                          for(int dir=0;dir<2;dir++)
                             {
                              int handle=iCustom(_Symbol,PERIOD_CURRENT,"::Indicators\\TestIndicator\\TestIndicator.ex5",HistoryDepth,
                                                                                                                        sl,
                                                                                                                        tp,
                                                                                                                        rsi,
                                                                                                                        rsi_tz,
                                                                                                                        wpr,
                                                                                                                        wpr_tz,
                                                                                                                        adx, 
                                                                                                                        adx_tz,
                                                                                                                        dir);
                              if(handle==INVALID_HANDLE)
                                 return INIT_FAILED;
                              ar_Handles.Add(handle);
                             }

После успешного запуска всех тестер-индикаторов инициализируем класс торговых операций и завершаем выполнение функции.

   Trade.SetAsyncMode(false);
   if(!Trade.SetTypeFillingBySymbol(_Symbol))
      return INIT_FAILED;
   Trade.SetMarginMode();
//---
   return(INIT_SUCCEEDED);
  }

Фильтрацию входных сигналов и торговые операции осуществляем в функции OnTick. Так как ранее мы определились, что открывать позиции будем только на открытии бара, то и в начале функции проверим появление этого события.

void OnTick()
  {
//---
   static datetime last_bar=0;
   datetime cur_bar=(datetime)SeriesInfoInteger(_Symbol,PERIOD_CURRENT,SERIES_LASTBAR_DATE);
   if(cur_bar==last_bar)
      return;

Вторым нашим ограничением является наличие не более одной открытой сделки в момент времени. Поэтому, при наличии открытой позиции прекращаем выполнение функции.

   if(PositionSelect(_Symbol))
     {
      last_bar=cur_bar;
      return;
     }

После проверки контрольных точек переходим к основному циклу перебора всех индикаторов в поисках сигналов. В начале цикла пытаемся загрузить сигнальный буфер индикатора. Если индикатор еще не пересчитан, или сигнал на совершение торговой операции отсутствует, переходим к следующему индикатору.

   int signal=0;
   double probability=0;
   double profit_factor=0;
   double tp=0,sl=0;
   bool ind_caclulated=false;
   double temp[];
   for(int i=0;i<ar_Handles.Total();i++)
     {
      if(CopyBuffer(ar_Handles.At(i),2,1,1,temp)<=0)
         continue;
      ind_caclulated=true;
      if(temp[0]==0)
         continue;

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

      if(signal!=0 && temp[0]!=signal)
        {
         last_bar=cur_bar;
         return;
        }
      signal=(int)temp[0];

Затем проверяем наличие минимально-необходимого количества сделок в тестируемом периоде. Если выборка является недостаточной, то переходим к следующему индикатору.

      if(CopyBuffer(ar_Handles.At(i),1,1,1,temp)<=0 || temp[0]<MinOrders)
         continue;

Далее, аналогично проверяется достаточность вероятности получения прибыльной сделки.

      if(CopyBuffer(ar_Handles.At(i),0,1,1,temp)<=0 || temp[0]<MathMax(probability,MinProbability))
         continue;

Если расхождения вероятности получения прибыльной сделки по анализируемому индикатору и проверенными ранее меньше 1 процента, из двух проходов выбирается лучший по профит-фактору и соотношению прибыли к риску. Для дальнейшей работы сохраняются данные лучшего прохода.

      if(MathAbs(temp[0]-probability)<=1)
        {
         double ind_probability=temp[0];
//---
         if(CopyBuffer(ar_Handles.At(i),3,1,1,temp)<=0 || temp[0]<MathMax(profit_factor,MinProfitFactor))
            continue;
         double ind_profit_factor=temp[0];
         if(CopyBuffer(ar_Handles.At(i),5,1,1,temp)<=0)
            continue;
         double ind_tp=temp[0];
         if(CopyBuffer(ar_Handles.At(i),6,1,1,temp)<=0)
            continue;
         double ind_sl=temp[0];
         if(MathAbs(ind_profit_factor-profit_factor)<=0.01)
           {
            if(sl<=0 || tp/sl>=ind_tp/ind_sl)
               continue;
           }
//---
         probability=ind_probability;
         profit_factor=ind_profit_factor;
         tp=ind_tp;
         sl=ind_sl;
        }

Если вероятность получения прибыльной сделки явно больше, проверяется выполнение требований к проходу по профит-фактору. При соблюдении всех требований информация о проходе сохраняется для дальнейшей работы.

      else /* MathAbs(temp[0]-probability)<=1 */
        {
         double ind_probability=temp[0];
//---
         if(CopyBuffer(ar_Handles.At(i),3,1,1,temp)<=0 || temp[0]<MinProfitFactor)
            continue;
         double ind_profit_factor=temp[0];
         if(CopyBuffer(ar_Handles.At(i),5,1,1,temp)<=0)
            continue;
         double ind_tp=temp[0];
         if(CopyBuffer(ar_Handles.At(i),6,1,1,temp)<=0)
            continue;
         double ind_sl=temp[0];
         probability=ind_probability;
         profit_factor=ind_profit_factor;
         tp=ind_tp;
         sl=ind_sl;
        }
     }

В случае, если после проверки всех тестер-индикаторов ни один из них не был пересчитан, выходим из функции до следующего тика в ожидании пересчета индикаторов.

   if(!ind_caclulated)
      return;

После успешной поверки индикаторов и при отсутствии действующего сигнала на совершение торговых операций выходим из функции до формирования нового бара.

   last_bar=cur_bar;
//---
   if(signal==0 || probability==0 || profit_factor==0 || tp<=0 || sl<=0)
      return;

В завершении функции, при наличии сигнала на открытие позиции отправляем ордер в соответствии с лучшим проходом.

   if(signal==1)
     {
      double price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      tp+=price;
      sl=price-sl;
      Trade.Buy(Lot,_Symbol,price,sl,tp,"Real Time Optimizator");
     }
   else
      if(signal==-1)
        {
         double price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
         tp=price-tp;
         sl+=price;
         Trade.Sell(Lot,_Symbol,price,sl,tp,"Real Time Optimizator");
        }
  }

С полным кодом эксперта можно ознакомиться во вложении.

5. Тестирование подхода

Для демонстрации работы метода было проведено тестирование построенного советника с параллельной оптимизацией классического эксперта с форвард-тестированием при использовании аналогичных диапазонов изменяемых параметров и сохранением общего временного интервала тестирования. С целью соблюдения равных условий был построен советник, запускающий только один тестер-индикатор и открывающий сделки по всем его сигналам без фильтрации по статистическим данным. Структура его построения аналогична советнику, построенному выше, за исключением блока фильтрации состоятельности сигнала. С полным кодом советника можно ознакомиться во вложении (ClassicExpert.mq5).

Тестирование советников проводилось на таймфрейме Н1 за 7 месяцев 2018 года. Для форвард-тестирования классического советника было оставлено 1/3 тестируемого периода.

Параметры тестирования

В качестве параметров для оптимизации были взяты периоды расчета индикаторов. Для всех индикаторов был взят один диапазон значений от 5 до 30 с шагом 5.

Параметры тестирования

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

Результаты оптимизации

Результаты графического анализа проведенных оптимизации и форвард-тестирования показали изменение структуры движения цены, в результате чего произошло смещение зоны прибыльности по периоду индикатора WPR.

График оптимизации по WPRГрафик Форвар-тестирования по WPR

Для проведения тестирования эксперта, построенному по предложенному методу, были заданы аналогичные параметры тестирования с сохранением анализируемого периода. Для фильтрации сигналов на открытие позиции были заданы минимальная вероятность прибыльной сделки 60% и минимальный профит-фактор за тестируемый период равный 2. Глубина тестирования задана в 500 свечей.

Тестирование предложенного метода

В результате тестирования советник показал прибыль за анализируемый период с фактическим профит-фактором 1.66. Стоит отметить, что при проведении данного тестирования в визуальном режиме агент тестирования занимал 1250 Мб оперативной памяти.

Заключение

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

Ссылки

  1. Создаем новую торговую стратегию с использованием технологии разложения входов на индикаторы

Программы, используемые в статье:

#
Имя
Тип
Описание
1Deal.mqhБиблиотека классаКласс виртуальных сделок
2TestIndicator.mq5ИндикаторТестер-Индикатор
3RealTimeOptimization.mq5СоветникСоветник, построенный по предложенному методу
4ClassicExpert.mq5СоветникСоветник, построенный по классическому методу для проведения сравнительной оптимизации