English 中文 Español Deutsch 日本語 Português
preview
Мультибот в MetaTrader: запуск множества роботов с одного графика

Мультибот в MetaTrader: запуск множества роботов с одного графика

MetaTrader 5Примеры | 10 апреля 2023, 15:26
1 891 0
Evgeniy Ilin
Evgeniy Ilin

Разделы


Введение

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

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


Постановка задачи и границы применимости

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

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

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

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

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


Отличия терминалов MetaTrader 4 и MetaTrader 5 в рамках использования мультибота

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

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

  • CopyClose - запрос цен закрытия баров
  • CopyOpen - зарос цен открытия баров
  • CopyHigh - запрос пиков баров
  • CopyLow - запрос впадин баров
  • CopyTime - запрос времени открытия баров
  • SymbolInfoTick - запрос последнего пришедшего тика для запрашиваемого инструмента
  • SymbolInfoInteger - запрос данных инструмента, которые могут быть описаны целыми числами и нумерованными списками
  • SymbolInfo******* - остальные нужные нам функции

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

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

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


Нюансы построения универсального шаблона

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

objects structure

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

  • Придется писать свои обработчики OnTick
  • Данные обработчики придется реализовать как производную от OnTimer
  • Тики не будут идеальными поскольку OnTimer работает с задержкой (величина задержки не важна, а важно ее наличие)
  • Для получения тиков потребуется функция SymbolInfoTick

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

  • Неточность в определении старта нового бара не существенно сказывается на торговлю
  • Чем больше период бара, тем меньше это влияние
  • Дискретность в виде баров обеспечивает повышение скорости тестирования на порядки
  • Обеспечивает одинаковое качество тестирования как при тестировании на реальных тиках, так и на искусственных

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

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

our realization

Хорошо, мы утвердили простейшую схему для реализации графиков. Теперь настало время подумать о входных параметрах такого шаблона, и что более важно, как учитывать динамическое количество графиков и советников под каждую ситуацию, но делать это в рамках допустимых возможностей языка MQL5? Единственный способ решения подобной задачи - использование строковых входных переменных. Строка позволяет хранить в себе очень большое количество данных. По сути, для описания всех необходимых параметров для такого шаблона потребуются динамические массивы во входных данных.  Конечно же такие вещи никто не будет реализовывать просто по той причине, что мало кто стал бы пользоваться подобными возможностями. Строка это и есть наш динамический массив, в который можно поместить все, что угодно. Им мы и будем пользоваться. Для своего простейшего шаблона я решил ввести 3 таких переменных:

  • Charts - наши графики (список)
  • Chart Lots - лоты для торговли (список)
  • Chart Timeframes - периоды графиков (список)

В общем-то можно объединить все эти данные в одну строку, но тогда ее структура будет сложной и потенциальному пользователю будет сложно разобраться в том, как корректно описывать данные, кроме того очень легко будет допустить ошибки в ее заполнении и можно получить много чего очень неприятного при использовании, не говоря уже о неимоверной сложности функции преобразования, которая будет вынимать эти данные из строк. Я видел подобные решения у продавцов и в целом все они сделали правильно. Все данные просто перечисляются через запятую. При старте советника эти данные вынимаются из строки с помощью спец функций и заполняются в соответствующие динамические массивы, которые потом используются в коде. По этому пути пойдем и мы. Можно добавить еще больше подобных строк с идентичными правилами перечисления. Я решил в качестве разделителя взять символ ":". Все дело в том, что если использовать запятую то непонятно как быть с double массивами такими как "Chart Lots"? Так я выбрал этот разделитель. Хочется в этой связи еще отметить, что можно добавлять и больше таких строковых переменных, и в целом можно построить еще более полноценный и универсальный шаблон, но моя задача здесь лишь показать, как это реализовать и дать вам первый вариант шаблона, который при желании будет очень легко и быстро модифицировать.

Пойдем дальше. Мало реализовать подобные массивы, дополнительно необходимо реализовать общие переменные, например такие:

  • Work Timeframe For Unsigned - период графика, там где он не указан
  • Fix Lot For Unsigned - лот, там где он не указан

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

Давайте теперь определим еще несколько важных переменных для минимальной реализации подобного шаблона:

  • Last Bars Count - количество последних баров для графика, которые мы храним для каждого графика
  • Deposit For Lot - депозит для использования указанного лота
  • First Magic - уникальный идентификатор сделок отдельного робота

С первой переменной я думаю ни у кого не возникнет вопросов. Вот вторая переменная я думаю будет не понятна всем. Таким образом я регулирую авто лот в своих советниках. Если я установлю ее значение в "0", то я сигнализирую алгоритму, что торговать нужно только фиксированным лотом, который я задаю как раз в соответствующей строке или в общей переменной, которые мы рассмотрели выше. В противном случае я задаю необходимый депозит для того, чтобы мог быть применен лот ровно тот, который указан в настройках. Несложно понять, что при более меньшем или большем депозите данный лот меняет свое значение по формуле:

  • Lot = Input Lot * ( Current Deposit / Deposit For Lot )

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

Отдельно стоит сказать про синхронизацию, а в частности про такой вопрос как настройку "Expert Magic Number", или уникального идентификатора советника. При торговле советниками или даже в смешанной форме, все уважающие себя программисты особое внимание уделяют именно данной переменной. Все дело в том, что при использовании множественных советников, очень важно следить за тем, чтобы у каждого такого советника был свой уникальный идентификатор, иначе при работе с ордерами, сделками или позициями вы получите полную кашу и ваши стратегии перестанут корректно работать, а в большинстве случаев совсем перестанут работать, надеюсь почему, объяснять не придется. При каждой установке советника на график, в таком случае необходимо настраивать эти идентификаторы и контролировать чтобы они не повторялись. Если допустить хотя бы одну ошибку, то можно очень серьезно заплатить за свою невнимательность. Кроме того, если вы случайно закрыли график с советником, то его надо настраивать заново и как вы понимаете вероятность допустить ошибку сильно повышается, кроме того, это очень неприятно по многим другим аспектам. Например, вы закрыли график и забыли, какой был там идентификатор, вам придется копаться в истории торговли искать его. Без него может некорректно заработать перезапущенный советник, и еще много-много чего неприятного может случиться.

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


Пишем универсальный шаблон

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

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input string SymbolsE="EURUSD:GBPUSD:USDCHF:USDJPY:NZDUSD:AUDUSD:USDCAD";//Charts
input string LotsE="0.01:0.01:0.01:0.01:0.01:0.01:0.01";//Chart Lots
input string TimeframesE="H1:H1:H1:H1:H1:H1:H1";//Chart Timeframes
input int LastBars=10;//Last Bars Count
input ENUM_TIMEFRAMES TimeframeE=PERIOD_M1;//Work Timeframe For Unsigned
input double RepurchaseLotE=0.01;//Fix Lot For Unsigned
input double DepositForRepurchaseLotE=0.00;//Deposit For Lot (if "0" then fix)
input int MagicE=156;//First Magic

Здесь вы можете наглядно посмотреть пример заполнения строковых переменных, которые и отражают наши динамические массивы, как и пример общих переменных. Кстати говоря, данный код будет иметь одинаковый вид, как в MQL4, так и в MQL5. Я старался делать все максимально одинаковым.

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

//+------------------------------------------------------------------+
//|Arrays                                                            |
//+------------------------------------------------------------------+
string S[];// Symbols array
double L[];//Lots array
ENUM_TIMEFRAMES T[];//Timeframes array

Заполняет эти массивы следующая функция:

//+------------------------------------------------------------------+
//| Fill arrays                                                      |
//+------------------------------------------------------------------+
void ConstructArrays()
   {
      int SCount=1;
      for (int i = 0; i < StringLen(SymbolsE); i++)//calculation of the number of tools
         {
         if (SymbolsE[i] == ':')
            {
            SCount++;
            }
         }
      ArrayResize(S,SCount);//set the size of the character array
      ArrayResize(CN,SCount);//set the size of the array to use bars for each character
      int Hc=0;//found instrument index
      for (int i = 0; i < StringLen(SymbolsE); i++)//building an array of tools
         {
         if (i == 0)//if we just started
            {
            int LastIndex=-1;
            for (int j = i; j < StringLen(SymbolsE); j++)
               {
               if (StringGetCharacter(SymbolsE,j) == ':')
                  {
                  LastIndex=j;
                  break;
                  }
               }
            if (LastIndex != -1)//if no separating colon was found
               {
               S[Hc]=StringSubstr(SymbolsE,i,LastIndex);
               Hc++;
               }
            else
               {
               S[Hc]=SymbolsE;
               Hc++;
               }
            }          
         if (SymbolsE[i] == ':')
            {
            int LastIndex=-1;
            for (int j = i+1; j < StringLen(SymbolsE); j++)
               {
               if (StringGetCharacter(SymbolsE,j) == ':')
                  {
                  LastIndex=j;
                  break;
                  }
               }
            if (LastIndex != -1)//if no separating colon was found
               {
               S[Hc]=StringSubstr(SymbolsE,i+1,LastIndex-(i+1));
               Hc++;
               }
            else
               {
               S[Hc]=StringSubstr(SymbolsE,i+1,StringLen(SymbolsE)-(i+1));
               Hc++;
               }               
            }
         }
      for (int i = 0; i < ArraySize(S); i++)//assignment of the requested number of bars
         {
         CN[i]=LastBars;
         }
      ConstructLots();
      ConstructTimeframe();         
   }

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

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

//+------------------------------------------------------------------+
//| Charts & experts pointers                                        |
//+------------------------------------------------------------------+
Chart *Charts[];
BotInstance *Bots[];

Начнем с класса графика:

//+------------------------------------------------------------------+
//| Chart class                                                      |
//+------------------------------------------------------------------+
class Chart
   {
   public:
   datetime TimeI[];
   double CloseI[];
   double OpenI[];
   double HighI[];
   double LowI[];
   string BasicSymbol;//the base instrument that was extracted from the substring
   double ChartPoint;//point size of the current chart
   double ChartAsk;//Ask
   double ChartBid;//Bid
   datetime tTimeI[];//auxiliary array to control the appearance of a new bar
   static int TCN;//tcn
   string CurrentSymbol;//symbol
   ENUM_TIMEFRAMES Timeframe;//timeframe
   int copied;//how much data is copied
   int lastcopied;//last amount of data copied
   datetime LastCloseTime;//last bar time
   MqlTick LastTick;//last tick fos this instrument
   
   Chart()
      {
      ArrayResize(tTimeI,2);
      }
   
   void ChartTick()//this chart tick
      {
      SymbolInfoTick(CurrentSymbol,LastTick);
      ArraySetAsSeries(tTimeI,false);
      copied=CopyTime(CurrentSymbol,Timeframe,0,2,tTimeI);
      ArraySetAsSeries(tTimeI,true);
      if ( copied == 2 && tTimeI[1] > LastCloseTime )
         {
         ArraySetAsSeries(CloseI,false);                        
         ArraySetAsSeries(OpenI,false);                           
         ArraySetAsSeries(HighI,false);                        
         ArraySetAsSeries(LowI,false);                              
         ArraySetAsSeries(TimeI,false);                                                            
         lastcopied=CopyClose(CurrentSymbol,Timeframe,0,Chart::TCN+2,CloseI);
         lastcopied=CopyOpen(CurrentSymbol,Timeframe,0,Chart::TCN+2,OpenI);   
         lastcopied=CopyHigh(CurrentSymbol,Timeframe,0,Chart::TCN+2,HighI);   
         lastcopied=CopyLow(CurrentSymbol,Timeframe,0,Chart::TCN+2,LowI);
         lastcopied=CopyTime(CurrentSymbol,Timeframe,0,Chart::TCN+2,TimeI);
         ArraySetAsSeries(CloseI,true);
         ArraySetAsSeries(OpenI,true);
         ArraySetAsSeries(HighI,true);                        
         ArraySetAsSeries(LowI,true);
         ArraySetAsSeries(TimeI,true);         
         LastCloseTime=tTimeI[1];
         }
      ChartBid=LastTick.bid;
      ChartAsk=LastTick.ask;
      ChartPoint=SymbolInfoDouble(CurrentSymbol,SYMBOL_POINT);
      }
   };
int Chart::TCN = 0;

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

Давайте идти дальше, теперь нам необходимо описать класс отдельного виртуального советника:

//+------------------------------------------------------------------+
//| Bot instance class                                               |
//+------------------------------------------------------------------+
class BotInstance//expert advisor object
   {
   public:
   CPositionInfo  m_position;// trade position object
   CTrade         m_trade;// trading object   
   ///-------------------this robot settings----------------------
   int MagicF;//Magic
   string CurrentSymbol;//Symbol
   double CurrentLot;//Start Lot
   int chartindex;//Chart Index
   ///------------------------------------------------------------   
      
   
   ///constructor
   BotInstance(int index,int chartindex0)//load all data from hat using index, + chart index
      {
      chartindex=chartindex0;
      MagicF=MagicE+index;
      CurrentSymbol=Charts[chartindex].CurrentSymbol;
      CurrentLot=L[index];
      m_trade.SetExpertMagicNumber(MagicF);
      }
   ///
   
   void InstanceTick()//bot tick
      {
      if ( bNewBar() ) Trade();
      }
      
   private:
   datetime Time0;
   bool bNewBar()//new bar
      {
      if ( Time0 < Charts[chartindex].TimeI[1] && Charts[chartindex].ChartPoint != 0.0 )
         {
         if (Time0 != 0)
            {
            Time0=Charts[chartindex].TimeI[1];
            return true;
            }
         else
            {
            Time0=Charts[chartindex].TimeI[1];
            return false;
            }
         }
      else return false;
      }
      
   //////************************************Main Logic********************************************************************
   void Trade()//main trade function
      {
      //Close[0]   -->   Charts[chartindex].CloseI[0] - example of access to data arrays of bars of the corresponding chart
      //Open[0]   -->   Charts[chartindex].OpenI[0] -----------------------------------------------------------------------
      //High[0]   -->   Charts[chartindex].HighI[0] -----------------------------------------------------------------------
      //Low[0]   -->   Charts[chartindex].LowI[0] -------------------------------------------------------------------------
      //Time[0]   -->   Charts[chartindex].TimeI[0] -----------------------------------------------------------------------      

      if ( true )
         {
            CloseBuyF();
            //CloseSellF();       
         }
      if ( true )
         {
            BuyF();
            //SellF(); 
         }

      }
      
   double OptimalLot()//optimal lot calculation
      {
      if (DepositForRepurchaseLotE != 0.0) return CurrentLot * (AccountInfoDouble(ACCOUNT_BALANCE)/DepositForRepurchaseLotE);
      else return CurrentLot;
      }
      
   //here you can add functionality or variables if the trading function turns out to be too complicated
   //////*******************************************************************************************************************
   
   ///trade functions
   int OrdersG()//the number of open positions / orders of this virtual robot
      {
      ulong ticket;
      bool ord;
      int OrdersG=0;
      for ( int i=0; i<PositionsTotal(); i++ )
         {
         ticket=PositionGetTicket(i);
         ord=PositionSelectByTicket(ticket);      
         if ( ord && PositionGetInteger(POSITION_MAGIC) == MagicF && PositionGetString(POSITION_SYMBOL) == CurrentSymbol )
            {
            OrdersG++;
            }
         }
      return OrdersG;
      }
   
   /////////********/////////********//////////***********/////////trade function code block
   void BuyF()//buy market
      {
      double DtA;
      double CorrectedLot;
   
      DtA=double(TimeCurrent())-GlobalVariableGet("TimeStart161_"+IntegerToString(MagicF));//unique bot marker last try datetime
      if ( (DtA > 0 || DtA < 0) )
         {
         CorrectedLot=OptimalLot(Charts[chartindex]);
         if ( CorrectedLot > 0.0 )
            {
            //try buy logic
            }            
         }
      }
      
   void SellF()//sell market
      {
      //Same logic
      }

   void CloseSellF()//close sell position
      {
      ulong ticket;
      bool ord;
      for ( int i=0; i<PositionsTotal(); i++ )
         {
         ticket=PositionGetTicket(i);
         ord=PositionSelectByTicket(ticket);      
         if ( ord && PositionGetInteger(POSITION_MAGIC) == MagicF && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL 
         && PositionGetString(POSITION_SYMBOL) == Charts[chartindex].CurrentSymbol )
            {
            //Close Sell logic
            }
         }    
      }
      
   void CloseBuyF()//close buy position
      {
      //same logic 
      }        
      
   bool bOurMagic(ulong ticket,int magiccount)//whether the magic of the current deal matches one of the possible magics of our robot
      {
      int MagicT[];
      ArrayResize(MagicT,magiccount);
      for ( int i=0; i<magiccount; i++ )
         {
         MagicT[i]=MagicE+i;
         }
      for ( int i=0; i<ArraySize(MagicT); i++ )
         {
         if ( HistoryDealGetInteger(ticket,DEAL_MAGIC) == MagicT[i] ) return true;
         }
      return false;
      }
   /////////********/////////********//////////***********/////////end trade function code block
   };

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

  • Trade() - основная торговая функция которая вызывается в обработчике баров для соответствующего графика
  • BuyF() - функция покупки по рынку
  • SellF() - функция продажи по рынку
  • CloseBuyF() - функция закрытия позиций на покупку по рынку
  • CloseSellF() - функция закрытия позиций на продажу по рынку

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

  • OrdersG() - просто подсчет позиций которые открыты на конкретном инструменте который сцеплен с графиком
  • OptimalLot() - подготовка лота перед отправлением в торговую функцию (выбор фиксированного лота или вычисление авто лота)
  • bOurMagic()  - проверка сделок из истории на соответствие списку допустимых (для фильтрации только собственной истории)

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

  • InstanceTick() - имитация тика на отдельном экземпляре советника
  • bNewBar() - предикат для проверки появления нового бара (используется внутри InstanceTick)

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

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

//+------------------------------------------------------------------+
//| Creation of graph objects                                        |
//+------------------------------------------------------------------+
void CreateCharts()
   {
   bool bAlready;
   int num=0;
   string TempSymbols[];
   string Symbols[];
   ConstructArrays();//array preparation
   int tempcnum=CN[0];
   Chart::TCN=tempcnum;//required number of stored bars for all instruments
   for (int j = 0; j < ArraySize(Charts); j++)//fill in all the names and set the dimensions of all time series, each graph
      {
      Charts[j] = new Chart();
      Charts[j].lastcopied=0;
      ArrayResize(Charts[j].CloseI,tempcnum+2);//assign size to character arrays
      ArrayResize(Charts[j].OpenI,tempcnum+2);//----------------------------------
      ArrayResize(Charts[j].HighI,tempcnum+2);//----------------------------------
      ArrayResize(Charts[j].LowI,tempcnum+2);//-----------------------------------
      ArrayResize(Charts[j].TimeI,tempcnum+2);//----------------------------------
      Charts[j].CurrentSymbol = S[j];//symbol
      Charts[j].Timeframe = T[j];//timeframe
      }
   ArrayResize(Bots,ArraySize(S));//assign a size to the array of bots      
   }

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

//+------------------------------------------------------------------+
//| create and hang all virtual robots on charts                     |
//+------------------------------------------------------------------+
void CreateInstances()
   {
   for (int i = 0; i < ArraySize(S); i++)
      {
      for (int j = 0; j < ArraySize(Charts); j++)
         {
         if ( Charts[j].CurrentSymbol == S[i] )
            {
            Bots[i] = new BotInstance(i,j);
            break;
            } 
         }
      }
   }

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

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

//+------------------------------------------------------------------+
//| All bcharts & all bots tick imitation                            |
//+------------------------------------------------------------------+
void AllChartsTick()
   {
   for (int i = 0; i < ArraySize(Charts); i++)
      {
      Charts[i].ChartTick();
      }
   }

void AllBotsTick()
   {
   for (int i = 0; i < ArraySize(S); i++)
      {
      if ( Charts[Bots[i].chartindex].lastcopied >= Chart::TCN+1 ) Bots[i].InstanceTick();
      }
   }

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

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


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

//+------------------------------------------------------------------+
//| Reserved elements                                                |
//+------------------------------------------------------------------+

   "template-UNSIGNED1",//UNSIGNED1
   "template-UNSIGNED2",//UNSIGNED2
   "template-UNSIGNED3",//UNSIGNED3

   //LabelCreate(0,OwnObjectNames[13],0,x+Border+2,y+17+Border+20*5+20*5+23,corner,"","Arial",11,clrWhite,0.0,ANCHOR_LEFT);//UNSIGNED1
   //LabelCreate(0,OwnObjectNames[14],0,x+Border+2,y+17+Border+20*5+20*5+23+20*1,corner,"","Arial",11,clrWhite,0.0,ANCHOR_LEFT);//UNSIGNED2
   //LabelCreate(0,OwnObjectNames[15],0,x+Border+2,y+17+Border+20*5+20*5+23+20*2,corner,"","Arial",11,clrWhite,0.0,ANCHOR_LEFT);//UNSIGNED3

   ////////////////////////////
   //TempText="UNSIGNED1 : ";
   //TempText+=DoubleToString(NormalizeDouble(0.0),3);   
   //ObjectSetString(0,OwnObjectNames[13],OBJPROP_TEXT,TempText);
   //TempText="UNSIGNED2 : ";
   //TempText+=DoubleToString(NormalizeDouble(0.0),3);   
   //ObjectSetString(0,OwnObjectNames[14],OBJPROP_TEXT,TempText);
   //TempText="UNSIGNED3 : ";
   //TempText+=DoubleToString(NormalizeDouble(0.0),3);
   //ObjectSetString(0,OwnObjectNames[15],OBJPROP_TEXT,TempText);
   ///////////////////////////
 

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

checking using MetaTrader 5 tester visualization


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


Заключение

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


Прикрепленные файлы |
MultiTemplate.mq4 (95.13 KB)
MultiTemplate.mq5 (92.6 KB)
Эксперименты с нейросетями (Часть 5): Нормализация входных параметров для передачи в нейросеть Эксперименты с нейросетями (Часть 5): Нормализация входных параметров для передачи в нейросеть
Нейросети наше все. Проверяем на практике, так ли это. MetaTrader 5 как самодостаточное средство для использования нейросетей в трейдинге. Простое объяснение.
Нейросети — это просто (Часть 37): Разреженное внимание (Sparse Attention) Нейросети — это просто (Часть 37): Разреженное внимание (Sparse Attention)
В предыдущей статье мы познакомились с реляционными моделями, в архитектуре которых используются механизмы внимания. Одной из особенностей указанных моделей является повышенное использование вычислительных ресурсов. В данной статье будет предложен один их механизмов уменьшения количества вычислительных операций внутри блока Self-Attention. Что позволит увеличить производительность модели в целом.
Понимание и эффективное использование OpenCL API путем воссоздания встроенной поддержки в виде DLL в Linux (Часть 1): Мотивация и проверка Понимание и эффективное использование OpenCL API путем воссоздания встроенной поддержки в виде DLL в Linux (Часть 1): Мотивация и проверка
Встроенная поддержка OpenCL в MetaTrader 5 по-прежнему имеет серьезную проблему, особенно связанную с ошибкой выбора устройства 5114, возникающей из-за невозможности создать контекст OpenCL с использованием CL_USE_GPU_ONLY или CL_USE_GPU_DOUBLE_ONLY, хотя GPU определяется правильно. Программа отлично работает с прямым использованием порядкового номера устройства GPU, который можно найти на вкладке "Журнал", однако пользователям не следует жестко кодировать устройство. Мы решим эту проблему, воссоздав поддержку OpenCL в виде DLL с помощью C++ в Linux. По пути мы познакомимся с OpenCL, от концепции до лучших практик в использовании его API, ровно настолько, чтобы мы могли широко использовать его позже, когда будем иметь дело с реализацией DLL на C++ и использовать его с MQL5.
Пример ансамбля ONNX-моделей в MQL5 Пример ансамбля ONNX-моделей в MQL5
ONNX (Open Neural Network eXchange) — открытый стандарт представления нейронных сетей. В данной статье мы покажем возможность одновременного использования двух ONNX-моделей в одном эксперте.