English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Использование ORDER_MAGIC для торговли разными экспертами на одном инструменте

Использование ORDER_MAGIC для торговли разными экспертами на одном инструменте

MetaTrader 5Примеры | 9 июля 2010, 19:17
8 250 31
Mykola Demko
Mykola Demko

Введение

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

Но прежде чем перейти к непосредственной теме статьи, разберём подробнее, что представляет собой магик. Что может быть магического в номере, который определяет, какой эксперт его выставил?  Чудеса начинаются с возможностей, которые закладывают разработчики в тип ulong, которым объявляется магик.

Тип ulong самый длиииииииииннннннннннный

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

Тип

Размер в байтах

Минимальное значение

Максимальное значение

Аналог в языке С++

long

8

-9 223 372 036 854 775 808

9 223 372 036 854 775 807

__int64

ulong

8

0

18 446 744 073 709 551 615

unsigned __int64

Таблица 1. Свойства типов данных long и ulong

но тип ulong переплюнул и его за счёт объединения положительной и отрицательной мантиссы.

Итак, длина задана огромная, но как она использовалась раньше?

По опыту работы на mql4, я частенько обращал внимание на бессмысленность кодирования магика многими разработчиками. Нет, магик использовался осмысленно, но вот его кодировка вызывала, по меньшей мере, улыбку. Что можно сказать об индивидуальности магика 12345, такой магик использует практически половина пишущей братии. Вторая половина пользуется магиками 55555, 33333 и 77777, вот, пожалуй, исчерпывающий набор. Хочу обратить внимание читателя, что на его компьютере вряд ли установлено более 1000 советников, так что заветное число 1000 будет достаточно для кодирования индивидуального имени всех ваших советников.

1000 - это всего 3 полных разряда, что же делать с остальными 15-ти полными разрядами, которые имеются в типе ulong? Ответ прост: кодировать.

Что же говорит всезнайка Вики о слове код:

Код — правило (алгоритм) сопоставления каждому конкретному сообщению строго определённой комбинации символов (знаков) (или сигналов).

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

Итак, подведём итог: какие возможности мы закладываем в систему:

  1. Возможность двум и более советникам работать на одном инструменте и не конфликтовать.
  2. Возможность двум и более советникам работать на разных инструментах и дополнять друг друга.
  3. Возможность идентификации ордера по инструменту, с которого работает советник.

Итак, задача поставлена, приступим к реализации.

Простой советник

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

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

//+------------------------------------------------------------------+
//| Класс обеспечивает вспомогательные торговые вычисления           |
//+------------------------------------------------------------------+
class CProvision
  {
protected:
   MqlTradeRequest   trades;                  // указатель на структуру запроса по OrderSend
public:
   int               TYPE(const double &v[]); // определяет тип, в зависимости от показания машки
   double            pricetype(int type);     // вычисляет уровень открытия в зависимости от типа 
   double            SLtype(int type);        // вычисляет уровень стоп-лосса в зависимости от типа
   double            TPtype(int type);        // вычисляет уровень тейк-профита в зависимости от типа
   long              spread();                // возвращает спред текущего инструмента
   int               SendOrder(ENUM_ORDER_TYPE type,double volume);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CProvision::SendOrder(ENUM_ORDER_TYPE type,double volume)
  {
   trades.action          =TRADE_ACTION_DEAL;      // Тип выполняемого действия
   trades.magic           =magic;                  // Штамп эксперта (идентификатор magic number)
   trades.symbol          =_Symbol;                // Имя торгового инструмента
   trades.volume          =volume;                 // Запрашиваемый объем сделки в лотах
   trades.price           =pricetype((int)type);   // Цена       
   trades.sl              =SLtype((int)type);      // Уровень Stop Loss ордера
   trades.tp              =TPtype((int)type);      // Уровень Take Profit ордера         
   trades.deviation=(int)spread();                 // Максимально приемлемое отклонение от запрашиваемой цены
   trades.type=type;                               // Тип ордера
   trades.type_filling=ORDER_FILLING_FOK;
   if(OrderSend(trades,res)){return(res.retcode);}
   return(-1);
  }
//+------------------------------------------------------------------+
//| Определяет тип, в зависимости от показания машки                 |
//+------------------------------------------------------------------+
int CProvision::TYPE(const double &v[])
  {
   double t=v[0]-v[1];
   if(t==0.0)t=1.0;
   return((int)(0.5*t/fabs(t)+0.5));
  }
//+------------------------------------------------------------------+
//| Вычисляет уровень открытия в зависимости от типа                 |
//+------------------------------------------------------------------+
double CProvision::pricetype(int type)
  {
   if(SymbolInfoTick(_Symbol,tick))
     {
      if(type==0)return(tick.ask);
      if(type==1)return(tick.bid);
     }
   return(-1);
  }
//+------------------------------------------------------------------+
//| Вычисляет уровень стоплосса в зависимости от типа                |
//+------------------------------------------------------------------+
double CProvision::SLtype(int type)
  {
   if(SymbolInfoTick(_Symbol,tick))
     {
      if(type==0)return(tick.bid-SL*SymbolInfoDouble(Symbol(),SYMBOL_POINT));
      if(type==1)return(tick.ask+SL*SymbolInfoDouble(Symbol(),SYMBOL_POINT));
     }
   return(0);
  }
//+------------------------------------------------------------------+
//| Вычисляет уровень тейкпрофита в зависимости от типа              |
//+------------------------------------------------------------------+
double CProvision::TPtype(int type)
  {
   if(SymbolInfoTick(_Symbol,tick))
     {
      if(type==0)return(tick.bid+TP*SymbolInfoDouble(Symbol(),SYMBOL_POINT));
      if(type==1)return(tick.ask-TP*SymbolInfoDouble(Symbol(),SYMBOL_POINT));
     }
   return(0);
  }
//+------------------------------------------------------------------+
//| Возвращает спред                                                 |
//+------------------------------------------------------------------+
long CProvision::spread() 
  {
   return(SymbolInfoInteger(_Symbol,SYMBOL_SPREAD));
  }

Имея подобный класс, мы без особых проблем напишем код простого советника:

//+------------------------------------------------------------------+
//| Код советника                                                    |
//+------------------------------------------------------------------+

//--- input parameters
input ulong              magic       =1;           // магик
input int                SL          =300;         // стоплосс
input int                TP          =1000;        // тейкпрофит
input int                MA_Period   =25;          // период MA
input double             lot         =0.1;         // объём позиции
input int                MA_shift    =0;           // сдвиг индикатора
input ENUM_MA_METHOD     MA_smooth   =MODE_SMA;    // тип сглаживания
input ENUM_APPLIED_PRICE price       =PRICE_OPEN// тип цены 

//--- будем хранить хендл индикатора
int 
MA_handle,              // хендл индикатора
type_MA,                // тип указывающий направление машки
rezult;                 // переменная принимает значение кода результата операции по OrderSend
double v[2];            // буфер для получения значений машки

MqlTradeResult  res;    // указатель на структуру ответа по OrderSend
MqlTick         tick;   // указатель на структуру последних рыночных данных
CProvision prov;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- создадим хендл индикатора
   MA_handle=iMA(Symbol(),0,MA_Period,MA_shift,MA_smooth,price);
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(CopyBuffer(MA_handle,0,0,2,v)<=0)
     {Print("№",magic,"Ошибка копирования");return;}
   type_MA=prov.TYPE(v);    // определим тип, в зависимости от показания машки

   if(PositionSelect(_Symbol)) // если есть открытая позиция 
     {
      if(PositionGetInteger(POSITION_TYPE)!=type_MA)// проверяем не пора ли закрывать
        {
         Print("№",magic,"Позиция по магику имеет объём ",PositionGetDouble(POSITION_VOLUME),
               " переворачиваем позицию типа ",PositionGetInteger(POSITION_TYPE)," на ",type_MA);
         rezult=prov.SendOrder((ENUM_ORDER_TYPE)type_MA,PositionGetDouble(POSITION_VOLUME)+lot);
         // перевернуть позицию
         if(rezult!=-1)Print("№",magic," Код результата операции",rezult," volume ",res.volume);
         else{Print("№",magic,"Ошибка",GetLastError()); return;}
        }
     }
   else // если нет открытой позиции, открываем
     { 
      Print("№",magic,"Позиция по магику имеет объём ",PositionGetDouble(POSITION_VOLUME),
            " открываем позицию типа ",type_MA);
      rezult=prov.SendOrder((ENUM_ORDER_TYPE)type_MA,lot);
      // открыть позицию 
      if(rezult!=-1)Print("№",magic," Код результата операции",rezult," volume ",res.volume);
      else{Print("№",magic,"Ошибка",GetLastError()); return;}
     }
  }

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

Рисунок 1. Работа одного советника на одном инструменте

Рисунок 1. Работа одного советника на одном инструменте

Теперь попробуем запустить этот советник, но на разных таймфреймах одного инструмента (для опытов мы выбрали первый попавшийся инструмент, первым попался EURUSD :o)

Рисунок 2. Конфликт двух советников на одном инструменте на разных таймфреймах

Рисунок 2. Конфликт двух советников на одном инструменте на разных таймфреймах

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


Позиция или виртуальная позиция?

Поскольку в MetaTrader 5 разработчики вместо учёта ордеров перешли на учёт позиций, то имеет смысл более детально рассмотреть функции, связанные с учётом позиций.

// Возвращает количество открытых позиций.
int     PositionsTotal();

// Возвращает символ открытой позиции по номеру в списке позиций.
string  PositionGetSymbol(int  index);

// Выбирает открытую позицию для дальнейшей работы с ней.
bool    PositionSelect(string  symbol, uint timeout=0);

// Функция возвращает запрошенное свойство открытой позиции.
double  PositionGetDouble(ENUM_POSITION_PROPERTY  property_id);

// Функция возвращает запрошенное свойство открытой позиции.
long    PositionGetInteger(ENUM_POSITION_PROPERTY  property_id);

// Функция возвращает запрошенное свойство открытой позиции.
string  PositionGetString(ENUM_POSITION_PROPERTY  property_id);

Идентификаторы перечислений для функций получения запроса соответствующих свойств позиций PositionGetDouble, PositionGetInteger и PositionGetString приведены в таблицах 2-4.

Идентификатор

Описание

Тип

POSITION_VOLUME

Объем позиции

double

POSITION_PRICE_OPEN

Цена позиции

double

POSITION_SL

Уровень Stop Loss для открытой позиции

double

POSITION_TP

Уровень Take Profit для открытой позиции

double

POSITION_PRICE_CURRENT

Текущая цена по символу

double

POSITION_COMMISSION

Комиссия

double

POSITION_SWAP

Накопленный своп

double

POSITION_PROFIT

Текущая прибыль

double

Таблица 2. Значения перечисления ENUM_POSITION_PROPERTY_DOUBLE

Идентификатор

Описание

Тип

POSITION_TIME

Время открытия позиции

datetime

POSITION_TYPE

Тип позиции

ENUM_POSITION_TYPE

POSITION_MAGIC

Magic number для позиции (смотри ORDER_MAGIC)

long

POSITION_IDENTIFIER

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

long

Таблица 3. Значения перечисления ENUM_POSITION_PROPERTY_INTEGER

Идентификатор

Описание

Тип

POSITION_SYMBOL

Символ, по которому открыта позиция

string

POSITION_COMMENT

Комментарий к позиции

string

Таблица 4. Значения перечисления ENUM_POSITION_PROPERTY_STRING

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

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

Раз уж мы имеем возможность работать с ООП, то объявим еще и собственную структуру (заодно и попрактикуемся писать объектно).

//+------------------------------------------------------------------+
//| Структура класса CPositionVirtualMagic                           |
//+------------------------------------------------------------------+
struct SPositionVirtualMagic
  {
   double            volume; // объём виртуальной позиции
   ENUM_POSITION_TYPE type;   //  тип виртуальной позиции
  };
//+------------------------------------------------------------------+
//| Класс рассчитывает виртуальную позицию советника по магику       |
//+------------------------------------------------------------------+
class CPositionVirtualMagic
  {
protected:
   SPositionVirtualMagic pvm;
public:
   // Возвращает объём виртуальной позиции советника
   double             cVOLUME(){return(pvm.volume);}

   // Возвращает  тип  виртуальной позиции советника
   ENUM_POSITION_TYPE   cTYPE(){return(pvm.type);}

   // Метод расчёта виртуальной позиции, возвращает наличие или отсутствие виртуальной позиции
   bool              PositionVirtualMagic(ulong Magic,
                                          string symbol,
                                          datetime CurrentTime);
private:
    // Заполняет массив тикетов
   void              prHistory_Deals(ulong &buf[],int HTD);
  };
//+------------------------------------------------------------------+
//| Метод расчёта виртуальной позиции,                               |
//| возвращает true при наличии виртуальной позиции                  |
//+------------------------------------------------------------------+
bool  CPositionVirtualMagic::PositionVirtualMagic(ulong Magic,
                                                  string symbol,
                                                  datetime CurrentTime)
  {
   int DIGITS=(int)-log10(SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP));
   if(DIGITS<0)DIGITS=0;
   ulong Dticket=0;
   int History_Total_Deals=-1;
   double volume=0,volume_BUY=0,volume_SELL=0;
   ulong DTicketbuf[];

   do
     {
      if(HistorySelect(0,TimeCurrent()))
        {
         History_Total_Deals=HistoryDealsTotal();
         prHistory_Deals(DTicketbuf,History_Total_Deals);
        }
      HistorySelect(0,TimeCurrent());  
     }
   while(History_Total_Deals!=HistoryDealsTotal());

   for(int t=0;t<History_Total_Deals;t++)
     {
      Dticket=DTicketbuf[t];
      if(HistoryDealSelect(Dticket))
        {
         if(HistoryDealGetInteger(Dticket,DEAL_TIME)>=CurrentTime)
           {
            if(HistoryDealGetInteger(Dticket,DEAL_MAGIC)==Magic)
              {
               if(HistoryDealGetInteger(Dticket,DEAL_TYPE)==DEAL_TYPE_BUY)
                 {
                  volume_BUY+=HistoryDealGetDouble(Dticket,DEAL_VOLUME);
                 }
               else
                 {
                  if(HistoryDealGetInteger(Dticket,DEAL_TYPE)==DEAL_TYPE_SELL)
                    {
                     volume_SELL+=HistoryDealGetDouble(Dticket,DEAL_VOLUME);
                    }
                 }
              }
           }
        }
      else{HistorySelect(0,TimeCurrent());t--;}
      // если сбой, то грузим историю и проходим шаг снова
     }
   volume=NormalizeDouble(volume_BUY-volume_SELL,DIGITS);
   if(volume<0)pvm.type=POSITION_TYPE_SELL;
   else
     {
      if(volume>0)pvm.type=POSITION_TYPE_BUY;
     }
   pvm.volume=fabs(volume);
   if(pvm.volume==0)return(false);
   else return(true);
  }

Выше по тексту (там, где выложен код класса CProvision) не велось объяснения того, откуда что берётся и куда чего девается, поскольку разработка советника не является предметом этой статьи.

Класс CPositionVirtualMagic же мы разберём подробно:

К классу придана структура:

struct SPositionVirtualMagic

которая используется для принятия результатов вычислений, такое себе глобальное объявление внутри класса, благодаря pvm (переменной структуры), эта структура будет доступна везде в любом методе класса.

Далее следуют два метода класса:

double               cVOLUME(){return(pvm.volume);} // Возвращает объём виртуальной позиции советника
ENUM_POSITION_TYPE   cTYPE()  {return(pvm.type);}   // Возвращает тип виртуальной позиции советника

Эти методы объявлены как public, а значит, будут доступны через вызывающую переменную класса в любом месте программы, и предназначены для вывода значений структуры в затребованном месте.

В этой же секции объявлен метод:

bool              PositionVirtualMagic(ulong Magic,string symbol,datetime CurrentTime);

Это основная функция класса, именно её мы разберём наиболее подробно, а пока, забегая наперёд, опишу функцию под спецификатором доступа private:

void              prHistory_Deals(ulong &buf[],int HTD);

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

Итак, вернёмся к  PositionVirtualMagic(), функция в самом своём начале имеет однострочное вычисление точности, до которой требуется округлять double-значение объёма рассчитанной позиции.

int DIGITS=(int)-log10(SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP)); if(DIGITS<0)DIGITS=0;

Требуется это для проведения операции сравнения с нулём, иначе какой-нибудь остаток в 8 знаке после запятой не даст приравнять значение нулю и вызовет ошибку исполнения.

Округляется же объём позиции до минимального шага. И если минимальный шаг больше 1, то округление идёт до целочисленной части. Далее идёт цикл while, но используется он по-новому (не так, как в mql4), т.к. проверка выражения истинности производиться не в начале цикла, а в конце:

    do
     {
      if(HistorySelect(0,TimeCurrent()))
        {
         History_Total_Deals=HistoryDealsTotal();
         prHistory_Deals(DTicketbuf,History_Total_Deals);
        }
      HistorySelect(0,TimeCurrent());  
     }
   while(History_Total_Deals!=HistoryDealsTotal());

Применён такой подход потому, что выражение истинности вычисляется в самом цикле и при входе ещё не готово к проверке.

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

HistorySelect(0,TimeCurrent())

Думаю, стоит объяснить используемую мной систему выбора имён переменных.

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

CurrentTime = TimeCurrent();

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

Разбирая код дальше, мы видим, что после подгрузки истории идёт вызов функции:

History_Total_Deals=HistoryDealsTotal();

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

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

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

Так организована проверка того, не было ли за время работы функции prHistory_Deals() изменений в истории сделок. Если изменений не было, то переменная History_Total_Deals будет равна HistoryDealsTotal() и произойдёт выход из цикла за один проход. Если же изменения были, система пойдёт на повтор, и так до тех пор, пока не будет загружена история тикетов без ошибок (и не забываем ";" в конце ставить):

while(History_Total_Deals!=HistoryDealsTotal());

Далее в цикле for идёт подсчёт виртуальной позиции.

Если сделка успешно прошла ряд фильтров (время совершения сделки и магик сделки), то её объем увеличивает ту часть виртуальной позиции, типу которого принадлежит сделка.

Хочу отметить, что я прописываю подсчёт виртуальной позиции только от запуска советника, хотя возможны варианты.

Тут нужно отметить, как вообще рассчитывается позиция. По счётной книге, которой все мы пользуемся с незапамятных времён и по сей день, мы имеем расход и приход, и подсчёт баланса ведётся как разность этих величин, Так же и при подсчёте позиции: если вы открыли 0.2 лота на Sell и 0.3 на Buy, то это значит, что вы держите позицию объёмом 0.1 на Buy. Время открытия и разность в уровнях - это уже категории прибыли, но позиция у вас будет 0.1 лота, тип Buy.

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

Подсчёт объёма позиции:

volume=NormalizeDouble(volume_BUY-volume_SELL,DIGITS);

Распознавание типа позиции с выводом значения в структуру:

   if(volume<0)pvm.type=POSITION_TYPE_SELL;
   else
     {
      if(volume>0)pvm.type=POSITION_TYPE_BUY;
     }

Вывод объёма в структуру:

pvm.volume=fabs(volume);

 Вывод значения функции: если объём позиции 0 то false, иначе, если позиция существует, то true:

   if(pvm.volume==0)return(false);
   else return(true);

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

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

//+------------------------------------------------------------------+
//| код советника                                                    |
//+------------------------------------------------------------------+

//--- input parameters
input ulong              magic       =1;           // магик
input int                SL          =300;         // стоплосс
input int                TP          =1000;        // тейкпрофит
input int                MA_Period   =25;          // период MA
input double             lot         =0.1;         // объём позиции
input int                MA_shift    =0;           // сдвиг индикатора
input ENUM_MA_METHOD     MA_smooth   =MODE_SMA;    // тип сглаживания
input ENUM_APPLIED_PRICE price       =PRICE_OPEN// тип цены 

//--- будем хранить хендл индикатора
int MA_handle,type_MA,rezult;
double v[2];
datetime  CurrentTime;// переменная хранит время запуска советника

MqlTradeResult  res;
MqlTick         tick;
CPositionVirtualMagic cpvm;
CProvision prov;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   CurrentTime=TimeCurrent();// переменная хранит время запуска советника
//--- создадим хендл индикатора
   MA_handle=iMA(Symbol(),0,MA_Period,MA_shift,MA_smooth,price);
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(CopyBuffer(MA_handle,0,0,2,v)<=0)
     {Print("№",magic,"Ошибка копирования");return;}
   type_MA=prov.TYPE(v); // определим тип, в зависимости от показания машки

   if(cpvm.PositionVirtualMagic(magic,_Symbol,CurrentTime))// если есть открытая позиция 
     {
      if((int)cpvm.cTYPE()!=type_MA)// проверяем не порали закрывать
        {
         Print("№",magic,"Позиция по магику имеет объём ",cpvm.cVOLUME(),
               " переворачиваем позицию типа ",(int)cpvm.cTYPE()," на ",type_MA);
         //cpvm.cVOLUME() - объём виртуальной позиции
         rezult=prov.SendOrder((ENUM_ORDER_TYPE)type_MA,cpvm.cVOLUME()+lot);// перевернуть позицию
         if(rezult!=-1)Print("№",magic," Код результата операции",rezult," volume ",res.volume);
         else{Print("№",magic,"Ошибка",GetLastError()); return;}
        }
     }
   else // если нет открытой позиции открываем
     {
      Print("№",magic,"Позиция по магику имеет объём ",cpvm.cVOLUME(),
            " открываем позицию типа ",type_MA);
      rezult=prov.SendOrder((ENUM_ORDER_TYPE)type_MA,lot);// открыть позицию 
      if(rezult!=-1)Print("№",magic," Код результата операции",rezult," volume ",res.volume);
      else{Print("№",magic,"Ошибка",GetLastError()); return;}
     }
  }

Запустим этот советник трижды на одном инструменте, но разных таймфреймах при этом зададим всем разные магики:

Рисунок 3. Зададим разные магики двум одинаковым советникам, (один инструмент, разные таймфреймы) старт первого советника

Рисунок 3. Зададим разные магики двум одинаковым советникам, (один инструмент, разные таймфреймы) старт первого советника

Рисунок 4. Зададим разные магики двум одинаковым советникам, (один инструмент, разные таймфреймы) старт второго советника

Рисунок 4. Зададим разные магики двум одинаковым советникам, (один инструмент, разные таймфреймы) старт второго советника

Рисунок 5. Результат бесконфликтной работы советников на одном инструменте с различными магиками

Рисунок 5. Результат бесконфликтной работы советников на одном инструменте с различными магиками

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

Первый пункт технического задания выполнен, то ли ещё будет. 


Кодирование magic

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

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

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

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

enum Emagic
  {
   ENUM_DIGITAL_NAME,    // цифровое имя советника
   ENUM_CODE_INTERACTION,// код взаимодействия
   ENUM_EXPERT_SYMBOL    // символ, на котором запущен советник
  };

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

Во-первых, если зададите переменную другого типа (к числам это не относится) при указании параметра из перечисления, при компиляции вы получите предупреждение об ошибке, а во-вторых, появляется наглядность: вы не просто присваиваете 0, а даёте команду присвоить ENUM_DIGITAL_NAME.

Как и при создании структуры или класса, имя перечисления я выбирал просто, к общему выбранному имени прибавляю E, получилось Emagic, соответственно структура будет Smagic а класс Cmagic.

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

Далее создадим структуру для хранения наших кодов.

struct Smagic
  {
   ulong             magicnumber;      // магик в собранном виде - как он пишеться в ордере
   int               digital_name;     // цифровое имя
   int               code_interaction; // код взаимодействия
   int               expert_symbol;    // символ, на котором запущен советник
  };

После чего объявим класс Cmagic в котором пропишем все методы по кодированию и декодированию магика, кстати, включим в этот класс и методы из предыдущего советника (просто объявим их в нашем классе и перепишем заголовки)

class Cmagic
  {
protected:
   Smagic            mag;
   SPositionVirtualMagic pvm;
public:
// функция возвращает сборный магик, собранный из входных данных
   ulong             SetMagic_request(int digital_name=0,int code_interaction=0);

// функция получает сборный магик и разделяет его в соответствии с логикой сборки
   ulong             SetMagic_result(ulong magicnumber);    

// функция получает идентификатор возврата и возвращает запрошенную часть сборного магика
   ulong             GetMagic_result(Emagic enum_); 

// функция получает идентификатор возврата и возвращает текстовую интерпретацию запрошенной части сборного магика
   string            sGetMagic_result(Emagic enum_);

// возвращает объём виртуальной позиции советника
   double            cVOLUME(){return(pvm.volume);}
   
// возвращает  тип  виртуальной позиции советника
   ENUM_POSITION_TYPE  cTYPE(){return(pvm.type);}
                                           
// метод расчёта виртуальной позиции, возвращает наличие или отсутствие виртуальной позиции   
   bool              PositionVirtualMagic(Emagic enum_,
                                          string symbol,
                                          datetime CurrentTime);
private:
// функция разделяет магик на три части по три разряда и возвращает ту часть, на которую указывает category
   int               decodeMagic_result(int category); 

// интерпретатор символов инструмента в цифровой код                                                      
   int               symbolexpert();     
   
// интерпретатор цифрового кода в прописанный текст(советники)
   string            expertcode(int code);    
                                 
// интерпретатор цифрового кода в прописанный текст(взаимодействие)   
   string            codeinterdescript(int code);

// интерпретатор цифрового кода в символ инструмента                                         
   string            symbolexpert(int code);

// цикл записи тикетов в буфер
   void              prHistory_Deals(ulong &buf[],int HTD);    
  };   

Теперь разработаем методы.

Первый метод в классе:

//+------------------------------------------------------------------+
//| Функция возвращает сборный магик, собранный из входных данных    |
//+------------------------------------------------------------------+
ulong Cmagic::SetMagic_request(int digital_name=0,int code_interaction=0)
  {
   if(digital_name>=1000)Print("Неверно задано цифровое имя советника (больше 1000)");
   if(code_interaction>=1000)Print("Неверно задан код опознания свой-чужой (больше 1000)");
   mag.digital_name     =digital_name;
   mag.code_interaction =code_interaction;
   mag.expert_symbol    =symbolexpert();
   mag.magicnumber      =mag.digital_name*(int)pow(1000,2)+
                         mag.code_interaction*(int)pow(1000,1)+
                         mag.expert_symbol;
   return(mag.magicnumber);
  }

Этод метод получает два значения: цифровое имя советника и код взаимодействия.

ulong Cmagic::SetMagic_request(int digital_name=0,int code_interaction=0)

И сразу их проверяет на корректность:

   if(digital_name>=1000)Print("Не верно задано цифровое имя советника(больше 1000)");
   if(code_interaction>=1000)Print("Не верно задан код опознания свой-чужой(больше 1000)");

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

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

int Cmagic::symbolexpert()

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

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

И, наконец, главная строка всего метода:

mag.magicnumber      =mag.digital_name*(int)pow(1000,2)+
                      mag.code_interaction*(int)pow(1000,1)+
                      mag.expert_symbol;

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

Следующий публичный метод класса:

//+------------------------------------------------------------------+
//| Функция получает сборный магик                                   |
//| и разделяет его в соответствии с логикой сборки                  |
//+------------------------------------------------------------------+
ulong Cmagic::SetMagic_result(ulong magicnumber)
  {
   mag.magicnumber      =magicnumber;
   mag.expert_symbol    =decodeMagic_result(1);
   mag.code_interaction =decodeMagic_result(2);
   mag.digital_name     =decodeMagic_result(3);
   return(mag.magicnumber);
  }

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

Но вернёмся к нашим приватам:

//+------------------------------------------------------------------+
//| Функция разделяет магик на три части по три разряда              |
//| и возвращает ту часть, на которую указывает category             |
//+------------------------------------------------------------------+
int Cmagic::decodeMagic_result(int category)
  {
   string string_value=(string)mag.magicnumber;
   int rem=(int)MathMod(StringLen(string_value),3);
   if(rem!=0)
     {
      rem=3-rem;
      string srem="0";
      if(rem==2)srem="00";
      string_value=srem+string_value;
     }
   int start_pos=StringLen(string_value)-3*category;
   string value=StringSubstr(string_value,start_pos,3);
   return((int)StringToInteger(value));
  }

Визуально этот метод можно представить как считывание трёхзначного числа из указанного поля, например, имеем магик 123456789, это можно представить как |123|456|789| если указано поле 1, то результат будет 789, т.к. нумерация полей идёт справа налево.

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

string string_value=(string)mag.magicnumber;

с последующим разбором отдельных компонентов строки.

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

//+------------------------------------------------------------------+
//| Функция получает идентификатор возврата                          |
//| и возвращает запрошенную часть сборного магика                   |
//+------------------------------------------------------------------+
ulong Cmagic::GetMagic_result(Emagic enum_)
  {
   switch(enum_)
     {
      case ENUM_DIGITAL_NAME     : return(mag.digital_name);     break;
      case ENUM_CODE_INTERACTION : return(mag.code_interaction); break;
      case ENUM_EXPERT_SYMBOL    : return(mag.expert_symbol);    break;
      default: return(mag.magicnumber); break;
     }
  }
//+------------------------------------------------------------------+
//| Функция получает идентификатор возврата и возвращает             |
//| текстовую интерпретацию запрошенной части сборного магика        |
//+------------------------------------------------------------------+
string Cmagic::sGetMagic_result(Emagic enum_)
  {
   switch(enum_)
     {
      case ENUM_DIGITAL_NAME     : return(expertcode(mag.digital_name));            break;
      case ENUM_CODE_INTERACTION : return(codeinterdescript(mag.code_interaction)); break;
      case ENUM_EXPERT_SYMBOL    : return(symbolexpert(mag.expert_symbol));         break;
      default: return((string)mag.magicnumber); break;
     }
  }

Функции возвращают ту часть магика, на которую указывает параметр типа Emagic, при этом первая выдаёт результат в виде ulong, и, соответственно, используется в расчётах, а вторая - результат типа string и может быть использована для визуализации.

В функции GetMagic_result() всё организовано просто, она распределяет значения структуры по веткам switch, тогда как в sGetMagic_result() немного сложней. Каждая ветка case вызывает табличную функцию, которая и переводит значение структуры в наглядную форму. Таким образом, если значение mag.expert_symbol=1 то первая функция выдаст 1, а вторая EURUSD.

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

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

Не мудрствуя лукаво с наследованием, я просто переобъявил их в новом классе, тем более что их пришлось немного доработать.

Теперь главный метод:

bool  Cmagic::PositionVirtualMagic(Emagic enum_,
                                   string symbol,
                                   datetime CurrentTime)

не только объявлен как метод класса Cmagic но и имеет другой набор параметров.

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

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

Что ж, раз класс разработан, нужно его опробовать на новом советнике:

//+------------------------------------------------------------------+
//| Код советника                                                    |
//+------------------------------------------------------------------+

//--- input parameters
input ulong              digital_name_       =4;           // цифровое имя советника
input ulong              code_interaction_   =1;           // код взаимодействия
input Emagic             _enum               =0;           // модель магика  
input int                SL                  =300;         // стоплосс
input int                TP                  =1000;        // тейкпрофит
input int                MA_Period           =25;          // период MA
input double             lot                 =0.4;         // объём позиции
input int                MA_shift            =0;           // сдвиг индикатора
input ENUM_MA_METHOD     MA_smooth           =MODE_SMA;    // тип сглаживания
input ENUM_APPLIED_PRICE price               =PRICE_OPEN// тип цены 

//--- будем хранить хендл индикатора
int MA_handle,type_MA,rezult;
static ulong magic;
double v[2];
datetime  CurrentTime;// переменная хранит время запуска советника

MqlTradeResult  res;   // указатель на структуру ответа по OrderSend
MqlTick        tick;  // указатель на структуру последних рыночных данных
CProvision prov;
Cmagic mg;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   magic=mg.SetMagic_request(digital_name_,code_interaction_);
// Штамп эксперта (идентификатор magic number) переменная magic обьявлена глобально
// используеться в    int CProvision::SendOrder(ENUM_ORDER_TYPE type,double volume)
   CurrentTime=TimeCurrent();// переменная хранит время запуска советника
//--- создадим хендл индикатора
   MA_handle=iMA(Symbol(),0,MA_Period,MA_shift,MA_smooth,price);
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(CopyBuffer(MA_handle,0,0,2,v)<=0)
     {Print("№",magic,"Ошибка копирования");return;}
   type_MA=prov.TYPE(v); // определим тип, в зависимости от показания машки
   mg.SetMagic_result(magic);// заносим данные в структуру
   if(mg.PositionVirtualMagic(_enum,_Symbol,CurrentTime))// если есть открытая позиция 
     {
      if((int)mg.cTYPE()!=type_MA)// проверяем не порали закрывать
        {
         mg.SetMagic_result(magic);// заносим данные в структуру
         Print("№",mg.GetMagic_result(_enum),"Позиция по магику имеет объём ",mg.cVOLUME(),
               " переворачиваем позицию типа ",(int)mg.cTYPE()," на ",type_MA);
         //cpvm.cVOLUME() - объём виртуальной позиции
         rezult=prov.SendOrder((ENUM_ORDER_TYPE)type_MA,mg.cVOLUME()+lot);// перевернуть позицию
         if(rezult!=-1)Print("№",magic," Код результата операции",rezult," volume ",res.volume);
         else{Print("№",magic,"Ошибка",GetLastError()); return;}
        }
     }
   else // если нет открытой позиции, открываем
     {
      Print("№",magic,"Позиция по магику имеет объём ",mg.cVOLUME(),
                     " открываем позицию типа ",type_MA);
      rezult=prov.SendOrder((ENUM_ORDER_TYPE)type_MA,lot);// открыть позицию 
      if(rezult!=-1)Print("№",magic," Код результата операции",rezult," volume ",res.volume);
      else{Print("№",magic,"Ошибка",GetLastError()); return;}
     }
  }

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

Рисунок 6. Установка трех советников с разными магиками на различные графики

Рисунок 6. Установка трех советников с разными магиками на различные графики

Рисунок 7. Результат бесконфликтной торговли трех советников с разными магиками

Рисунок 7. Результат бесконфликтной торговли трех советников с разными магиками

Как видно из распринтовки в сообщениях экспертов, все три участника стартовали успешно и конфликтностью не отличаются.


Заключение

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

Удачи и до новых встреч.


Прикрепленные файлы |
magic_exp0.mq5 (8.25 KB)
magic_exp1.mq5 (11.6 KB)
magic_exp2.mq5 (24.72 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (31)
[Удален] | 24 окт. 2016 в 22:03
Приходят в голову такие умные мысли, что неплохо было бы иметь готовую MQL5 функцию, которая бы выдавала список сделок, из которых состоит текущая открытая позиция. Я об неттинге. Тогда бы можно было всегда знать, какие сделки и с каким Magic присутствуют на данный момент в позиции. Сейчас же, если один эксперт с одним Magic открыл позицию, второй эксперт со вторым Magic добавил в позицию, затем трейдер закрыл часть позиции руками и получилась неизвестность - от какого Magic откусили.
Dmitry Fedoseev
Dmitry Fedoseev | 24 окт. 2016 в 22:37
RickD:
Приходят в голову такие умные мысли, что неплохо было бы иметь готовую MQL5 функцию, которая бы выдавала список сделок, из которых состоит текущая открытая позиция. Я об неттинге. Тогда бы можно было всегда знать, какие сделки и с каким Magic присутствуют на данный момент в позиции. Сейчас же, если один эксперт с одним Magic открыл позицию, второй эксперт со вторым Magic добавил в позицию, затем трейдер закрыл часть позиции руками и получилась неизвестность - от какого Magic откусили.
HistorySelectByPosition() не то?
[Удален] | 25 окт. 2016 в 01:39
Dmitry Fedoseev:
HistorySelectByPosition() не то?
Не то. Если 5 сделок пришло в плюс и затем 3 в минус, то HistorySelectByPositionEx() показывала бы оставшиеся 2 сделки, из которых состоит позиция. Это аналог открытых ордеров в MT4.
Viktor Vlasenko
Viktor Vlasenko | 5 мар. 2017 в 09:54

на всякий случай, если кто надумает воспользоваться данной библиотекой (классом), гляньте тут: https://www.mql5.com/ru/forum/171241

‌хотел использовать его, столкнулся с проблемами

п‌онятно, что всегда можно подправить, но тем не менее

SergeyNU
SergeyNU | 18 мая 2020 в 20:03

Добрый день!

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

Написание советника в MQL5 с использованием объектно-ориентированного подхода Написание советника в MQL5 с использованием объектно-ориентированного подхода
Эта статья посвящена использованию объектно-ориентированного подхода для создания советника, рассмотренного в статье "Пошаговое руководство по написанию советников для начинающих". Большинство людей думают, что это сложно, но могу вас заверить, что после прочтения этой статьи вы сможете написать свой собственный советник на основе объектно-ориентированного похода.
Функции для управления капиталом в экспертах Функции для управления капиталом в экспертах
Разработка торговой стратегии, в первую очередь, заключается в поиске закономерностей для входа в рынок, выхода из рынка и правил удержания позиций. Если найденные закономерности удается формализовать в правила для автоматической торговли, то перед трейдером возникают вопросы по расчету объемов позиций, вычислению размера маржи и поддержанию безопасного уровня залоговых средств для обеспечения открытых позиций в автоматическом режиме. В этой статье мы напишем на MQL5 простые примеры для выполнения этих расчетов.
Исследование быстродействия скользящих средних в MQL5 Исследование быстродействия скользящих средних в MQL5
Со времён создания первого индикатора простой скользящей средней появилась масса разнообразных индикаторов. Многие из них построены именно на схожем принципе или используют в своих расчётах те или иные способы обработки ценового ряда. При этом зачастую за бортом остаётся вопрос скорости вычислений таких индикаторов и оптимальности алгоритмов, заложенных в них. В статье рассмотрены все возможные варианты использования скользящих средних и проведён сравнительный анализ быстродействия каждого.
Реализация взаимодействия между клиентскими терминалами MetaTrader 5 при помощи именованных каналов (Named Pipes) Реализация взаимодействия между клиентскими терминалами MetaTrader 5 при помощи именованных каналов (Named Pipes)
Данная статья знакомит с реализацией межпроцессного взаимодействия между терминалами MetaTrader 5 посредством именованных каналов (named pipes). Предложен класс CNamedPipes, реализующий возможность использования именованных каналов. Рассмотрен тиковый индикатор для тестирования связи между двумя клиентскими терминалами MetaTrader 5 и измерения общей пропускной способности системы. Представленный метод взаимодействия оказался пригодным для отправки котировок в реальном времени.