English Русский 中文 Español Deutsch 日本語 Português Français Italiano Türkçe
MetaTrader 5를MetaTrader4 시그널 프로바이더로 활용하기

MetaTrader 5를MetaTrader4 시그널 프로바이더로 활용하기

MetaTrader 5 | 11 10월 2021, 15:58
72 0
Karlis Balcers
Karlis Balcers

개요

여러 이유로 이 글을 작성하고 싶었어요. 실제로 실행 가능한 일인지 확인하고 싶기도 했고요.

첫째로, MetaTrader 5가 출시한 지는 꽤 되었지만 아직 실제 거래에 적용할 수가 없죠. 이미 MQL5로 효과적인 전략을 만들어 당장 사용하고 싶어하는 분들도 계실 겁니다. 아니면 수동 거래를 하더라도MetaTrader4 대신MetaTrader5를 이용하고 싶으신 분들도 계시겠죠.

둘째는 자동 매매 챔피언십 기간 동안 모두가 실제 계좌로 선두를 달리는 참가자들을 따라해 보고 싶어하기 때문입니다. 이미 거래를 따라할 방법을 마련하신 분들도 있겠지만 여전히 어떻게 해야 할지, 어떻게 하면 챔피언십 참가자들과 비슷한 실적을 낼지, 어떻게 해야 동일한 자금 관리 시스템을 적용할 수 있는지를 궁금해 하는 사람들이 많습니다.

셋째로, 자신이 사용하는 매매 신호를 다른 사람들에게도 알려주고 싶어 하는 사람들이 있기 때문이죠. 그러려면 실적에 영향을 미치지 않으면서 다중 접속을 통해 실시간으로 신호를 공유할 방법이 필요할 겁니다.

이런 이유들을 염두에 두고 해결책을 마련할 수 있을지 생각해 봤어요.


1. MQL5 챔피언십 따라하기

최근 MQL5.community에서 제 수준에 맞는 몇 개의 글을 보고 저도 EA를 만들 수 있겠구나 싶더군요. 제 실제 계좌로 챔피언십 참가자들의 거래를 따라가는 중이기도 합니다. 다행히 수익을 내고 있죠. 문제는 데이터가 5분 간격으로 업데이트되기 때문에 포지션 오픈 및 청산 기회를 놓칠 수도 있다는 거죠.

챔피언십 포럼을 보니 저랑 비슷한 분들이 계시더라고요. 이는 그다지 효과적인 방법이 아닌데다가 트래픽량까지 증가시키니 주최 측에서는 별로 안 좋아할 겁니다. 어떻게 하면 문제를 해결할 수 있을까요? 열심히 생각해 봤는데MetaTrader5의 '투자자' 모드(거래 불가)로 참가자의 계좌에 액세스하는 방법이 제일 마음에 들더라고요.

이렇게 하면 거래 활동에 대한 모든 정보를 실시간으로 주고 받을 수 있을까요? 해답을 찾기 위해 엑스퍼트 어드바이저를 생성하고 투자자 모드의 계정으로 실행해 보았습니다. 놀랍게도 실행이 될뿐만 아니라 포지션, 주문, 그리고 거래에 대한 정보를 얻을 수 있더군요. 문제 해결의 시작이 되는 정보들이죠!


2. 포지션, 주문 혹은 거래 추적하기

MetaTrader 5에서MetaTrader4로 정보를 전송하려면MetaTrader4의 주문 유형을 잘 알아야 합니다. 그리고 계좌 내 거래와 관련된 모든 정보를 추적할 수 있어야 하겠죠. 따라서 포지션만으로는 우리가 원하는 정보를 얻을 수 없습니다. 매초마다 포지션의 변화를 비교할 게 아니면요.

그러니까 주문이나 거래를 추적하는 편이 낫겠죠?

우선 주문을 살펴봤어요.

주문

거래보다 먼저 실행되는 점이 좋았어요. 대기 주문(지정가 주문)에 대한 정보를 포함하는 것도 좋았고요. 다만 거래에는 있는 한 가지가 없었죠. 바로 열거형 입력 형식(ENUM_DEAL_ENTRY)말입니다.

거래

DEAL_ENTRY_TYPE 열거형을 이용하면 거래처럼 병렬 연산이 필요하지 않죠. 제일 좋은 방법은 주문과 거래를 합치는 겁니다. 그러면 대기 주문에 대한 정보를 얻는 동시에 계좌에서 일어나는 모든 변화를 좇을 수 있죠. 브로커마다 가격 움직임이 다르므로 대기 주문이 부정확한 결과를 초래할 수도 있습니다.

거래만 추적하는 경우 대기 주문은 실행할 수 있지만 인터넷 연결 상태에 따라 약간의 지연이 발생할 수 있죠. 속도(대기 주문)와 실적(거래) 중에서 저는 실적(거래)을 선택하기로 했습니다.


3. 시그널 제공하기

MetaTrader 5의 데이터를 다른 애플리케이션이나 컴퓨터로 옮기는 방법에 대해서는 이미 많은 글이 올라와 있습니다. 서로 다른 컴퓨터를 사용하는 클라이언트와 연결되어야 하므로 TCP 소켓 통신을 이용하겠습니다.

MQL5에서는 API 함수를 사용할 수 없으므로 외부 라이브러리가 필요합니다. WinInet.dll 라이브러리에 대한 많은 글('WinInet.dll로 온라인 터미널 데이터 교환하기' 등)이 있지만 우리가 원하는 내용은 없네요.

다행히 C#을 조금 알기 때문에 직접 라이브러리를 생성하기로 했습니다. 'Unmanaged Exports로 MQL5에 C# 코드 첨부하기'를 참고하여 호환성 문제를 해결했고요. 동시에 최대 500대의 클라이언트가 접속 가능한 매우 간단한 인터페이스의 서버를 생성했습니다. (.NET framework 3.5 이상이 필요하나 대부분의 경우 PC에 'Microsoft .NET Framework 3.5'로 설치).

#import "SocketServer.dll"    // Library created on C# (created by using information available on https://www.mql5.com/ko/articles/249)
string About();            // Information about library.
int SendToAll(string msg);  // Sends one text message to all clients.
bool Stop();               // Stops the server.
bool StartListen(int port); // Starts the server. Server will listen from incomming connections (max 500 clients). 
                               // All clients are built on Assync threads.
string ReadLogLine();       // Retrieve one log line from server (can contain erros and other information). 
                               // Reading is optional. Server stores only last 100 lines.
#import

서버는 백그라운드의 다른 쓰레드에서 실행되므로 클라이언트 수가 많아도MetaTrader5 작업 속도는 영향을 받지 않습니다.

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#의 비동기 서버 소켓에 대한 더 많은 정보는 마이크로소프트 MSDN 또는 구글에 검색해 보세요.


4. 시그널 수집하기

새로운 틱이 생길 때마다가 아닌 매 순간의 정보가 필요합니다. 따라서MetaTrader4에서는 엑스퍼트 어드바이저 대신 스크립트를 이용하죠. 또한 시그널 프로바이더인MetaTrader5를 연결하는 소켓이 필요합니다.

MQL4 코드 베이스 'https://www.mql5.com/ko/code/9296'에서 도움을 받았습니다. 해당 글의 첨부 파일(WinSock.mqh) 덕분에 굉장히 간단한 방법으로 소켓을 이용할 수 있었어요. 안정성을 문제 삼는 분들도 계시지만 제가 쓰기에는 충분했습니다. 테스트 중 문제도 없었고요.

#include <winsock.mqh>  // Downloaded from MQ4 homepage
                     // DOWNLOAD:   http://codebase.mql4.com/download/18644
                     // ARTICLE:    http://codebase.mql4.com/6122


5. 데이터 프로세싱

이제 모든 거래 내역이 클라이언트가 인식 가능한 포맷으로 전송되도록 만들 차례입니다.

5.1. 서버 측

앞서 언급했다시피 우리가 만들 엑스퍼트 어드바이저는 통화쌍과는 관련이 없습니다.

연결될 스레드의 영향을 받기도 합니다.

int OnInit()
  {
   string str="";
   Print(UTF8_to_ASCII(About()));
//--- start the server
   Print("Starting server on port ",InpPort,"...");
   if(!StartListen(InpPort))
     {
      PrintLogs();
      Print("OnInit() - FAILED");
      return -1;
     }

이 경우 엑스퍼트 어드바이저는 연결된 클라이언트의 영향을 받지 않습니다. 거래가 발생할 때마다 클라이언트에게 알림을 전송하죠. 거래에 대한 정보만 알면 되므로 OnTick() 함수는 제거하고 OnTrade() 함수만을 사용합니다. 해당 함수는 최신 기록을 확인하여 해당 거래에 대한 알림 여부를 결정합니다.

코드 내 주석에 보다 자세한 설명을 첨부했습니다.

//+------------------------------------------------------------------+
//| OnTrade() - every time when there is an activity related to      |
//|             traiding.                                            |
//+------------------------------------------------------------------+
void OnTrade()
  {
//--- find all new deals and report them to all connected clients
//--- 24 hours back.
   datetime dtStart=TimeCurrent()-60*60*24;
//--- 24 hours front (in case if you live in GMT-<hours>)
   datetime dtEnd=TimeCurrent()+60*60*24;
//--- select history from last 24 hours.
   if(HistorySelect(dtStart,dtEnd))
     {
      //--- go through all deals (from oldest to newest).
      for(int i=0;i<HistoryDealsTotal();i++)
        {
         //--- get deal ticket.
         ulong ticket=HistoryDealGetTicket(i);
         //--- if this deal is interesting for us.
         if(HistoryDealGetInteger(ticket,DEAL_ENTRY)!=DEAL_ENTRY_STATE)
           {
            //Print("Entry type ok.");
            //--- check if this deal is newer than previously reported one.
            if(HistoryDealGetInteger(ticket,DEAL_TIME)>g_dtLastDealTime)
              {
               //--- if some part of position has been closed then check if we need to enable it
               if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_OUT)
                 {
                  vUpdateEnabledSymbols();
                 }
               //--- if opposite position is opened, then we need to enable disabled symbol.
               else if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT)
                 {
                  //--- enable this specific symbol.
                  vEnableSymbol(HistoryDealGetString(ticket,DEAL_SYMBOL));
                 }
               //--- check if symbol is enabled.
               if(bIsThisSymbolEnabled(HistoryDealGetString(ticket,DEAL_SYMBOL)))
                 {
                  //--- build deal-string and send to all connected clients
                  int cnt=SendToAll(sBuildDealString(ticket));
                  //--- technical error with server.
                  if(cnt<0)
                    {
                     Print("Failed to send new deals!");
                    }
                  //--- if sent to no one (cnt==0) or if sent to someone (cnt>0)                  
                  else
                    {
                     //--- update datetime for last sucessfully transfered deal
                     g_dtLastDealTime=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
                    }
                 }
               //--- do not notify becayse symbol is disabled.
               else
                 {
                  //--- update datetime for last deal, we will not notify about.
                  g_dtLastDealTime=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
                 }
              }
           }
        }
     }
  }

새로운 거래가 감지되면 BuildDealString() 함수를 호출해 데이터 전송을 준비합니다. 모든 데이터는 텍스트 형식으로 전송되며 각 거래는 '<'로 시작해 '>'로 끝납니다.

TCP/IP 프로토콜을 이용하면 한번에 두 개 이상의 거래를 수신할 수 있으므로 기호를 이용해 거래를 구분하는 거죠.

//+------------------------------------------------------------------+
//| This function builds deal string                                 |
//| Examples:                                                        |
//| 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;
//--- find deal volume.
//--- if this is INOUT then volume must contain ONLY volume of 'IN'.
   if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT)
     {
      if(PositionSelect(HistoryDealGetString(ticket,DEAL_SYMBOL)))
        {
         volume=PositionGetDouble(POSITION_VOLUME);
        }
      else
        {
         Print("Failed to get volume!");
        }
     }
//--- if it's 'IN' or 'OUT' deal then use it's volume as is.
   else
     {
      volume=HistoryDealGetDouble(ticket,DEAL_VOLUME);
     }
//--- build deal string(format sample: "<EURUSD;BUY;IN;0.01;1.37294>").
   int iDealEntry=(int)HistoryDealGetInteger(ticket,DEAL_ENTRY);
//--- if this is OUT deal, and there are no open positions left.
   if(iDealEntry==DEAL_ENTRY_OUT && !PositionSelect(HistoryDealGetString(ticket,DEAL_SYMBOL)))
     {
      //--- For safety reasons, we check if there is any position left with current symbol. If NO, then let's use 
      //--- new deal type - OUTALL. This will guarante that there are no open orders left on or account when all
      //--- position has been closed on 'remote' MetaTrader 5 side. This can happen due to fact, that volume is 
      //--- is mapped to new values on client side, therefor there can be some very small difference which leaves
      //--- order open with very small lot size. 
      iDealEntry=DEAL_ENTRY_OUTALL;  // My own predefined value (this value should not colide with EMUN_DEAL_ENTRY values).
     }
   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 형식인 DEAL_ENTRY_OUTALL을 발견하고 놀라실 수도 있겠는데요. 제가 직접 만든 겁니다.MetaTrader4 거래량 핸들링에 대해 이야기하면서 추가적으로 설명하도록 할게요.

OnTimer() 함수도 꽤 function. 초기화 시 EventSetTimer(1)를 호출해 매 초마다 OnTimer() 함수를 호출할 수 있도록 했습니다. 해당 함수 내부에는 서버 라이브러리로부터 프린팅된 한 줄의 로그가 포함되어 있습니다.

//+------------------------------------------------------------------+
//| Print logs from Server every second (if there are any)           |
//+------------------------------------------------------------------+
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 parameters
input ENUM_STARTUP_TYPE InpStartupType=STARTUP_TYPE_CONTINUE; // Startup type

시그널 프로바이더가 이미 포지션이 오픈된 계정에 추가될 수 있기 때문에 발생할 수 있는 클라이언트 측의 혼동을 방지하려는 목적을 갖습니다. 해당 매개 변수 설정을 통해 기존 모든 거래에 대한 정보 또는 새로운 포지션에 대한 정보만을 얻을지를 선택할 수 있습니다.

기존의 계좌나 컴퓨터를 재시작한 경우, 또는 코드를 수정한 경우에는 재설정이 필요합니다.


5.2. 클라이언트

클라이언트 측에는 소켓으로부터 자료를 수신하는 함수인 recv()가 있죠. 서버에서 자료가 수신될 때까지 해당 함수는 실행되지 않으므로 프로세싱 지연은 걱정하지 않아도 됩니다.

//--- server up and running. Start data collection and processing
   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);

다만 이는 클라이언트가 멈추는 원인이 됩니다. '스크립트 삭제'를 클릭해도 스크립트가 삭제되지 않을 겁니다. 두 번을 클릭해야 타임아웃으로 스크립트가 제거되죠. 데이터 수신 함수에 타임아웃을 적용하면 해결될 문제이지만 저는 코드베이스의 샘플을 이용 중이니 그대로 두겠습니다. 

데이터가 수신되면 실제 계좌에 거래를 적용하기에 앞서 데이터를 분리하고 검증합니다.

         //--- split records
         string arrDeals[];
         //--- split raw data in multiple deals (in case if more than one is received).
         int iDealsReceived=Split(sRawData,"<",10,arrDeals);
         Print("Found ",iDealsReceived," deal orders.");
         //--- process each record
         //--- go through all DEALs received
         for(int j=0;j<iDealsReceived;j++) 
           {
            //--- split each record to values
            string arrValues[];
            //--- split each DEAL in to values
            int iValuesInDeal=Split(arrDeals[j],";",10,arrValues);
            //--- verify if DEAL request received in correct format (with correct count of values)
            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)
                 {
                  //--- it might be incompleate beginning of next deal.
                  sLeftOver=arrDeals[j];
                 }
              }
           }
//+------------------------------------------------------------------+
//| Processing received raw data (text format)                       |
//+------------------------------------------------------------------+
bool ProcessOrderRaw(string saccount,string ssymbol,string stype,string sentry,string svolume,string sprice)
  {
//--- clearing
   saccount= Trim(saccount);
   ssymbol = Trim(ssymbol);
   stype=Trim(stype);
   sentry=Trim(sentry);
   svolume= Trim(svolume);
   sprice = Trim(sprice);
//--- validations
   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);}
//--- convertations
   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,"]");
//--- execution
   ProcessOrder(account,symbol,type,entry,volume,price);
   return(true);
  }

모두가 동일한 예수금을 갖고 있지 않으므로 GetLotSize() 함수를 이용해 랏 크기가 결정됩니다. 서버 측에서 실행되는 전략이 자금 관리 시스템을 포함할 수도 있으며 이는 클라이언트 측에도 동일하게 적용되어야 합니다.

'랏 매핑'이라는 걸 한번 사용해 볼게요. 클라이언트 사용자가 최소 및 최대 랏 사이즈를 설정하면 클라이언트 스크립트가 매핑을 실행합니다.

extern string _1 = "--- LOT MAPPING ---";
extern double  InpMinLocalLotSize  =  0.01;
extern double  InpMaxLocalLotSize  =  1.00; // Recomended bigger than
extern double  InpMinRemoteLotSize =  0.01;
extern double  InpMaxRemoteLotSize =  15.00;
//+------------------------------------------------------------------+
//| Calculate lot size                                               |
//+------------------------------------------------------------------+
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자리 브로커를 지원하며 일반 랏(0.1)과 미니 랏(0.01)을 지원합니다. 그렇기 때문에 새로운 DEAL_ENTRY 형식인 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.MetaTrader5 포지션 vsMetaTrader4 주문

EA를 구현하다 또 다른 문제를 발견했습니다.MetaTrader5에는 각 통화쌍 별로 단 하나의 포지션만 오픈할 수 있더군요.MetaTrader4에서는 완전히 다른 방식이 적용되죠. 최대한 비슷한 조건을 형성하기 위해MetaTrader4에 여러 개의 주문을 설정합니다.

각각의 'IN' 거래는 새 주문이 되고 'OUT' 거래의 경우 다음 세 단계를 따르도록 했습니다.

  1. 모든 주문 확인 후 요청된 사이즈와 동일한 주문 청산
  2. 위에 해당하는 주문이 없는 경우 요청된 'OUT' 거래보다 거래량이 적은 주문 청산
  3. 다른 주문이 남은 경우 요청된 사이즈보다 큰 주문을 청산하고 알맞은 크기의 주문 오픈 보통의 경우 세 번째 단계가 필요할 일은 없습니다. 예치금 보호 목적으로 만든 단계예요.
//+------------------------------------------------------------------+
//| Process 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);
   //--- Search for orders with equal VOLUME size and with PROFIT > 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;
                        }
                     }
                  }
               }
            }
         }
      }
   }
   //--- Search for orders with equal VOLUME size and with ANY profit size
   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;
   //--- Search for orders with smaller volume AND with PROFIT > 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; // Because it will be increased at end of cycle and will have value 0.
                        }
                     }
                  }
               }
            }
         }
      }
   }
   //--- Search for orders with smaller volume
   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();
                        i = -1; // Because it will be increased at end of cycle and will have value 0.
                     }
                  }
               }
            }
         }
      }
   }
   //--- Search for orders with higher volume
   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
                        {
                           //Open new to compensate lose
                           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);
   }
}

결론

첨부된 파일은 클라이언트 서버 프로토콜 수정 등을 통해 개선될 수 있습니다. 하지만 제 목적은 우리가 실제로 사용 가능할 정도로만 만드는 것이었으니까요.

MQL5 챔피언십 참가자들의 전략이나 여러분의 전략에 적용하기에는 충분합니다. MQL4와 MQL5의 기능은 전문적이거나 또는 상업적으로 이용할 수 있을 정도로 뛰어납니다. 여러분도 직접 여러분만의 전략을 이용하여MetaTrader4와MetaTrader5 클라이언트에 적용 가능한 훌륭한 시그널 프로바이더를 만드실 수 있을 겁니다.

제가 쓴 코드를 보완해서 제게 피드백을 주셨으면 좋겠네요. 질문이 있으시면 최대한 답을 드리려고 하겠습니다. 지금 저는 제가 좋아하는 챔피언십 참가자들의 전략으로 테스트를 하는 중인데요. 이제 일주일 정도 됐습니다. 문제가 보이면 여기에 알릴게요.

Tsaktuo

해당 EA를 귀하의 실제 계정에 적용할 경우 모든 손실에 대한 책임은 귀하에게 있습니다. 반드시 본문을 정독하고 테스트 단계를 거친 후에 실제 계정으로 거래를 실행하십시오.


MetaQuotes 소프트웨어 사를 통해 영어가 번역됨
원본 기고글: https://www.mql5.com/en/articles/344

다중 선형 회귀 분석 올인원 전략 생성기와 전략 테스터 다중 선형 회귀 분석 올인원 전략 생성기와 전략 테스터
이번 글에서는 매매 시스템 개발에 여러 방식으로 다중 선형 회귀 분석을 적용하는 방법을 다룹니다. 자동 전략 검색에 회귀 분석을 이용하는 방법을 알아보겠습니다. 프로그래밍에 대한 이해도가 높지 않아도 이용할 수 있는 회귀 방정식이 포함된 EA를 예로 들겠습니다.
MQL5-RPC. MQL5에서의 원격 프로시져 호출(Remote Procedure Call): 재미와 돈을 위한 웹 서비스 접속과 XML-RPC ATC 분석기 MQL5-RPC. MQL5에서의 원격 프로시져 호출(Remote Procedure Call): 재미와 돈을 위한 웹 서비스 접속과 XML-RPC ATC 분석기
본 문서에서는 MQL5에서 원격 프로시져 호출(Remote Procedure Call)을 활성화시켜주는 MQL5-RPC 프레임워크에 대해 알아볼 것입니다. 우선 XML-RPC 기초로 시작하여, MQL5 구현을 짚고, 그 뒤엔 실용 예제 두개를 다뤄볼 것입니다. 첫 예제는 간단한 외부 웹 서비스를 이용하는 것이고 두번째 예제는 간단한 XML-RPC ATC 2011 분석기 서비스 클라이언트입니다. 만약 리얼타임으로 ATC 2011의 각기 다른 통계 자료를 구현하고 분석하는지 보고싶으시다면 이 문서는 바로 당신을 위한 것입니다.
최초 구매 고객을 위한 팁 최초 구매 고객을 위한 팁
유명한 말이 있죠? '실패는 성공의 어머니다'라고. 반박하기 힘든 말입니다. 여러분 또는 타인의 과거의 실패를 분석해서 미래의 실패를 최소화할 수 있죠. 구인 서비스를 이용할 때 일어날 수 있는 여러 상황에 대해 알아보겠습니다.
EA 트리를 이용하여 MQL5 Expert Advisor 뚝딱 만들기: 1부 EA 트리를 이용하여 MQL5 Expert Advisor 뚝딱 만들기: 1부
EA Tree는 최초의 드래그 앤 드랍 MetaTrader MQL5 Expert Advisor 생성기입니다. 매우 사용하기 편리한 GUI를 이용하여 복잡한 MQL5도 만들 수 있습니다. EA 트리에서는 박스들을 서로 연결하는 것으로 Expert Advisor를 만들 수 있습니다. 각 박스에는 MQL5 함수, 기술 인디케이터, 커스텀 인디케이터, 혹은 값이 들어있을 수 있습니다. "박스 트리"를 이용하여 EA 트리는 Expert Advisor MQL5 코드를 생성합니다.