English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Параллельные вычисления в MetaTrader 5 штатными средствами

Параллельные вычисления в MetaTrader 5 штатными средствами

MetaTrader 5Примеры | 24 ноября 2010, 12:18
6 646 15
ds2
ds2

Введение в процессорную параллельность

Уже практически все современные персональные компьютеры умеют выполнять несколько задач одновременно – за счет наличия в процессоре нескольких ядер. Их число с каждым годом растет – 2, 3, 4, 6 ядер… Компания Intel недавно продемонстрировала работающий экспериментальный 80-ядерный процессор (да-да, это не опечатка - восемьдесят ядер, - жаль, в компьютерных магазинах появление этого чуда не планируется, так как данный процессор был создан лишь для изучения предельных возможностей используемой технологии).

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


Вариант A на диаграмме показывает, что происходит, когда на одноядерном процессоре выполняется одна программа. Процессор уделяет ее выполнению всё свое время, и программа выполняет некий объем своей работы за время T.

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

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

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

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


Параллельность в MetaTrader

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

Есть два способа решения этой проблемы:

1. Использовать DLL 2. Использовать внеязыковые ресурсы MetaTrader
Создав DLL на языке, имеющем встроенный инструментарий распараллеливания, мы получим распараллеливание и в MQL5-советнике. По заявлениям разработчиков MetaTrader, архитектура этого терминала – многопоточная. Значит, при определенных условиях, поступающие рыночные данные обрабатываются в отдельных программных потоках. Таким образом, если мы сможем нужным образом разделить код нашей программы на несколько экспертов или индикаторов, то MetaTrader сможет задействовать для ее выполнения несколько ядер процессора.


1-й способ мы в этой статье рассматривать не будем. Понятно, что в DLL можно реализовать всё что угодно. Мы же попробуем найти решение, задействующее лишь штатные средства MetaTrader и не требующее использования других языков, кроме MQL5.

Итак, подробнее о 2-м способе. Нам понадобится провести ряд экспериментов, чтобы выяснить, как именно в MetaTrader реализована поддержка многоядерности. Для этого создадим тестовый индикатор и тестового эксперта, которые будут выполнять какую-нибудь продолжительную работу, хорошо нагружающую процессор. Я написал такой индикатор i-flood:

#property indicator_chart_window

input string id;
//+------------------------------------------------------------------+
void OnInit()
  {
   Print(id,": OnInit");
  }
//+------------------------------------------------------------------+
int OnCalculate(const int rt,const int pc,const int b,const double &p[])
  {
   Print(id,": OnCalculate Begin");
   
   for (int i=0; i<1e9; i++)
     for (int j=0; j<1e1; j++);
     
   Print(id,": OnCalculate End");
   return(0);   
  }

И аналогичный ему эксперт e-flood:

input string id;
//+------------------------------------------------------------------+
void OnInit()
  {
   Print(id,": OnInit");
  }
//+------------------------------------------------------------------+
void OnTick()
  {
   Print(id,": OnTick Begin");
   
   for (int i=0; i<1e9; i++)
     for (int j=0; j<1e1; j++);
     
   Print(id,": OnTick End");
  }

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

Измерять количество работающих ядер можно через "Диспетчер задач" Windows:



Результаты всех измерений я собрал в таблицу:


комбинации
 Содержимое терминала
Загрузка
процессора
1
2 индикатора на одном графике 1 ядро
2
2 индикатора на разных графиках, одинаковая пара 1 ядро
3
2 индикатора на разных графиках, разные пары 2 ядра
4
2 эксперта на одном графике – такая ситуация невозможна -
5
2 эксперта на разных графиках, одинаковая пара 2 ядра
6
2 эксперта на разных графиках, разные пары 2 ядра
7
2 индикатора на разных парах, созданы из эксперта 2 ядра


7-я комбинация - это распространенный способ создания индикаторов, используется во многих торговых стратегиях. Единственная особенность - я создавал два индикатора на двух разных валютных парах, т.к. из комбинаций 1 и 2 понятно, что на одинаковой паре помещать индикаторы нет смысла. Для  этой комбинации я использовал эксперта e-flood-starter, который создавал две копии i-flood:

void OnInit() 
  {
   string s="EURUSD";
   for(int i=1; i<=2; i++) 
     {
      Print("Создан индикатор, handle=",
            iCustom(s,_Period,"i-flood",IntegerToString(i)));
      s="GBPUSD";
     }
  }

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


Проектируем параллельную систему

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

Такая система состоит из двух типов программных компонентов:

  • ВМ – вычислительный модуль. Их количество может быть от 2-х до количества ядер процессора. Именно в ВМ выносится весь код, который следует распараллелить. Как мы выяснили в предыдущей главе, ВМ может быть реализован как в виде индикатора, так и в виде эксперта – для любой формы реализации есть комбинации, использующие все ядра процессора;
  • ГМ – главный модуль. Осуществляет основные функции системы. Т.е. если ГМ является индикатором, то он выполняет рисование на графике; а если ГМ – эксперт, то осуществляет торговые функции. Также ГМ управляет всеми ВМ.

Например, для ГМ-эксперта и 2-ядерного процессора схема работы системы будет выглядеть так:



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




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

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

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

Комбинация

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

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

Обмен данными

Между ГМ и ВМ можно передавать информацию любым из 3-х способов:

  1. глобальные переменные терминала;
  2. файлы;
  3. индикаторные буферы.

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

Альтернативой является 2-й способ, т.к. в файл(ы) можно записать что угодно. И это удобный (и, возможно, более быстрый, чем 1-й) способ там, где нужно передать большие объемы данных.

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

Синхронизация

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

Значит, в ВМ для получения управления не следует использовать события, зависящие от котировок – OnTick и OnCalculate. Вместо них нужно использовать новшество MQL5 – событие OnTimer, исполняющееся с заданной периодичностью (например, 1 секунда). Тогда задержки в системе будут строго ограничены.

Также вместо OnTimer можно использовать прием зацикливания. Т.е. у ВМ в OnInit или в OnCalculate размещать бесконечный цикл. Каждая его итерация - это аналог таймерного тика.

Предостережение. Я проводил эксперименты и выяснил, что при использовании Комбинации 7 событие OnTimer в индикаторах почему-то не работает, хотя сами таймеры создаются успешно. Также нужно быть осторожным с бесконечными циклами в OnInit и OnCalculate: если хотя бы один ВМ-индикатор размещается на той же валютной паре, где и ГМ-эксперт, то на графике перестает двигаться цена, и эксперт перестает работать (у него перестает генерироваться событие OnTick). Разработчики терминала объяснили причины этого.

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

Программа Выполнение Примечание
Скрипт В собственном потоке, сколько скриптов - столько потоков выполнения для них Зацикленный скрипт не может нарушить работу других программ
Эксперт В собственном потоке, сколько экспертов - столько потоков выполнения для них Зацикленный эксперт не может нарушить работу других программ
Индикатор Один поток выполнения для всех индикаторов на одном символе. Сколько символов с индикаторами - столько потоков выполнения для них Бесконечный цикл в одном индикаторе остановит работу всех остальных индикаторов на этом символе


Создание тестового эксперта

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

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

При небольших размерах последовательности такая стратегия будет работать в MetaTrader 5 очень быстро - в пределах секунды. Но вот если взять большую длину – например, все бары таймфрейма M1 за последние сутки (что составит 1440 баров), - то при глубине поиска вглубь истории на один год (примерно 375000 баров) потребуется ощутимое количество времени. А меж тем, этот поиск можно легко распараллелить: достаточно поделить историю на равные участки по количеству доступных ядер процессора и поручить каждому ядру поиск на соответствующем участке.

Параметры параллельной системы у нас будут следующими:

  • ГМ – это эксперт, реализующий паттерновую стратегию торговли;
  • параллельные вычисления производятся в ВМ-индикаторах, автоматически создаваемых из эксперта (т.е. реализуется Комбинация 7);
  • вычисляющий код в ВМ-индикаторах размещен внутри бесконечного цикла в OnInit;
  • обмен данными между ГМ-экспертом и ВМ-индикаторами – через глобальные переменные терминала.

Для удобства разработки и последующего использования мы создадим эксперт так, чтобы в зависимости от настроек он мог работать и как параллельный (с вычислениями в индикаторах), и как обычный (т.е. без использования индикаторов). Код получившегося эксперта e-MultiThread:

input int Threads     = 1; // Сколько ядер использовать
input int MagicNumber = 0;

// Параметры стратегии

input int PatternLen  = 1440;   // Длина анализируемой последовательности (паттерна)
input int PrognozeLen = 60;     // На сколько баров в будущее строить прогноз
input int HistoryLen  = 375000; // Глубина истории для поиска похожей последовательности

input double Lots=0.1;
//+------------------------------------------------------------------+
class IndData 
  {
public:
   int               ts,te;
   datetime          start_time;
   double            prognoze,rating;
  };

IndData Calc[];
double CurPattern[];
double Prognoze;
int  HistPatternBarStart;
int  ExistsPrognozeLen;
uint TicksStart,TicksEnd;
//+------------------------------------------------------------------+
#include <ThreadCalc.mqh>
#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
int OnInit() 
  {

   double rates[];

   //=== Убедимся, что есть необходимое количество истории

   int HistNeed=HistoryLen+Threads+PatternLen+
                PatternLen+PrognozeLen-1;
   if(TerminalInfoInteger(TERMINAL_MAXBARS)<HistNeed) 
     {
      Print("Измените настройку терминала \"Макс. баров в окне\" до значения не менее ",
            HistNeed," и перезапустите терминал");
      return(1);
     }
   while(Bars(_Symbol,_Period)<HistNeed) 
     {
      Print("Недостаточная длина истории (",Bars(_Symbol,_Period),
            ") в терминале, подкачиваем...");
      CopyClose(_Symbol,_Period,0,HistNeed,rates);
     }
   Print("Длина истории в терминале: ",Bars(_Symbol,_Period));

   //=== Для мультиядерного режима создадим вычислительные индикаторы

   if(Threads>1) 
     {
      GlobalVarPrefix="MultiThread_"+IntegerToString(MagicNumber)+"_";
      GlobalVariablesDeleteAll(GlobalVarPrefix);

      ArrayResize(Calc,Threads);

      // Длина истории на каждое ядро
      int HistPartLen=MathCeil(HistoryLen/Threads);
      // С учетом полного захвата граничных последовательностей
      int HistPartLenPlus=HistPartLen+
                          PatternLen+PrognozeLen-1;

      string s;
      int snum=0;
      // Создадим все вычислительные индикаторы
      for(int t=0; t<Threads; t++) 
        {
         // Каждому индикатору - своя валютная пара,
         // при этом она не должна быть такой как у эксперта
         do
            s=SymbolName(snum++,false);
         while(s==_Symbol);

         int handle=iCustom(s,_Period,"i-Thread",
                            GlobalVarPrefix,t,_Symbol,PatternLen,
                            PatternLen+t*HistPartLen,HistPartLenPlus);

         if(handle==INVALID_HANDLE) return(1);
         Print("Создан индикатор, пара ",s,", handle ",handle);
        }
     }

   return(0);
  }
//+------------------------------------------------------------------+
void OnTick() 
  {
   TicksStart=GetTickCount();

   // Заполним последовательность из последних баров
   while(CopyClose(_Symbol,_Period,0,PatternLen,CurPattern)
         <PatternLen) Sleep(1000);

   // Если есть открытая позиция, замерим ее "возраст"
   // и скорректируем дальность прогноза на оставшееся 
   // плановое время жизни сделки
   CalcPrognozeLen();

   // Найдем в истории наиболее похожую посл-ть
   // и прогноз движения цены на ее основе
   FindHistoryPrognoze();

   // Осуществим необходимые торговые действия
   Trade();

   TicksEnd=GetTickCount();
   // Отладочная информация в лог
   PrintReport();
  }
//+------------------------------------------------------------------+
void FindHistoryPrognoze() 
  {
   Prognoze=0;
   double MaxRating;

   if(Threads>1) 
     {

      //--------------------------------------
      // ИСПОЛЬЗУЕМ ВЫЧИСЛИТЕЛЬНЫЕ ИНДИКАТОРЫ
      //--------------------------------------

      // Пройдем по всем вычислительным индикаторам 
      for(int t=0; t<Threads; t++) 
        {

         // Отправим параметры вычислительного задания
         SetParam(t,"PrognozeLen",ExistsPrognozeLen);

         // Сигнал "Начать вычисление"
         SetParam(t,"Query");
        }

      for(int t=0; t<Threads; t++) 
        {

         // Дождемся результата

         while(!ParamExists(t,"Answer"))
            Sleep(100);
         DelParam(t,"Answer");

         // Получим результат

         double progn        = GetParam(t, "Prognoze");
         double rating       = GetParam(t, "Rating");
         datetime time[];
         int start=GetParam(t,"PatternStart");
         CopyTime(_Symbol,_Period,start,1,time);
         Calc [t].prognoze   = progn;
         Calc [t].rating     = rating;
         Calc [t].start_time = time[0];
         Calc [t].ts         = GetParam(t, "TS");
         Calc [t].te         = GetParam(t, "TE");

         // Выбираем лучший результат

         if((t==0) || (rating>MaxRating)) 
           {
            MaxRating = rating;
            Prognoze  = progn;
           }
        }

     }
   else 
     {

      //----------------------------
      // ИНДИКАТОРЫ НЕ ИСПОЛЬЗУЮТСЯ
      //----------------------------

      // Посчитаем всё в советнике, в один поток
      FindPrognoze(_Symbol,CurPattern,0,HistoryLen,ExistsPrognozeLen,
                   Prognoze,MaxRating,HistPatternBarStart);

     }
  }
//+------------------------------------------------------------------+
void CalcPrognozeLen() 
  {
   ExistsPrognozeLen=PrognozeLen;

   // Если есть открытая позиция, определим, 
   // сколько баров прошло с ее открытия

   if(PositionSelect(_Symbol)) 
     {
      datetime postime=PositionGetInteger(POSITION_TIME);
      datetime curtime,time[];
      CopyTime(_Symbol,_Period,0,1,time);
      curtime=time[0];
      CopyTime(_Symbol,_Period,curtime,postime,time);
      int poslen=ArraySize(time);
      if(poslen<PrognozeLen)
         ExistsPrognozeLen=PrognozeLen-poslen;
      else
         ExistsPrognozeLen=0;
     }
  }
//+------------------------------------------------------------------+
void Trade() 
  {

   // Закроем открытую позицию, если она против прогноза

   if(PositionSelect(_Symbol)) 
     {
      long type=PositionGetInteger(POSITION_TYPE);
      bool close=false;
      if((type == POSITION_TYPE_BUY)  && (Prognoze <= 0)) close = true;
      if((type == POSITION_TYPE_SELL) && (Prognoze >= 0)) close = true;
      if(close) 
        {
         CTrade trade;
         trade.PositionClose(_Symbol);
        }
     }

   // Если позиций нет, то откроем по прогнозу

   if((Prognoze!=0) && (!PositionSelect(_Symbol))) 
     {
      CTrade trade;
      if(Prognoze > 0) trade.Buy (Lots);
      if(Prognoze < 0) trade.Sell(Lots);
     }
  }
//+------------------------------------------------------------------+
void PrintReport() 
  {
   Print("------------");
   Print("Эксперт: начало работы ",TicksStart,
         ", конец работы ",TicksEnd,
         ", длительность (мс) ",TicksEnd-TicksStart);
   Print("Эксперт: прогноз на ",ExistsPrognozeLen," баров");

   if(Threads>1) 
     {
      for(int t=0; t<Threads; t++) 
        {
         Print("Индикатор ",t+1,
               ": прогноз ", Calc[t].prognoze,
               ", рейтинг ", Calc[t].rating,
               ", последовательность от ",TimeToString(Calc[t].start_time)," в прошлое");
         Print("Индикатор ",t+1,
               ": начало работы ",  Calc[t].ts,
               ", конец работы ",   Calc[t].te,
               ", длительность (мс) ",Calc[t].te-Calc[t].ts);
        }
     }
   else 
     {
      Print("Индикаторы не использовались");
      datetime time[];
      CopyTime(_Symbol,_Period,HistPatternBarStart,1,time);
      Print("Эксперт: последовательность от ",TimeToString(time[0])," в прошлое");
     }

   Print("Эксперт: прогноз ",Prognoze);
   Print("------------");
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason) 
  {
   // Отправим индикаторам команды завершения работы
   if(Threads>1)
      for(int t=0; t<Threads; t++)
         SetParam(t,"End");
  }
//+------------------------------------------------------------------+


Код автоматически создаваемого экспертом вычислительного индикатора i-Thread:

#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1

//--- input parameters

input string VarPrefix;  // Префикс для глобальных переменных (аналог MagicNumber)
input int    ThreadNum;  // Номер ядра (чтобы индикаторы на разных ядрах могли 
                         // отличить свои задания от "соседних" ядер)

input string DataSymbol; // На какой паре работает ГМ-эксперт
input int    PatternLen; // Длина последовательности для анализа
input int    BarStart;   // С какого бара истории начинать поиск похожей посл-ти
input int    BarCount;   // Сколько баров истории вести поиск

//--- indicator buffers

double Buffer[];

//---

double CurPattern[];

//+------------------------------------------------------------------+
#include <ThreadCalc.mqh>
//+------------------------------------------------------------------+
void OnInit() 
  {
   SetIndexBuffer(0,Buffer,INDICATOR_DATA);

   GlobalVarPrefix=VarPrefix;

   // Вечный цикл - чтобы индикатор всегда был "на связи", постоянно "слушая", 
   // нет ли новых команд для него от эксперта
   while(true) 
     {

      // Завершим работу индикатора, если есть команда завершения
      if(ParamExists(ThreadNum,"End"))
         break;

      // Ждем сигнала начала вычислений
      if(!ParamExists(ThreadNum,"Query")) 
        {
         Sleep(100);
         continue;
        }
      DelParam(ThreadNum,"Query");

      uint TicksStart=GetTickCount();

      // Получим параметры задания
      int PrognozeLen=GetParam(ThreadNum,"PrognozeLen");

      // Заполним последовательность из последних баров
      while(CopyClose(DataSymbol,_Period,0,PatternLen,CurPattern)
            <PatternLen) Sleep(1000);

      // Проводим вычисления
      int HistPatternBarStart;
      double Prognoze,Rating;
      FindPrognoze(DataSymbol,CurPattern,BarStart,BarCount,PrognozeLen,
                   Prognoze,Rating,HistPatternBarStart);

      // Отправляем результаты вычислений
      SetParam(ThreadNum,"Prognoze",Prognoze);
      SetParam(ThreadNum,"Rating",Rating);
      SetParam(ThreadNum,"PatternStart",HistPatternBarStart);
      SetParam(ThreadNum,"TS",TicksStart);
      SetParam(ThreadNum,"TE",GetTickCount());
      // Сигнал "всё готово"
      SetParam(ThreadNum,"Answer");
     }
  }
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   // Обработчик этого события нужен по стандарту. 
   // Просто вставим в него заглушку.
   
   return(0);
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason) 
  {
   SetParam(ThreadNum,"End");
  }
//+------------------------------------------------------------------+


И эксперт, и индикатор используют общую для них подключаемую библиотеку ThreadCalc.mqh. Вот ее код:

string GlobalVarPrefix;
//+------------------------------------------------------------------+
// В заданном диапазоне истории находит ценовую последовательность,
// максимально похожую на заданную.
// Возвращает оценку похожести и направление дальнейшего изменения
// цен в истории.
//+------------------------------------------------------------------+
void FindPrognoze(
                  string DataSymbol,    // На какой валютной паре работать

                  double  &CurPattern[],// эталонная ценовая последовательность

                  int BarStart,         // с какого бара начинать поиск

                  int BarCount,         // глубина поиска - сколько баров в прошлое

                  int PrognozeLen,      // с какого бара брать прогнозную цену - расстояние
                                        // в будущее от найденной последовательности

                  // ВОЗВРАЩАЕМЫЙ РЕЗУЛЬТАТ

                  double  &Prognoze,        // направление изменения прогнозной цены:
                                            // минус, 0 или плюс

                  double  &Rating,          // оценка похожести найденной последовательности

                  int  &HistPatternBarStart // начальный бар найденной последовательности
                  ) 
  {

   int PatternLen=ArraySize(CurPattern);

   Prognoze=0;
   if(PrognozeLen<=0) return;

   double rates[];
   while(CopyClose(DataSymbol,_Period,BarStart,BarCount,rates)
         <BarCount) Sleep(1000);

   double rmin=-1;
   // Сдвигаясь по одному бару, переберем все ценовые последовательности в истории
   for(int bar=BarCount-PatternLen-PrognozeLen; bar>=0; bar--) 
     {
      // Корректировка для устранения разницы уровней цен в последовательностях
      double dr=CurPattern[0]-rates[bar];

      // Рассчитаем степень непохожести последовательности - как сумму
      // квадратов отклонений цен от эталонных значений
      double r=0;
      for(int i=0; i<PatternLen; i++)
         r+=MathPow(MathAbs(rates[bar+i]+dr-CurPattern[i]),2);

      // Найдем последовательность с наименьшей непохожестью
      if((r<rmin) || (rmin<0)) 
        {
         rmin=r;
         HistPatternBarStart   = bar;
         int HistPatternBarEnd = bar + PatternLen-1;
         Prognoze=rates[HistPatternBarEnd+PrognozeLen]-rates[HistPatternBarEnd];
        }
     }
   // Переведем номер бара в индикаторную систему координат
   HistPatternBarStart=BarStart+BarCount-HistPatternBarStart-PatternLen;

   // Непохожесть переведем в рейтинг похожести
   Rating=-rmin;
  }
//====================================================================
// Набор процедур для упрощения работы с глобальными переменными.
// В качестве параметров содержат номера вычислительных потоков 
// и имена переменных, автоматически конвертируя их в уникальные
// глобальные имена.
//====================================================================
//+------------------------------------------------------------------+
string GlobalParamName(int ThreadNum,string ParamName) 
  {
   return GlobalVarPrefix+IntegerToString(ThreadNum)+"_"+ParamName;
  }
//+------------------------------------------------------------------+
bool ParamExists(int ThreadNum,string ParamName) 
  {
   return GlobalVariableCheck(GlobalParamName(ThreadNum,ParamName));
  }
//+------------------------------------------------------------------+
void SetParam(int ThreadNum,string ParamName,double ParamValue=0) 
  {
   string VarName=GlobalParamName(ThreadNum,ParamName);
   GlobalVariableTemp(VarName);
   GlobalVariableSet(VarName,ParamValue);
  }
//+------------------------------------------------------------------+
double GetParam(int ThreadNum,string ParamName) 
  {
   return GlobalVariableGet(GlobalParamName(ThreadNum,ParamName));
  }
//+------------------------------------------------------------------+
double DelParam(int ThreadNum,string ParamName) 
  {
   return GlobalVariableDel(GlobalParamName(ThreadNum,ParamName));
  }
//+------------------------------------------------------------------+

Наша торговая система, умеющая использовать при работе более одного ядра процессора, готова!

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


Измерение скорости работы эксперта

Обычный режим

Откроем минутный график EURUSD и добавим на него нашего эксперта, созданного в предыдущей главе. В настройках его укажем длину паттернов 1 сутки (1440 минутных баров) и глубину поиска в истории - 1 год (375000 баров).

Параметр "Threads" зададим равным 1. Это означает, что все вычисления эксперт будет производить в один поток (на одном ядре). При этом он не станет использовать вычислительные индикаторы, а всё вычислит сам. Т.е. по принципу работы это обычный эксперт. Лог его выполнения:


Параллельный режим

Теперь удалим этого эксперта и открытую им позицию. Добавим эксперта заново, но уже с параметром "Threads" равным 2. Теперь эксперт должен будет создать и использовать в своей работе 2 вычислительных индикатора, заняв 2 процессорных ядра. Лог его выполнения:


Сравнение скорости

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

  • в обычном режиме - 52 секунды;
  • в 2-ядерном режиме - 27 секунд.

Т.е. за счет распараллеливания на 2-ядерном процессоре нам удалось повысить скорость работы эксперта в 1.9 раза. Можно полагать, что при использовании процессора с большим количеством ядер скорость работы возрастет еще сильнее, пропорционально числу ядер.

Контроль правильности работы

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

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

Вот как выглядят паттерны в описанных в логах ситуациях. Такой была текущая рыночная ситуация (длина - 1440 минутных баров) на момент запуска эксперта в обычном режиме:

Эксперт нашел в истории такой похожий паттерн:


При запуске эксперта в параллельном режиме, этот же паттерн был найден "Индикатором 1". "Индикатор 2", как следует из лога, искал паттерны в другом полугодии истории, поэтому он нашел другой похожий паттерн:



А так выглядят глобальные переменные MetaTrader 5 во время работы эксперта в параллельном режиме:

Обмен данными между экспертом и индикатором через глобальные переменные осуществлялся успешно.


Заключение

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

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

Прикрепленные файлы |
e-flood.mq5 (0.58 KB)
i-flood.mq5 (0.7 KB)
threadcalc.mqh (4.23 KB)
i-thread.mq5 (3.08 KB)
e-multithread.mq5 (8.18 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (15)
Mykola Demko
Mykola Demko | 30 мар. 2011 в 00:59
papaklass:

 

А как Ваш метод применить на чемпионате? Правила: Один эксперт и один график. Позволят ли организаторы запускать шпионов на другие графики? 

Вы не верно трактуете правила.

III. Программы Экспертов (Expert Advisors) для MetaTrader 5

...

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

4. Мультивалютные эксперты могут использовать любые валютные пары из 12 доступных.

...

Это означает что эксперт будет присоединён к одному чарту, а конкретно к тому, символ и период которого укажет участник в профайле.

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

Вы ведь находитесь на терминале один и никому не мешаете.

Konstantin Gruzdev
Konstantin Gruzdev | 30 мар. 2011 в 01:37
papaklass:

 

А как Ваш метод применить на чемпионате? Правила: Один эксперт и один график. Позволят ли организаторы запускать шпионов на другие графики? 

Можно. Urain уже ответил. В дополнение: вместо Мультивалютный обработчик тиков OnTickMarketWatch используйте это Мультивалютный обработчик событий OnTick(string symbol) или что-то подобное. Проблем не будет.
Forester
Forester | 15 июн. 2017 в 20:11

Одиночный проход экспертом начинается с 0-го бара

      FindPrognoze(_Symbol,CurPattern,0,HistoryLen,ExistsPrognozeLen,
                   Prognoze,MaxRating,HistPatternBarStart);

а индикаторам задания раздаются не с 0 а с  PatternLen, т.е. сутки назад на минутном ТФ

         int handle=iCustom(s,_Period,path+"i-Thread",
                            GlobalVarPrefix,t,_Symbol,PatternLen,
                            PatternLen+t*HistPartLen,HistPartLenPlus);

В итоге решения получаются разными.

Konstantin Efremov
Konstantin Efremov | 15 февр. 2020 в 20:25

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

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

Только что сделал, всё работает на ура.

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

Построение каналов - взгляд изнутри и снаружи Построение каналов - взгляд изнутри и снаружи
Наверное, не будет преувеличением сказать, что после скользящих средних каналы - самый популярный инструмент для анализа рыночной ситуации и принятия торговых решений. Не углубляясь во множество существующих стратегий использования каналов и их составных элементов, мы здесь рассмотрим математические основы и практическую реализацию индикатора, строящего канал, заданный тремя экстремумами на экране терминала.
Димитар Манов:"На Чемпионате я боюсь только исключительных обстоятельств"(ATC 2010) Димитар Манов:"На Чемпионате я боюсь только исключительных обстоятельств"(ATC 2010)
В недавнем отчете Бориса Одинцова одним из самых стабильных советников Чемпионата был назван эксперт болгарского Участника Manov. Мы решили поговорить с автором этой разработки и выяснить, в чем заключается секрет ее успеха. В интервью Димитар Манов рассказывает о том, какую ситуацию его эксперт не переживет, почему он не использует индикаторы и рассчитывает ли на победу в соревновании.
Томаш Таузовски:"Мне остается лишь молиться о появлении убыточных позиций" (ATC 2010) Томаш Таузовски:"Мне остается лишь молиться о появлении убыточных позиций" (ATC 2010)
Томаш Таузовски (ttauzo) - ветеран первой десятки Чемпионата Automated Trading Championship 2010. Его эксперт уже седьмую неделю находится между пятым и седьмым местом. И это неудивительно: по мнению текущего лидера Чемпионата Бориса Одинцова ttauzo - один из самых стабильных экспертов, участвующих в соревновании.
Разработка и реализация новых виджетов на основе класса CChartObject Разработка и реализация новых виджетов на основе класса CChartObject
После написания статьи про полуавтоматический советник с графическим интерфейсом пользователя у меня возникла необходимость расширения интерфейса новым функционалом для более сложных индикаторов и экспертов. Ознакомившись с классами Стандартной библиотеки, я сделал новые виджеты. В этой статье описан процесс создания и использования новых элементов пользовательского интерфейса, созданных на базе класса CChartObjectEdit.