Передача данных между индикаторами - простое решение наболевшей проблемы

Alexey Subbotin | 15 января, 2010

Введение

Ценность новичков – не только в том, что они задают вопросы, упорно не желая пользоваться поиском и тем самым побуждая остальных к созданию бросающихся в глаза разделов с названиями вроде «FAQ», «Новичкам сюда» или «Тот, кто задаст вопрос из этого списка, будет гореть в аду». Их истинное предназначение – задавать вопросы, начинающиеся с фразы «А как… ?» и «Можно ли… ?» и получать ответы «Никак» и «Нельзя».

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

1. Постановка задачи

Вот, к примеру, не слишком давняя цитата одной из веток форума MQL4 Community (орфография и пунктуация подправлена))):

 … на рабочем столе установлены два индикатора (назовем их А и В). Индикатор А строится как обычно, непосредственно с графика [цены - авт.], а индикатор В с данных индикатора А. Теперь возникает вопрос, как сделать так, чтобы индикатор В рисовался не через iCustom("indicator A", ...), а динамически с данных индикатора А (который уже установлен на графике), то есть меняя настройки индикатора А, не нужно было бы менять соответственно параметры индикатора В?

или в другой формулировке:

… Допустим, мы к графику прикрепили индикатор Moving Average, теперь как можно добраться к буферу этого индикатора непосредственно?

и еще:

… при обращении к какому либо индикатору через вызов iCustom вызываемый индикатор полностью перегружается, даже если был загружен до вызова. Это можно как-нибудь обойти? 

Список вопросов подобного рода можно продолжать довольно долго, они возникают у многих пользователей, и не только у новичков. Если обобщить, то, по сути, проблема заключается в том, что в платформе MetaTrader отсутствует штатный механизм доступа к показаниям пользовательских индикаторов – без использования функции iCustom (MQL4) либо связки iCustom - CopyBuffer (MQL5). А было бы так соблазнительно при разработке очередного шедевра на MQL написать в коде программы – возьми-ка ты, дружок, данные с такого-то графика, да посчитай мне из них то-то…

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

На моей памяти предлагались варианты «доступа к гландам» (это тоже цитата с форума:) различными путями, в том числе:

ну, и ряд других, в той или иной степени повторяющих перечисленные, а также и совсем экзотические вроде сокетов, мэйлслотов и т.д. (есть еще и радикальный метод, часто используемый при написании советников – перенос расчетной индикатора непосредственно в код эксперта, но это уже немного «из другой оперы»).

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

Итак, попробуем сформулировать задачу: 

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

При разработке автор имел в своем распоряжении компилятор семейства С++ Builder для создания DLL, а также терминалы MetaTrader 4 и MetaTrader 5. Исходные коды ниже будут приводиться на языке программирования MQL5, вариант для предыдущей версии языка прикреплен к статье, его существенные отличия будут прокомментированы.

2. Массивы

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

Поскольку динамические массивы в MQL – образования переменного размера, закономерно встает вопрос, каким образом разрешается проблема перераспределения памяти под данные при увеличении размера массива при недоступности участка памяти, следующего за уже имеющимися данными массива. Принципиальных вариантов всего два: 

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

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

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

Хотя данный вывод не 100% логически строг, его все-таки можно считать вполне достоверным (что косвенно подтверждается и исправной работой нашего продукта, эксплуатирующего эту идею). 

Таким образом, будем считать верными следующие утверждения:

И еще примем некоторые допущения: под моментами времени мы понимаем моменты вызова функции OnCalculate() (для MQL4 – start()) соответствующего индикатора, кроме того, для простоты будем считать, что буферы данных, с которыми мы работаем, имеют одно измерение и тип double[].

3. Sine qua non

Общеизвестно, что язык MQL не поддерживает работу с такими вещами как указатели (так называемый указатель на объект не в счет, поскольку это собственно и не указатель вовсе) – это неоднократно утверждалось и подтверждалось представителями компании MetaQuotes Software. Проверим, так ли это :-).

Что такое, в сущности, указатель? Это не просто идентификатор со звездочкой, а адрес ячейки в памяти компьютера. А что представляет собой адрес ячейки? Это ее номер по порядку от некоего начала. Наконец, что же такое номер? Это целое число, поставленное в соответствие данной ячейке. А теперь спросим себя: что мешает нам работать с указателем как с целым числом? Да ничего – ведь программа MQL отлично работает с целыми числами! 

Только один вопрос – как преобразовать указатель в целое. В этом нам и поможет Dynamic Link Library, а именно, возможности языка C++ по приведению типов данных. В связи с тем, что указатели в C++ являются четырехбайтовым типом данных, в наших целях удобно использовать четырехбайтовый же тип int.

На знаковый бит можно внимания не обращать, так как, по большому счету, безразлично, как его интерпретировать в контексте типа int (если он равен 1, то, вообще говоря, это число будет выглядеть как отрицательное), главное, что мы можем сохранить все биты указателя в неизменном виде. Можно, конечно, использовать и беззнаковый uint, однако автору хотелось бы обеспечить минимальное количество отличий версий библиотеки для MQL5 и MQL4, где беззнаковые типы отсутствуют.

Итак, 

extern "C" __declspec(dllexport) int __stdcall GetPtr(double *a)
{
        return((int)a);
}

Вуаля, имеем в распоряжении длинное целое, содержащее адрес начала массива! Осталось научиться считывать значение его i-го элемента:

extern "C" __declspec(dllexport) double __stdcall GetValue(int pointer,int i)
{
        return(((double*) pointer)[i]);
}

… и записывать (вообще-то наша задача этого не требует, но для комплекта…):

extern "C" __declspec(dllexport) void __stdcall SetValue(int pointer,int i,double value)
{
        ((double*) pointer)[i]=value;
}

Собственно, все. Теперь MQL умеет работать с указателями :-).

4. Обертка

Ядро системы нами уже создано, теперь предстоит рутинная работа – облечь его в удобоваримую для MQL-программ форму. Тем не менее, любителей эстетики прошу не расстраиваться, будет и еще кое-что :-).

В принципе, вариантов, как поступить дальше – миллиард. Мы пойдем по пути, который показался автору наиболее приемлемым. Вспомним, что в терминале MetaTrader есть штатное средство для обмена небольшими порциями информации между отдельными программами – Global Variables. Представляется целесообразным использовать именно их для хранения указателей на индикаторные буфера, к которым мы будем осуществлять доступ. Обзовем совокупность таких переменных таблицей дескрипторов. Каждый дескриптор будет иметь имя – строку следующего вида:

строковый_идентификатор#номер_буфера#символ#период#длина_буфера#направление_индексации#случайное_число,

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

Немного о значении полей дескриптора. 

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

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

void RegisterBuffer(double &Buffer[], string name, int mode) export
{
   UnregisterBuffer(Buffer);                    //сначала удалим на всякий случай переменную
   
   int direction=0;
   if(ArrayGetAsSeries(Buffer)) direction=1;    //устанавливаем флаг направления индексации

   name=name+"#"+mode+"#"+Symbol()+"#"+Period()+"#"+ArraySize(Buffer)+"#"+direction;
   int ptr=GetPtr(Buffer);                      //вытаскиваем указатель
   if(ptr==0) return;
   
   MathSrand(ptr);                              //очень удобно использовать значение указателя
                                                //(а не время) для инициализации ГПСЧ
   while(true)
   {
      int rnd=MathRand();
      if(!GlobalVariableCheck(name+"#"+rnd))    //обеспечим уникальность - полагаем,
      {                                         //что вряд ли кому придет в голову
                                                //исследовать больше, чем RAND_MAX буферов:)
         name=name+"#"+rnd;                     
         GlobalVariableSet(name,ptr);           //ну и записываем в глобальные
         break;
      }
   }   
}
void UnregisterBuffer(double &Buffer[]) export
{
   int ptr=GetPtr(Buffer);                      //будем разрегистрироваться по фактическому адресу буфера
   if(ptr==0) return;
   
   int gt=GlobalVariablesTotal();               
   int i;
   for(i=gt-1;i>=0;i--)                         //просто перебрать все глобальные переменные 
   {                                            //и удалить наш буфер отовсюду, где он "засветился"
      string name=GlobalVariableName(i);        
      if(GlobalVariableGet(name)==ptr)
         GlobalVariableDel(name);
   }      
}

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

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

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

int FindBuffers(string name, int mode, string symbol, int period, string &buffers[]) export
{
   int count=0;
   int i;
   bool found;
   string name_i;
   string descriptor[];
   int gt=GlobalVariablesTotal();

   StringTrimLeft(name);                                    //обрезать строку от лишних пробелов
   StringTrimRight(name);
   
   ArrayResize(buffers,count);                              //обнулить размер

   for(i=gt-1;i>=0;i--)
   {
      found=true;
      name_i=GlobalVariableName(i);
      
      StringExplode(name_i,"#",descriptor);                 //разбить строку на поля
      
      if(StringFind(descriptor[0],name)<0&&name!=NULL) found=false; //проверяем каждое поле 
                                                                    //на соответствие критерию поиска
      if(descriptor[1]!=mode&&mode>=0) found=false;
      if(descriptor[2]!=symbol&&symbol!=NULL) found=false;
      if(descriptor[3]!=period&&period>0) found=false;
      
      if(found)
      {
         count++;                                           //все условия сошлись, заносим в список
         ArrayResize(buffers,count);
         buffers[count-1]=name_i;
      }
   }
   
   return(count);
}

Тут можно немного прокомментировать. Как видно из кода функции, некоторые условия поиска можно пропускать. Так, если в качестве параметра name передать NULL, проверка поля строковый_идентификатор дескриптора будет пропускаться. Аналогично и по другим полям: для пропуска соответствующих элементов надо задать mode<0, symbol:=NULL или period<=0. Это при необходимости расширит возможности поиска в таблице дескрипторов.

Например, можно найти все индикаторы Moving Average на всех окнах или только на окнах EURUSD с периодом M15 и т.п. Еще одно замечание: проверка поля строковый_идентификатор производится не по строгому равенству, а с помощью StringFind() – это сделано для того, чтобы можно было осуществлять поиск по части дескриптора (скажем, несколько индикаторов задают строку вида “МА(xxx)”, а поиск осуществляем по строке “МА” – найдем все зарегистрированные МАшки).

Еще нам потребовалась функция StringExplode(string s, string separator, string &result[]). Она делит строку s на части, используя разделитель separator, и записывает результат в массив result.

void StringExplode(string s, string separator, string &result[])
{
   int i,pos;
   ArrayResize(result,1);
   
   pos=StringFind(s,separator); 
   if(pos<0) {result[0]=s;return;}
   
   for(i=0;;i++)
   {
      pos=StringFind(s,separator); 
      if(pos>=0)
      {
         result[i]=StringSubstr(s,0,pos);
         s=StringSubstr(s,pos+StringLen(separator));
      }
      else break;
      ArrayResize(result,ArraySize(result)+1);
   }
}

Теперь, когда мы получили список необходимых дескрипторов, осталось достать данные из индикатора:

double GetIndicatorValue(string descriptor, int shift) export
{
   int ptr;
   string fields[];
   int size,direction;
   if(GlobalVariableCheck(descriptor)>0)               //убедились в валидности дескриптора
   {                
      ptr = GlobalVariableGet(descriptor);             //получили значение указателя
      if(ptr!=0)
      {
         StringExplode(descriptor,"#",fields);         //разбили имя дескриптора на поля
         size = fields[4];                             //нам нужен текущий размер массива
         direction=fields[5];                                 
         if(direction==1) shift=size-1-shift;          //если индексация с конца
         if(shift>=0&&shift<size)                      //проверка правильности индекса - чтобы не уронить терминал
            return(GetValue(MathAbs(ptr),shift));      //добро, возвращаем значение
      }   
   } 
   return(EMPTY_VALUE);                                //в противном случае извиняйте...
}

Как видим, эта функция представляет собой «обертку» для вызываемой из DLL GetValue(). Перед обращением к ней необходимо проверить, правильно ли указан дескриптор, не выходит ли индекс массива за его текущие пределы, а также изменить направление индексации на обратное в случае, если значение дескриптора отрицательно. При неудачном завершении функция вернет EMPTY_VALUE.

Почти так же осуществляется и запись:

bool SetIndicatorValue(string descriptor, int shift, double value) export
{
   int ptr;
   string fields[];
   int size,direction;
   if(GlobalVariableCheck(descriptor)>0)               //убедились в валидности дескриптора
   {                
      ptr = GlobalVariableGet(descriptor);             //получили значение указателя
      if(ptr!=0)
      {
         StringExplode(descriptor,"#",fields);         //разбили имя дескриптора на поля
         size = fields[4];                             //нам нужен текущий размер массива
         direction=fields[5];                                 
         if(direction==1) shift=size-1-shift;          //если индексация с конца
         if(shift>=0&&shift<size)                      //проверка правильности индекса - чтобы не уронить терминал
         {
            SetValue(MathAbs(ptr),shift,value);
            return(true);
         }   
      }   
   }
   return(false);
}

При удачной проверке всех условий из DLL вызывается функция SetValue(). Возвращаемое значение соответствует успешности всех операций: true – успешно, false – ошибка.

5. Проверка боем

Настало время испытать все на примере. Возьмем в качестве жертвы входящий в стандартную поставку индикатор Average True Range (ATR) и покажем, какие изменения нужно внести в его код, чтобы, скажем, скопировать его значения в окно другого индикатора. Заодно можно и показать, как поменять что-нибудь в исходном индикаторе.

Первое, что мы сделаем, это добавим несколько строчек в начало функции OnCalculate():

//+------------------------------------------------------------------+

//| Average True Range                                               |
//+------------------------------------------------------------------+
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 &TickVolume[],
                const long &Volume[],
                const int &Spread[])
  {
   if(prev_calculated!=rates_total)
      RegisterBuffer(ExtATRBuffer,"ATR",0);
…

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

Кроме этого, понадобится удалить дескрипторы из таблицы при завершении работы индикатора. Для этого необходимо написать обработчик события deinit (не забываем, что он должен возвращать void и иметь один входной параметр виде const int reason): 

void OnDeinit(const int reason)
{
   UnregisterBuffer(ExtATRBuffer);
}
Вот что показывает наш видоизмененный индикатор в окне терминала (Рис. 1) и в списке глобальных переменных (Рис. 2):

 

Рис. 1. Индикатор Average True Range

Рис. 2. Дескриптор создается в списке глобальных переменных терминала

Рис. 2. Дескриптор создается в списке глобальных переменных терминала 

На следующем шаге попытаемся получить доступ к данным индикатора ATR. Создадим новый индикатор (назовем его test) и напишем:

//+------------------------------------------------------------------+
//|                                                         test.mq5 |
//|                                             Copyright 2009, alsu |
//|                                                 alsufx@gmail.com |
//+------------------------------------------------------------------+
#property copyright "2009, alsu"
#property link      "alsufx@gmail.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1

#include <exchng.mqh>

//---- plot ATRCopy
#property indicator_label1  "ATRCopy"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Red
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- indicator buffers
double ATRCopyBuffer[];

string atr_buffer;
string buffers[];

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+

int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,ATRCopyBuffer,INDICATOR_DATA);
//---
   return(0);
  }

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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 found=FindBuffers("ATR",0,NULL,0,buffers);   //поиск дескрипторов среди глобальных переменных
   if(found>0) atr_buffer=buffers[0];               //нашли? сохраняем указатель в переменную
   else atr_buffer=NULL;
   int i;
   for(i=prev_calculated;i<rates_total;i++)
   {
      if(atr_buffer==NULL) break;
      ATRCopyBuffer[i]=GetIndicatorValue(atr_buffer,i);  //ну а теперь достать данные
      SetIndicatorValue(atr_buffer,i,i);                 //никакого труда не представляет,
                                                         //как и записать впрочем
   }
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

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

Теперь запускаем индикатор test (наш ATR должен до сих пор быть прикреплен к окну). На Рис. 3 видим, что график ATR волшебным образом перебрался в нижнее окошко, а на его месте, как нам и хотелось, появилась прямая линия, значения которой соответствуют индексам массива.

 

Рис. 3. Результат работы индикатора Test.mq5

Ловкость рук – и никакого мошенничества :)

6. Backward compatibility

Автор попытался сделать библиотеку, написанную в 5 версии языка MQL, максимально близкой к стандартам MQL4. В связи с этим изменения, которые необходимо внести в программу для ее успешного применения в MetaTrader 4, минимальны.

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

Заключение

Несколько слов о перспективах. 

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

Из технических моментов пока вижу необходимость в двух вещах:

Также буду рад любым другим предложениям по усовершенствованию библиотеки.

Все необходимые файлы находятся во вложении к статье. Представлены варианты на MQL5, MQL4, а также исходный код библиотеки exchng.dll. Файлы разобраны по папкам так, как они должны лежать в директориях соответствующих терминалов.

Предупреждение

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

Благодарности

В статье использованы цитаты с форума MQL4.Community http://forum.mql4.com и ссылки на идеи пользователей igor.senych, satop, bank, StSpirit, TheXpert, jartmailru, ForexTools, marketeer, IlyaA – простите, если кого забыл.