MQL4: Пишем копировщик сделок для MetaTrader4

Добрый день, друзья!

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

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

MQL4 Пишем копировщик сделок для MT4

Общая идея

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

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

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

struct Params
  {
   int               account;
   char              symbol[SYMBOL_LEN];
   int               ticket;
   int               magic;
   int               type;
   double            volume;
   double            sl;
   double            tp;
   double            equity;
  };

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

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

void write(string name,Params &a[],bool local=false)
     {
      int h;
      do
        {
         h=local ? FileOpen(name,FILE_WRITE|FILE_BIN) : 
              FileOpen(name,FILE_WRITE|FILE_BIN|FILE_COMMON);
         if(GetLastError()!=0)
            Sleep(DELAY);
        }
      while(h==INVALID_HANDLE);
      FileWriteArray(h,a);
      FileClose(h);
     }

   void read(string name,Params &a[],bool local=false)
     {
      int h;
      do
        {
         h=local ? FileOpen(name,FILE_READ|FILE_BIN) : 
             FileOpen(name,FILE_READ|FILE_BIN|FILE_COMMON);
         if(GetLastError()!=0)
            Sleep(DELAY);
        }
      while(h==INVALID_HANDLE);
      FileReadArray(h,a);
      FileClose(h);
     }

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

void create()
     {
      account=AccountNumber();
      backupName=IntegerToString(AccountNumber());
      sharedName="shared";
      backup();
     }

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

void backup()
     {
      write(backupName,shared,true);
     }

Также есть вариант для чтения из файла, где возвращается количество сохраненных позиций.

int backup(Params &a[])
     {
      read(backupName,a,true);
      return ArraySize(a);
     }

Имеются такие же методы для шаринга позиций. “pull” – для получения данных о позициях из общего файла, “share” – для записи изменений в общий файл. В “share” имеется поддержка нескольких провайдеров. Чтобы исключить конфликт нескольких мастер-счетов, пытающихся получить доступ к одному файлу, каждый провайдер идентифицируется по номеру аккаунта.

int pull(Params &a[])
     {
      read(sharedName,a);
      return ArraySize(a);
     }

   void share()
     {
      Params orders[],temp[];
      read(sharedName,orders); // читаем расшаренные позиции

      int szO=ArraySize(orders);
      int szP=ArraySize(positions);
      int szTemp=ArrayResize(temp,0,szO+szP);

      // копируем все "чужие" ордера во временный массив
      for(int i=0;i<szO;i++)
         if(orders[i].account!=this.account)
           {
            szTemp=ArrayResize(temp,szTemp+1);
            ArrayCopy(temp,orders,szTemp-1,i,1);
           }
      // теперь дописываем текущие позиции на счету
      ArrayResize(temp,szTemp+szP);
      for(int i=szTemp;i<szTemp+szP;i++)
         ArrayCopy(temp,positions,i,i-szTemp,1);

      // сохраняем изменения в общий файл
      write(sharedName,temp);
     }

Метод “opened” возвращает текущие открытые позиции на счету. В начале я говорил, что при частичном закрытии Метатрейдер изменяет тикет ордера, а старый записывает в комментарий к новому. Поэтому, чтобы клиент понимал, что это не новый ордер, запоминаем старый тикет, записывая в поле “magic”.

for(int i=total-1;i>=0;i--)
        {
         if(OrderSelect(i,SELECT_BY_POS) && OrderType() <= 1)
           {
            sz=ArrayResize(positions,sz+1);
            char sa[];
            StringToCharArray(OrderSymbol(),sa);
            ArrayCopy(positions[sz-1].symbol,sa);
            positions[sz-1].ticket=OrderTicket();
            c=OrderComment();
            positions[sz-1].magic=
       StringToInteger(StringSubstr(c,StringFind(c,"#")+1));
            positions[sz-1].volume=OrderLots();
            positions[sz-1].sl=OrderStopLoss();
            positions[sz-1].tp=OrderTakeProfit();
            positions[sz-1].type=OrderType();
            positions[sz-1].equity=eq;
           }
        }

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

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

TradeCopier *func()
  {
   ...
   return GetPointer(this);
  }

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

void OnTimer()
  {
   if(CopierType==SLAVE)
     {
      copier.mergeAndTrade();
     }
   else if(CopierType==MASTER)
     {
      copier.opened().share();
     }
  }

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

Также, можно выбрать способ копирования объемов. Фиксированный объем копируется один в один, независимо от размера депозита. Динамический объем копируется кратно размеру эквити на клиенте. То есть, если на провайдере открыта позиция 0.1 лота с депозитом 100 долларов, то на клиенте с депозитом в 200 долларов откроется сделка объемом 0.2 лота.

Если лот динамический, вычисляем отношение эквити текущего счета к счету мастера, и умножаем это значение на объем ордера. Также, не забываем нормализовать значение объема.

double v=LotType==DYNAMIC
   ? normalizeLot(CharArrayToString(p.symbol),
       eq/p.equity*p.volume)
   : p.volume;

static double normalizeLot(string symbol,double lot)
     {
      double lotMin = SymbolInfoDouble(symbol,
         SYMBOL_VOLUME_MIN);
      double lotMax = SymbolInfoDouble(symbol,
         SYMBOL_VOLUME_MAX);
      double lotStep= SymbolInfoDouble(symbol,
         SYMBOL_VOLUME_STEP);
      double c=1.0/lotStep;
      double l=floor(lot*c)/c;
      if(l  lotMax) l = lotMax;
      return l;
     }

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

Заключение

Итак, на данном этапе реализовано:

  • Надежность передачи – файлы не используются одновременно разными процессами, при этом у каждого клиента имеется снимок текущих позиций;
  • Копирование фиксированного и относительного объема позиций, поддержка модификации стоп уровней и частичного закрытия;
  • Возможность применения на любом количестве терминалов MT4/MT5, с неограниченным количеством Провайдеров/Клиентов.

К недостаткам можно отнести:

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

Скачать исходник Копировщика

Тема на форуме

С уважением, Дмитрий аkа Silentspec

Уроки по MQL4 , , , ,