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

Использование MetaTrader 5 как поставщика торговых сигналов для MetaTrader 4

MetaTrader 5Примеры | 10 ноября 2011, 18:12
5 612 3
Karlis Balcers
Karlis Balcers

Введение

Для изучения этого вопроса и написания статьи у меня было несколько причин.

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

Во-вторых, в течение Automated Trading Championship у многих возникала мысль о копировании сделок лидеров на реальные счета. Некоторые создали свои собственные способы копирования сделок, другие все в поиске наилучших вариантах реализации этой идеи (и способов управления капиталом) для получения результатов, наиболее близких к участникам Чемпионата.

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

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


1. Как копировать торговлю участников Automated Trading Championship?

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

Из форума Чемпионата я узнал, что другие люди также используют этот способ, однако он не является эффективным. Кроме того, он создает огромную нагрузку на сервер Чемпионата и организаторам это может не понравиться. Есть ли решение? Я посмотрел все варианты и мне понравилась возможность доступа по паролю инвестора (при котором торговля запрещена) в MetaTrader 5 к счету любого участника Чемпионата.

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


2. Что копировать - позиции, ордера или сделки?

Если нам придется передавать информацию из MetaTrader 5 в MetaTrader 4, то нам потребуется учесть все типы ордеров, возможные в MetaTrader 4. Кроме того, при следовании за торговлей мы хотим знать о любых действиях, совершаемых на торговом счете. Сравнение статуса позиций (Positions) на каждом тике или каждую секунду не даст нам полную информацию.

По этой причине лучше следовать за ордерами (Orders) или сделками (Deals).

Я стал смотреть на структуру ордеров:

Orders

Ордера хороши по той причине, что они выставляются до совершения сделки, и также содержат информацию о том, является ли ордер отложенным. Однако в ордерах не хватает одной важной вещи, которая присутствует в сделках - типе сделки (ENUM_DEAL_ENTRY):

Deals

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

В случае если мы следуем лишь за сделками (Deals), мы по-прежнему будем исполнять ордера, но с небольшой задержкой, которая определяется сетевым соединением. При выборе между скоростью (отложенные ордера) и производительностью (сделки) я выбрал последнее.


3. Как предоставлять "сигналы"?

Способы соединения и передачи данными между терминалом MetaTrader и другими приложения и компьютерами обсуждались в различных статьях. Поскольку я хочу чтобы другие клиенты имели возможность к нам подсоединяться (скорее всего, они будут делать это с других компьютеров), я выбрал работу через сокеты по протоколу TCP.

Так как язык MQL5 не позволяет работать напрямую с функциями API, нам потребуется использовать внешние библиотеки. Существует множество статей об использовании библиотеки "WinInet.dll" (например, "Использование WinInet.dll для обмена данными между терминалами через Интернет" и др.), но ни одна из них не удовлетворяет нашим требованиям.

Поскольку я немного знаком с языком C# (до этого я сделал пару серверов, работающих в режиме реального времени), я решил создать свою собственную библиотеку. Я воспользовался статьей "Как открыть мир C# из MQL5 путем экспорта неуправляемого кода", которая помогла мне решить вопросы совместимости. Я написал сервер с очень простым интерфейсом и возможностью одновременного соединения с 500 клиентами (для его работы на вашем компьютере должен быть установлен .NET framework версия 3.5 или выше, на большинстве компьютеров "Microsoft .NET Framework 3.5 уже установлен).

#import "SocketServer.dll"    // Библиотека, написанная на языке C# 
                              // (создана при помощи метода, описанного в статье https://www.mql5.com/ru/articles/249)
string About();            // Информация о библиотеке.
int SendToAll(string msg);  // Посылает одно текстовое сообщение всем клиентам.
bool Stop();               // Останавливает сервер.
bool StartListen(int port); // Стартует сервер. Сервер будет прослушивать все входящие соединения (до 500 подключений). 
                               // Все клиенты построены на базе асинхронных потоков.
string ReadLogLine();       // Запрашивает одну строку лога сервера (может содержать сведения об ошибках и другую информацию). 
                               // Сервер хранит только последние 100 строк лога.
#import

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

Код на C# выглядит следующим образом:

internal static void WaitForClients()
        {
            if (server != null)
            {
                Debug("Cant start lisening! Server not disposed.");
                return;
            }
            try
            {

                IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, iPort);
                server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

                server.Bind(localEndPoint);
                server.Listen(500);
                isServerClosed = false;
                isServerClosedOrClosing = false;

                while (!isServerClosedOrClosing)
                {
                    allDone.Reset();
                    server.BeginAccept(new AsyncCallback(AcceptCallback), server);
                    allDone.WaitOne();
                }
                
            }
            catch (ThreadAbortException)
            {
            }
            catch (Exception e)
            {
                Debug("WaitForClients() Error: " + e.Message);
            }
            finally
            {
                if (server != null)
                {
                    server.Close();
                    server = null;
                }
                isServerClosed = true;
                isServerClosedOrClosing = true;
            }
        }

        internal static void AcceptCallback(IAsyncResult ar)
        {
            try
            {
                allDone.Set();
                if (isServerClosedOrClosing)
                    return;
                Socket listener = (Socket)ar.AsyncState;
                Socket client = listener.EndAccept(ar);

                if (clients != null)
                {
                    lock (clients)
                    {
                        Array.Resize(ref clients, clients.Length + 1);
                        clients[clients.Length - 1].socket = client;
                        clients[clients.Length - 1].ip = client.RemoteEndPoint.ToString();
                        clients[clients.Length - 1].alive = true;
                    }
                    Debug("Client connected: " + clients[clients.Length - 1].ip);
                }
            }
            catch (Exception ex)
            {
                Debug("AcceptCallback() Error: " + ex.Message);
            }
        }

Подробнее о работе с асинхронными серверными сокетами C# можно прочитать в Microsoft MSDN или статьях, которые можно найти при помощи Google.


4. Отправка торговых сигналов

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

Для этой цели я воспользовался помощью MQL4 codebase: "https://www.mql5.com/en/code/9296", где есть вполне хорошая библиотека (WinSock.mqh) позволяющая очень просто работать с сокетами. Некоторые высказывали претензии по стабильности ее работы, однако для моих целей она оказалась вполне достаточной, и в процессе тестирования проблем у меня не было.

#include <winsock.mqh>  // Библиотека из Codebase на mql4.com
                        // Ссылка:   https://www.mql5.com/en/code/9296
                        // Статья:   https://www.mql5.com/en/code/download/9296


5. Обработка данных

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

5.1. Серверная часть

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

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

int OnInit()
  {
   string str="";
   Print(UTF8_to_ASCII(About()));
//--- старт сервера
   Print("Starting server on port ",InpPort,"...");
   if(!StartListen(InpPort))
     {
      PrintLogs();
      Print("OnInit() - FAILED");
      return -1;
     }

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

Для лучшего понимания в коде есть комментарии:

//+------------------------------------------------------------------+
//| OnTrade() - обработчик событий торговой активности               |
//+------------------------------------------------------------------+
void OnTrade()
  {
//--- найдем все новые сделки и уведомим о них всех клиентов
//--- -24 часа
   datetime dtStart=TimeCurrent()-60*60*24;
//--- +24 часа (если у вас время-<hours>)
   datetime dtEnd=TimeCurrent()+60*60*24;
//--- запрашиваем историю сделок за последние сутки
   if(HistorySelect(dtStart,dtEnd))
     {
      //--- пройдемся по всем сделкам (от старых к новым)
      for(int i=0;i<HistoryDealsTotal();i++)
        {
         //--- получаем тикет сделки
         ulong ticket=HistoryDealGetTicket(i);
         //--- if this deal is interesting for us.
         if(HistoryDealGetInteger(ticket,DEAL_ENTRY)!=DEAL_ENTRY_STATE)
           {
            //Print("Entry type ok.");
            //--- проверяем, была ли эта сделка позднее той, о которой уведомляли в последний раз
            if(HistoryDealGetInteger(ticket,DEAL_TIME)>g_dtLastDealTime)
              {
               //--- если часть позиции закрыта, проверяем необходимость разрашения символа
               if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_OUT)
                 {
                  vUpdateEnabledSymbols();
                 }
               //--- если открыта позиция в противоположном направлении, нужно разрешить запрещенный символ
               else if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT)
                 {
                  //--- разрешаем этот символ
                  vEnableSymbol(HistoryDealGetString(ticket,DEAL_SYMBOL));
                 }
               //--- проверка разрешенности торговли по символу
               if(bIsThisSymbolEnabled(HistoryDealGetString(ticket,DEAL_SYMBOL)))
                 {
                  //--- подготовка строки со сделкой и ее отправка всем подключенным клиентам
                  int cnt=SendToAll(sBuildDealString(ticket));
                  //--- техническая проблема на сервере.
                  if(cnt<0)
                    {
                     Print("Failed to send new deals!");
                    }
                  //--- если никому не отправлено (cnt==0) или удалось кому-либо отправить (cnt>0)                  
                  else
                    {
                     //--- обновляем время последней сделки
                     g_dtLastDealTime=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
                    }
                 }
               //--- уведомления не требуется, поскольку торговля по символу запрещена
               else
                 {
                  //--- обновляем время последней сделки, о которой мы не будем уведомлять
                  g_dtLastDealTime=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
                 }
              }
           }
        }
     }
  }

Как вы уже поняли, в случае нахождения новой сделки мы вызываем функцию BuildDealString() для подготовки данных к отправке. Все данные передаются в текстовом формате, информация о каждой сделки начинается с символа '<' и заканчивается символом  '>'.

Это поможет нам разделить строки с несколькими сделками, поскольку при передаче данных по протоколу TCP/IP пакеты могут склеиваться и в них может содержаться информация о нескольких сделках.

//+------------------------------------------------------------------+
//| Функция построения строки с информацией о сделке                 |
//| Примеры:                                                         |
//| EURUSD;BUY;IN;0.01;1.37294                                       |
//| EURUSD;SELL;OUT;0.01;1.37310                                     |
//| EURUSD;SELL;IN;0.01;1.37320                                      |
//| EURUSD;BUY;INOUT;0.02;1.37294                                    |
//+------------------------------------------------------------------+
string sBuildDealString(ulong ticket)
  {
   string deal="";
   double volume=0;
   bool bFirstInOut=true;
//--- определяем объем сделки
//--- случай если сделка является INOUT
   if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT)
     {
      if(PositionSelect(HistoryDealGetString(ticket,DEAL_SYMBOL)))
        {
         volume=PositionGetDouble(POSITION_VOLUME);
        }
      else
        {
         Print("Failed to get volume!");
        }
     }
//--- если сделка 'IN' или 'OUT', то используем объем сделки
   else
     {
      volume=HistoryDealGetDouble(ticket,DEAL_VOLUME);
     }
//--- строим строку сделки(пример: "<EURUSD;BUY;IN;0.01;1.37294>").
   int iDealEntry=(int)HistoryDealGetInteger(ticket,DEAL_ENTRY);
//--- если это сделка OUT и не осталось открытых позиций
   if(iDealEntry==DEAL_ENTRY_OUT && !PositionSelect(HistoryDealGetString(ticket,DEAL_SYMBOL)))
     {
      //--- в целях безопасности мы проверяем, осталась ли позиция по текущему символу. Если НЕТ, то будем
      //--- использовать новый тип сделки - OUTALL. Это гарантирует нам, что после этого не останется открытых ордеров
      //--- и позиция закрыта на "удаленных" клиентах. Это обусловлена особенностями отображения объемов
      //--- в новые на клиентской стороне, поэтому может быть очень маленькая разница между ними, и ордер
      //--- с очень маленьким объемом может остаться.
      //--- значение введено мной (его нет в перечислении ENUM_DEAL_ENTRY).
      iDealEntry=DEAL_ENTRY_OUTALL;  
     }
   StringConcatenate(deal,"<",AccountInfoInteger(ACCOUNT_LOGIN),";", HistoryDealGetString(ticket,DEAL_SYMBOL),";",
                   Type2String((ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE)),";",
                   Entry2String(iDealEntry),";",DoubleToString(volume,2),";",
                   DoubleToString(HistoryDealGetDouble(ticket,DEAL_PRICE),
                   (int)SymbolInfoInteger(HistoryDealGetString(ticket,DEAL_SYMBOL),SYMBOL_DIGITS)),">");
   Print("DEAL:",deal);
   return deal;
  }

Возможно, вы удивлены появлением нового типа сделки - DEAL_ENTRY_OUTALL. Этот тип создан мной, причину его появления мы увидим при рассмотрении обработки объемов торговли на стороне MetaTrader 4.

Другой интересный момент - функция OnTimer(). При инициализации производится вызов функции EventSetTimer(1) для вызова таймера каждую секунду. В коде обработчика таймера присутствует лишь одна строка, которая выводит информацию в логи сервера:

//+------------------------------------------------------------------+
//| Выводит логи сервера каждую секунду (если таковые имеются)       |
//+------------------------------------------------------------------+
void OnTimer()
  {
   PrintLogs();
  }

Функцию PrintLogs() рекомендую вызывать (для вывода информации о статусе и ошибках) после выполнения каждой функции серверной библиотеки.

На стороне сервера вы также найдете входной параметр StartupType:

//+------------------------------------------------------------------+
//| Режим старта                                                     |
//+------------------------------------------------------------------+
enum ENUM_STARTUP_TYPE
  {
   STARTUP_TYPE_CLEAR,    // CLEAR - посылать каждую новую СДЕЛКУ, появившуюся на счете 
                          // (Send every new DEAL wich appears on account).
   STARTUP_TYPE_CONTINUE  // CONTINUE - не посылать СДЕЛКУ до закрытия существующей ПОЗИЦИИ
                          // (Do not send DEAL before existing POSITION has been closed).
  };
//--- входные параметры
input int               InpPort=2011;                         // Входящий порт
input ENUM_STARTUP_TYPE InpStartupType=STARTUP_TYPE_CONTINUE; // Тип старта

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

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


5.2. Клиентская часть

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

/--- запуск и начала сбора и обработки данных
   while(!IsStopped())
     {
      Print("Client: Waiting for DEAL...");
      ArrayInitialize(iBuffer,0);
      iRetVal=recv(iSocketHandle,iBuffer,ArraySize(iBuffer)<<2,0);
      if(iRetVal>0)
        {
         string sRawData=struct2str(iBuffer,iRetVal<<18);
         Print("Received("+iRetVal+"): "+sRawData);

Но это создает проблему при остановке клиента. При вызове команды остановки скрипта (Remove Script) скрипт не будет остановлен с первого раза. Необходимо сделать это дважды и скрипт будет остановлен по таймауту. Это можно исправить установкой таймаута для функции recv, но поскольку используется общедоступная библиотека из Codebase, оставим это дело ее автору.

После получения данных, мы их обрабатываем и проверяем, до совершения сделки на реальном счете:

         //--- парсинг записей
         string arrDeals[];
         //--- разбиваем данные на несколько сделок (если их несколько)
         int iDealsReceived=Split(sRawData,"<",10,arrDeals);
         Print("Found ",iDealsReceived," deal orders.");
         //--- обработка каждой сделки
         //--- цикл по всем полученным сделкам
         for(int j=0;j<iDealsReceived;j++) 
           {
            //--- парсим значения каждой записи сделки
            string arrValues[];
            //--- получаем значения
            int iValuesInDeal=Split(arrDeals[j],";",10,arrValues);
            //--- проверяем корректность формата полученных данных (кол-во данных)
            if(iValuesInDeal==6)
              {
               if(ProcessOrderRaw(arrValues[0],arrValues[1],arrValues[2],arrValues[3],
                                  arrValues[4],StringSubstr(arrValues[5],0,StringLen(arrValues[5])-1)))
                 {
                  Print("Processing of order done sucessfully.");
                 }
               else
                 {
                  Print("Processing of order failed:\"",arrDeals[j],"\"");
                 }
              }
            else
              {
               Print("Invalid order received:\"",arrDeals[j],"\"");
               //--- this was last one in array
               if(j==iDealsReceived-1)
                 {
                  //--- начало следующей сделки может быть неполным
                  sLeftOver=arrDeals[j];
                 }
              }
           }
//+------------------------------------------------------------------+
//| Обработка "сырых" (raw) полученных данных (в текстовом формате)  |
//+------------------------------------------------------------------+
bool ProcessOrderRaw(string saccount,string ssymbol,string stype,
                    string sentry,string svolume,string sprice)
  {
//--- очистка
   saccount= Trim(saccount);
   ssymbol = Trim(ssymbol);
   stype=Trim(stype);
   sentry=Trim(sentry);
   svolume= Trim(svolume);
   sprice = Trim(sprice);
//--- проверка корректности
   if(!ValidateAccountNumber(saccount)){Print("Invalid account:",saccount);return(false);}
   if(!ValidateSymbol(ssymbol)){Print("Invalid symbol:",ssymbol);return(false);}
   if(!ValidateType(stype)){Print("Invalid type:",stype);return(false);}
   if(!ValidateEntry(sentry)){Print("Invalid entry:",sentry);return(false);}
   if(!ValidateVolume(svolume)){Print("Invalid volume:",svolume);return(false);}
   if(!ValidatePrice(sprice)){Print("Invalid price:",sprice);return(false);}
//--- преобразования
   int account=StrToInteger(saccount);
   string symbol=ssymbol;
   int type=String2Type(stype);
   int entry=String2Entry(sentry);
   double volume= GetLotSize(StrToDouble(svolume),symbol);
   double price = NormalizeDouble(StrToDouble(sprice),(int)MarketInfo(ssymbol,MODE_DIGITS));
   Print("DEAL[",account,"|",symbol,"|",Type2String(type),"|",Entry2String(entry),"|",volume,"|",price,"]");
//--- исполнение
   ProcessOrder(account,symbol,type,entry,volume,price);
   return(true);
  }

Поскольку не у всех на счете есть 10 000$, при помощи функции GetLotSize() производится перерасчет торгового объема на клиентской части. Торговая стратегия, работающая на стороне сервера, может использовать свою систему управления капиталом, поэтому то же самое нужно делать и на стороне клиента.

Я предлагаю использовать "Lot mapping" - пользователь может указать границы возможных торговых объемов (минимальное и максимальное значения) и скрипт на клиентской части произведет автоматическое преобразование:

extern string _1="--- LOT MAPPING ---";
extern double  InpMinLocalLotSize=0.01; // Границы отображения объема
extern double  InpMaxLocalLotSize=1.00; // Рекомендуется установить большее значение
extern double  InpMinRemoteLotSize =      0.01;
extern double  InpMaxRemoteLotSize =      15.00;
//+------------------------------------------------------------------+
//| Преобразование торгового объема                                  |
//+------------------------------------------------------------------+
double GetLotSize(string remote_lots,string symbol)
  {
   double dRemoteLots=StrToDouble(remote_lots);
   double dLocalLotDifference=InpMaxLocalLotSize-InpMinLocalLotSize;
   double dRemoteLotDifference=InpMaxRemoteLotSize-InpMinRemoteLotSize;
   double dLots=dLocalLotDifference *(dRemoteLots/dRemoteLotDifference);
   double dMinLotSize=MarketInfo(symbol,MODE_MINLOT);
   if(dLots<dMinLotSize)
      dLots=dMinLotSize;
   return(NormalizeDouble(dLots,InpVolumePrecision));
  }

На стороне клиента поддерживаются брокеры с 4 и 5-значными котировками, также поддерживаются объемы "regular-lot" (0.1) и "mini-lot" (0.01). В этом была причина введения нового типа сделки DEAL_OUTALL.

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

//+------------------------------------------------------------------+
//| Обработка ордера (данные сконвертированы и проверены)            |
//+------------------------------------------------------------------+
void ProcessOrder(int account,string symbol,int type,int entry,double volume,double price)
  {
   if(entry==OP_IN)
     {
      DealIN(symbol,type,volume,price,0,0,account);
     }
   else if(entry==OP_OUT)
     {
      DealOUT(symbol,type,volume,price,0,0,account);
     }
   else if(entry==OP_INOUT)
     {
      DealOUT_ALL(symbol,type,account);
      DealIN(symbol,type,volume,price,0,0,account);
     }
   else if(entry==OP_OUTALL)
     {
      DealOUT_ALL(symbol,type,account);
     }
  }

5.3. Позиции MetaTrader 5 vs Ордера MetaTrader 4

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

Каждая новая сделка "IN" является новым ордером, затем следует сделка "OUT" , функционал закрытия реализован в 3 шага

  1. Перебрать все открытые ордера и закрыть те из них, которые соответствуют объему сделки, в случае если таковых нет, то выполнить пункт 2;
  2. Перебрать все открытые ордера и закрыть те из них, объем которых меньше, чем указанный объем OUT, если после этого остались незакрытые ордера, то выполнить пункт 3;
  3. Закрыть ордер, объем которого больше, чем запрошенный объем и затем открыть новый ордер с объемом, который должен остаться. В обычных случаях до этого шага доходить не должны, он сделан с целью защиты.
//+------------------------------------------------------------------+
//| Обработка DEAL ENTRY OUT                                         |
//+------------------------------------------------------------------+
void DealOUT(string symbol, int cmd, double volume, double price, double stoploss, double takeprofit, int account)
{
   int type = -1;
   int i=0;
   
   if(cmd==OP_SELL)
      type = OP_BUY;
   else if(cmd==OP_BUY)
      type = OP_SELL;  
   
   string comment = "OUT."+Type2String(cmd);
   //--- поиск ордеров с объемом, равным заданному и прибылью > 0
   for(i=0;i<OrdersTotal();i++)
   {
      if(OrderSelect(i,SELECT_BY_POS))
      {
         if(OrderMagicNumber()==account)
         {
            if(OrderSymbol()==symbol)
            {
               if(OrderType()==type)
               {
                  if(OrderLots()==volume)
                  {
                     if(OrderProfit()>0)
                     {
                        if(CloseOneOrder(OrderTicket(), symbol, type, volume))
                        {
                           Print("Order with exact volume and profit>0 found and executed.");
                           return;
                        }
                     }
                  }
               }
            }
         }
      }
   }
   //--- поиск ордеров с объемом, равным заданному с любой прибылью
   for(i=0;i<OrdersTotal();i++)
   {
      if(OrderSelect(i,SELECT_BY_POS))
      {
         if(OrderMagicNumber()==account)
         {
            if(OrderSymbol()==symbol)
            {
               if(OrderType()==type)
               {
                  if(OrderLots()==volume)
                  {
                     if(CloseOneOrder(OrderTicket(), symbol, type, volume))
                     {
                        Print("Order with exact volume found and executed.");
                        return;
                     }
                  }
               }
            }
         }
      }
   }
   double volume_to_clear = volume;
   //--- поиск ордеров с объемом, меньшим чем заданный и прибылью > 0
   int limit = OrdersTotal();
   for(i=0;i<limit;i++)
   {
      if(OrderSelect(i,SELECT_BY_POS))
      {
         if(OrderMagicNumber()==account)
         {
            if(OrderSymbol()==symbol)
            {
               if(OrderType()==type)
               {
                  if(OrderLots()<=volume_to_clear)
                  {
                     if(OrderProfit()>0)
                     {
                        if(CloseOneOrder(OrderTicket(), symbol, type, OrderLots()))
                        {
                           Print("Order with smaller volume and profit>0 found and executed.");
                           volume_to_clear-=OrderLots();
                           if(volume_to_clear==0)
                           {
                              Print("All necessary volume is closed.");
                              return;
                           }
                           limit = OrdersTotal();
                           i = -1;
                        }
                     }
                  }
               }
            }
         }
      }
   }
   //--- поиск ордеров с объемом, меньшим чем заданный
   limit = OrdersTotal();
   for(i=0;i<limit;i++)
   {
      if(OrderSelect(i,SELECT_BY_POS))
      {
         if(OrderMagicNumber()==account)
         {
            if(OrderSymbol()==symbol)
            {
               if(OrderType()==type)
               {
                  if(OrderLots()<=volume_to_clear)
                  {
                     if(CloseOneOrder(OrderTicket(), symbol, type, OrderLots()))
                     {
                        Print("Order with smaller volume found and executed.");
                        volume_to_clear-=OrderLots();
                        if(volume_to_clear==0)
                        {
                           Print("All necessary volume is closed.");
                           return;
                        }
                        limit = OrdersTotal();
                        //--- поскольку значение будет увеличено и в конце цикла будет равно 0.
                        i = -1; 
                     }
                  }
               }
            }
         }
      }
   }
   //--- поиск ордеров с объемом, большим чем заданный
   for(i=0;i<OrdersTotal();i++)
   {
      if(OrderSelect(i,SELECT_BY_POS))
      {
         if(OrderMagicNumber()==account)
         {
            if(OrderSymbol()==symbol)
            {
               if(OrderType()==type)
               {
                  if(OrderLots()>=volume_to_clear)
                  {
                     if(CloseOneOrder(OrderTicket(), symbol, type, OrderLots()))
                     {
                        Print("Order with smaller volume found and executed.");
                        volume_to_clear-=OrderLots();
                        if(volume_to_clear<0)//Closed too much
                        {
                           //--- открыть новый ордер
                           DealIN(symbol,type,volume_to_clear,price,OrderStopLoss(),OrderTakeProfit(),account);
                        }
                        else if(volume_to_clear==0)
                        {
                           Print("All necessary volume is closed.");
                           return;
                        }
                     }
                  }
               }
            }
         }
      }
   }
   if(volume_to_clear!=0)
   {
      Print("Some volume left unclosed: ",volume_to_clear);
   }
}

Выводы

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

Предложенное решение достаточно хорошо работает и может быть использовано как для копирования сигналов ваших собственных стратегий, так и для копирования сделок участников Automated Trading Championship. Производительность и возможности, предоставляемые языками MQL4 и MQL5 достаточны для профессионального и коммерческого использования. Я считаю, что можно сделать очень хороший поставщик торговых сигналов для всех клиентов MetaTrader 4 и MetaTrader 5, используя лишь свой компьютер и собственную стратегию.

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

Примечание автора (Tsaktuo):

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


Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/344

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
Automated-Trading
Automated-Trading | 21 нояб. 2011 в 15:59

Обновлен код DealClient: tsaktuodealclient.mq4  (см. комментарий)

Evgeniy Trofimov
Evgeniy Trofimov | 25 дек. 2011 в 17:36
Огромное спасибо за предоставленный код и dll. Пытаюсь написать серверную часть для mql4. Фрагмент кода, где происходит конвертации котировок вывела меня из строя, но я так просто не сдамся :)
Evgeniy Trofimov
Evgeniy Trofimov | 28 дек. 2011 в 10:40

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

2011.12.28 16:34:54 Client EURUSD,M15: Received(20): ????????????????8???

Как можно исправить эту ошибку? Отправляю строку: <EURUSD;1.30675;1.30667> 

Интервью с Гэ Сэньлинем (ATC 2011) Интервью с Гэ Сэньлинем (ATC 2011)
Советник Гэ Сэньлиня (yyy999) из Китая появился в первой десятке турнирной таблицы Automated Trading Championship 2011 в конце октября и с тех пор не покидал ее. Не часто участники из Поднебесной добиваются успехов на Чемпионате, ведь торговля на Форексе запрещена в этой стране. После неудачного выступления в прошлом году, Гэ Сэньлинь подготовил новый мультивалютный советник, который не закрывает убыточные позиции и использует наращивание позиции для выхода из минуса. Что ж, посмотрим, сможет ли этот эксперт подняться еще выше с такой рискованной стратегией.
Стратегия "Всё или Ничего" на Форексе Стратегия "Всё или Ничего" на Форексе
Цель данной статьи - создание максимально простой торговой стратегии, реализующей игровой принцип "Всё или Ничего". Задача создания прибыльного советника не ставится, цель - увеличение начального депозита в несколько раз с максимально возможной вероятностью. Возможно ли, ничего не зная о техническом анализе и не используя никаких индикаторов, использовать Форекс для получения большого выигрыша против вероятности всё потерять?
Разработка эксперта средствами UML Разработка эксперта средствами UML
В статье рассматривается создание торгового советника с помощью графического языка UML, который используется для визуального моделирования объектно-ориентированных программных систем. Основным преимуществом такого подхода является визуализация работы проектировщика. Приведен пример проектирования структуры и свойств советника при помощи программы Software Ideas Modeler.
Эконометрика: прогноз EURUSD на один шаг вперед Эконометрика: прогноз EURUSD на один шаг вперед
Статья посвящена реализации прогнозирования движения валютной пары EURUSD на один шаг вперед с помощью пакета EViews с последующей оценкой результатов прогнозирования с помощью программ на EViews. Прогнозирование осуществляется при помощи регрессионных моделей, для проверки корректности прогноза разработан советник для MetaTrader 4.