Советник Complex_Expert_V2

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

 

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

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

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

В чем же заключается это "индивидуальное оформление"? Дело в том, что одним из самых распространенных и удобных способов передачи информации между советником и индикатором является использование глобальных переменных терминала ("Сервис"-"Глобальные переменные" или просто F3). Поэтому сутью доработки каждого индикатора будет являться создание таких глобальных переменных, которые будут явно идентифицированы советником. Здесь основной проблемой является синхронизация передачи/получения информации. Ведь мы не хотим, чтобы советник реагировал на сигналы, которые индикатор послал еще два часа назад.

Для проведения синхронизации МТ4 обладает уникальной характеристикой - время открытия бара, которое является одинаковым как для советника, так и для индикатора. Поэтому первой глобальной переменной для связи индикатор-эксперт будет переменная, несущая время последнего установленного сигнала. Непосредственно сигнал будем передавать через вторую переменную, значение которой соответствует всего лишь трем состояниям - "Buy", "Sell' и "нет сигнала".

Остается только привести названия переменных к единому формату, чтобы советник мог различать индикаторы между собой. Первыми символами в имени переменной определим принадлежность переменной к эксперту по первым буквами его названия - "CE" (см. рис. 1 - красный цвет). Дальше необходимо указать, на какой валютной паре установлен индикатор. Чаще всего это шесть символов (отмечены желтым цветом). После названия инструмента будет идти сокращенное имя индикатора (зеленый цвет) и в конце неизменные пять символов, определяющие непосредственно тип переменной - "STime" (Signal Time).

 

Рис. 1. - Кодирование имени глобальной переменной для времени сигнала.

Подобным же образом будут распределяться символы в названии глобальной переменной, отвечающей за содержимое сигнала. Ее имя Signal (см. рис. 2).

 

Рис. 2. - Кодирование имени глобальной переменной для значения сигнала.

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

 
//+-------------------------------------------------------------------------------------+
//| Расчет сигнала                                                                      |
//|             Если значение Signal больше нуля, значит Buy                            |      
//|             Если значение Signal меньше нуля, значит Sell                           |      
//+-------------------------------------------------------------------------------------+
bool GetSignal()
{
// - 1 - =========================== Обнуление данных ===================================
 Signal = 0;                                                                    // Сигнал
 UseInd = 0;                                    // Количество присоедниненных индикаторов
 Res = True;                                              // Флаг отработки текущего бара
// - 1 - =========================== Окончание блока ====================================
 
 Comm = "";
// - 2 - =========================== Перебор всех существующих индикаторов ==============
 GetOneSignal("CE"+Symbol()+"ADXSTime", "CE"+Symbol()+"ADXSignal", "ADX\n");
 GetOneSignal("CE"+Symbol()+"BandsSTime", "CE"+Symbol()+"BandsSignal", 
              "Bollinger Bands\n");
 GetOneSignal("CE"+Symbol()+"CCISTime", "CE"+Symbol()+"CCISignal", "CCI\n");
 GetOneSignal("CE"+Symbol()+"CrossMASTime", "CE"+Symbol()+"CrossMASignal", "CrossMA\n");
 GetOneSignal("CE"+Symbol()+"PSARSTime", "CE"+Symbol()+"PSARSignal", "Parabolic\n");
 GetOneSignal("CE"+Symbol()+"StDevSTime", "CE"+Symbol()+"StDevSignal", "StDev\n");
 GetOneSignal("CE"+Symbol()+"MACDSTime", "CE"+Symbol()+"MACDSignal", "MACD\n");
 GetOneSignal("CE"+Symbol()+"DMSTime", "CE"+Symbol()+"DMSignal", "DeMarker\n");
 GetOneSignal("CE"+Symbol()+"EnvSTime", "CE"+Symbol()+"EnvSignal", "Envelopes\n");
 GetOneSignal("CE"+Symbol()+"FISTime", "CE"+Symbol()+"FISignal", "ForceIndex\n");
 GetOneSignal("CE"+Symbol()+"MomSTime", "CE"+Symbol()+"MomSignal", "Momentum\n");
 GetOneSignal("CE"+Symbol()+"OsMASTime", "CE"+Symbol()+"OsMASignal", "OsMA\n");
 GetOneSignal("CE"+Symbol()+"RSISTime", "CE"+Symbol()+"RSISignal", "RSI\n");
 GetOneSignal("CE"+Symbol()+"RVISTime", "CE"+Symbol()+"RVISignal", "RVI\n");
 GetOneSignal("CE"+Symbol()+"StochSTime", "CE"+Symbol()+"StochSignal", "Stochastic\n");
 GetOneSignal("CE"+Symbol()+"WPRSTime", "CE"+Symbol()+"WPRSignal", "WPR\n");
 GetOneSignal("CE"+Symbol()+"AOSTime", "CE"+Symbol()+"AOSignal", "AO\n");
 GetOneSignal("CE"+Symbol()+"ACSTime", "CE"+Symbol()+"ACSignal", "AC\n");
 GetOneSignal("CE"+Symbol()+"FracSTime", "CE"+Symbol()+"FracSignal", "Fractals\n");
 GetOneSignal("CE"+Symbol()+"AlligatorSTime", "CE"+Symbol()+"AlligatorSignal",
              "Alligator\n");
 GetOneSignal("CE"+Symbol()+"BWMFISTime", "CE"+Symbol()+"BWMFISignal",
              "Bill Williams MFI\n");
// - 2 - =========================== Окончание блока ====================================
  
 if (!Res) return(False);    // Один (или больше) из индикаторов не успел обновить данные
   
// - 3 - =========================== Сообщение о кол-ве индикаторов =====================
 if (UseInd == 0)
   {
    CommAll = "Не присоединен ни один индикатор!";
    Comment(CommAll);
    return(False);
   } 
  else
   {
    CommAll = "Подключены следующие индикаторы:\n"+Comm;
    Comment(CommAll);
   } 
// - 3 - =========================== Окончание блока ====================================

// - 4 - ========================= Сравнение кол-ва индикаторов и общего сигнала ========
 if (UseInd == MathAbs(Signal))
   if (Signal > 0)
     {
      if (LastBuySignal <= LastSellSignal)
        LastBuySignal = Time[0];
     } 
    else
      if (LastSellSignal <= LastBuySignal)
        LastSellSignal = Time[0];
// - 4 - =========================== Окончание блока ====================================

 return(True);
}

В первом блоке приводятся к нулю переменные Signal (значение совокупного сигнала всех используемых индикаторов) и UseInd (количество используемых индикаторов). Напомню, для получения сигнала Buy или Sell значения Signal и UseInd должны быть равны по модулю. Флаг Res сигнализирует о получении свежей информации от всех присоединенных пользователем индикаторов. Это необходимо для повторения опроса всех индикаторов, если к моменту опроса не все индикаторы успели установить сигнал, соответствующий текущему бару. Когда все значения индикаторов признаны актуальными, возврат в функцию GetSignal на текущем баре больше не происходит.

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

Третий блок выводит информационную строку в верхнем левом углу графика валютной пары о присоединенных индикаторах. Заметьте, если значение UseInd равно нулю, то расчет сигнала не производится и результат функции GetSignal - False. Такой результат приведет к вызову функции на следующем тике. То же самое происходит, если один или более индикаторов не успел обновить данные (Res = False).

Четвертый блок - точная копия окончания функции GetSignal из первой версии эксперта.

Функция GetOneSignal  имеет меньший объем:

 
//+-------------------------------------------------------------------------------------+
//| Получение сигнала по одному индикатору                                              |
//+-------------------------------------------------------------------------------------+
void GetOneSignal(string ST, string SS, string IndName)
{
 if (GlobalVariableCheck(ST))      // Переменная существует, значит индикатор присоединен
   {
    Comm = Comm + IndName;                      // Добавляем в строку название индикатора
    UseInd++;                // Увеличиваем количество присоелиненных индикаторов на один
    if (GlobalVariableGet(ST) >= Time[0])           // Если сигнал содержит свежие данные
      Signal += NormalizeDouble(GlobalVariableGet(SS), 0);  // то принимаем их к сведению
     else
      Res = False;                                          // иначе ждем следующего тика
   }
}

Функция выполняется только если глобальная переменная STime (имя передано в переменной ST) существует. Если это так, то предполагается, что глобальная переменная Signal, соответствующая данному индикатору, тоже существует. Но перед тем как прочесть значение сигнала, переменная Comm дополняется названием индикатора (IndName), а количество участвующих в расчете сигнала индикаторов (UseInd) увеличивается на единицу. Затем считывается время последнего обновления сигнала, что делается проверкой значения STime. Если время обновления принадлежит текущему бару, то только тогда производится чтение значения глобальной переменной Signal. В противном случае сигнал не читается, а флаг Res изменяет свое состояние на False.

Все остальные функции эксперта остаются неизменными, поэтому вторая версия советника уже полностью укомплектована. Далее нам потребуются индикаторы, которые визуально должны являться точной копией стандартных индикаторов. Все коды приводить не будем, так как принцип работы у индикаторов похожий. Рассмотрим этот принцип на примере индикатора MACD. Он, кстати, не будет выглядеть как стандартный. Внесем в него некоторые улучшения, а именно: увеличение и уменьшение значения гистограммы будут отличаться по цвету, как это сделано в индикаторе 3colorMACD. Итак, функция инициализации индикатора:

 
//+-------------------------------------------------------------------------------------+
//| Custom indicator initialization function                                            |
//+-------------------------------------------------------------------------------------+
int init()
  {
   Activate = False;
// - 1 - ============================ Управление отображением индикатора ================
   IndicatorBuffers(4);
   if (ShowMACD)
     {
      SetIndexStyle(0, DRAW_HISTOGRAM, STYLE_SOLID, 3);
      SetIndexStyle(1, DRAW_HISTOGRAM, STYLE_SOLID, 3);
      SetIndexStyle(2, DRAW_LINE, STYLE_DASHDOTDOT, 1);
     }
    else
     {
      SetIndexStyle(0, DRAW_NONE);
      SetIndexStyle(1, DRAW_NONE);
      SetIndexStyle(2, DRAW_NONE);
     }  
// - 1 - ============================ Окончание блока ===================================
     
// - 2 - ============================ Инициализация буферов индикатора ==================
   SetIndexBuffer(0, MACDGr);
   SetIndexBuffer(1, MACDLe);
   SetIndexBuffer(2, MACDS);
   SetIndexBuffer(3, MACD);
   SetIndexEmptyValue(0, 0.0);
   SetIndexEmptyValue(0, 0.0);
// - 2 - ============================ Окончание блока ===================================
   
// - 3 - ======== Проверка существования такого индикатора на графике ===================
   STime = "CE"+Symbol()+"MACDSTime";                                 // Время обновления
   Signal = "CE"+Symbol()+"MACDSignal";                           // Сигнал (-1, 0 или 1)
   if (GlobalVariableCheck(STime))           // Если такая глобальная переменная есть, то 
     {                                // это означает, что такой индикатор уже прикреплен
      Alert("Один индикатор ''MACD'' уже присоединен!");  //и индикатор работать не будет
      return(0);       
     }
// - 3 - ============================ Окончание блока ===================================
     
   GlobalVariableSet(STime, 0);                    // Последнее время обновления значений
   GlobalVariableSet(Signal, 0);                       // Текущее состояние "нет сигнала"
   
   LastBar = 0;
   Activate = True;  
//----
   return(0);
  }

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

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

А вот третий блок и является тем "индивидуальным оформлением", которое отличает индикатор от обычного пользовательского или стандартного индикатора. В переменные STime и Signal заносятся имена глобальных переменных, при помощи которых индикатор будет "общаться" с экспертом. Тут же проверяется наличие соответствующей глобальной переменной STime. Это делается для того, чтобы не допустить существование двух индикаторов одного типа на одном графике во избежание конфликтов в сигналах.

Если глобальная переменная не существует, то сразу же создается пара глобальных переменных STime и Signal, а также выдается разрешение на функционирование индикатора (Activate = True).

Чтобы индикатор не перепутал свое существование с существованием другого такого же, при отсоединении он должен удалить свои глобальные переменные. Заодно и эксперт будет уведомлен об отсутствии этого индикатора:

 
//+-------------------------------------------------------------------------------------+
//| Custom indicator deinitialization function                                          |
//+-------------------------------------------------------------------------------------+
int deinit()
  {
// - 1 - ========== Если индикатор был в работе, то "прибираем за собой" ================ 
   if (Activate)
     {
      GlobalVariableDel(STime);
      GlobalVariableDel(Signal);
     } 
// - 1 - ============================ Окончание блока ===================================
   return(0);
  }

 Здесь всего лишь одно действие - удаление глобальных переменных. Это действие производится только, если индикатор работал.

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

 
//+-------------------------------------------------------------------------------------+
//| Custom indicator iteration function                                                 |
//+-------------------------------------------------------------------------------------+
int start()
 { 
   if (!Activate) return(0);        // Индикатор работает только если был инициализирован
   
// - 1 - ============================ Стандартный блок каждого индикатора ===============
   int limit, i; 
   int counted_bars=IndicatorCounted();               // Сколько баров уже было посчитано
   if (counted_bars<0) return(-1);                           // Проверка возможной ошибки
   if (counted_bars>0) counted_bars--;         // Пересчитываем последний посчитанный бар
   limit=Bars-counted_bars;                    // Начинаем с последнего посчитанного бара
// - 1 - ============================ Окончание блока ===================================

// - 2 - ======================= Отображение индикатора на истории ====================== 
   for(i = limit; i >= 0; i--)
    {
     MACD[i] = iMACD(Symbol(), 0, MACDFast, MACDSlow, MACDSignal, MACDPrice,MODE_MAIN,i);
     if (MACD[i] > MACD[i+1])
       {
        MACDGr[i] = MACD[i];
        MACDLe[i] = 0.0;
       } 
      else
       {
        MACDLe[i] = MACD[i];         
        MACDGr[i] = 0.0;
       } 
     MACDS[i] = iMACD(Symbol(), 0, MACDFast, MACDSlow, MACDSignal, MACDPrice, MODE_SIGNAL, i);
    }   
// - 2 - ============================ Окончание блока ===================================
    
// - 3 - ======================= Выдача сигналов ======================================== 
   if (LastBar != Time[0])                                        // Один раз за один бар
     { 
      if (MACD[1] > MACDS[1])
        GlobalVariableSet(Signal, 1);                                              // Buy
       else 
        if (MACD[1] < MACDS[1])
          GlobalVariableSet(Signal, -1);                                          // Sell
         else
          GlobalVariableSet(Signal, 0);                                    // Нет сигнала
      GlobalVariableSet(STime, Time[0]);                   // Время последнего обновления
      LastBar = Time[0];     
     }   
// - 3 - ============================ Окончание блока ===================================
    
   return(0);
 }

Первый блок является стандартным. Его описание можно увидеть непосредственно в справке Meta Editor ("Справочник MQL4"-"Пользовательские индикаторы"-"Indicator counted").

Второй блок - это циклический расчет значений MACD. Если текущее значение гистограммы больше предыдущего, то значение MACD заносится в буфер MACDGr, иначе - значение получает буфер MACDLe. Значение сигнальной линии MACD напрямую попадает в MACDS.

В третьем блоке один раз за бар выносится вердикт сложившейся ситуации. Если гистограмма MACD выше сигнальной линии, то глобальная переменная терминала Signal получает значение 1 (сигнал BUY). Если MACD ниже сигнальной линии, то в Signal заносится -1 (сигнал SELL). В тех редких случаях, когда значения гистограммы и сигнальной линии равны (с учетом сравнения двух вещественных чисел это большая редкость), в Signal заносится 0 ("нет сигнала"). После присвоения значения переменной Signal, обновляется значение глобальной переменной STime, в которую заносится время открытия текущего бара. Таким вот нехитрым способом индикатор передает информацию о ситуации эксперту.

 

Общие принципы работы эксперта

Подводя итог, рассмотрим последовательность действий пользователя при работе с комплексным экспертом и присоединяемыми к нему индикаторами. Файл Complex_Expert_V2.mq4 необходимо положить в папку MQL4\Experts рабочего каталога терминала (для ее открытия используется главное меню терминала Файл - Открыть каталог данных). Содержимое архива присоединяемые индикаторы нужно распаковать в папку MQL4\Indicators. Только после этого можно запускать МТ4. Чтобы пользователю было легче отличать индикаторы эксперта от других подобных, имена их файлов начинаются с символов "CE". Например, версия индикатора MACD называется CE_MACD.mq4.

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

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

Еще одно обстоятельство, которое стоит учитывать, это невозможность открытия одного CE-индикатора пусть даже с разными настройками в пределах одной валютной пары (например, графики разные, таймфреймы разные, а валютная пара одна). Этот же момент касается проверки советника и индикаторов в тестере стратегий в режиме визуализации. Если индикатор присоединен уже на "живую" валютную пару, то его нельзя использовать на этой же валютной паре в тестере стратегий. Кстати, тестирование Complex_Expert_V2 возможно только в режиме визуализации, так как только в нем возможно присоединение индикатора к графику. И хитрости с применением кнопки "Пропустить до" режима визуализации ни к чему не приведут. Поэтому для тестирования стратегии все же нужно применять первую версию советника, а вот для работы онлайн намного удобнее использовать вторую версию советника.

 

Заключение

Удобством продемонстрированного подхода к автоторговле является возможность изменения настроек "налету" (правда с учетом применения настроек только к следующему бару) без необходимости вызова свойств эксперта. К тому же, не нужно задумываться, как интерпретировать значения индикаторов. Достаточно просто присоединить необходимые индикаторы к графику, а всю остальную работу проделает эксперт.

 

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

Игорь Герасько

Октябрь 2009