English Русский 中文 Español Deutsch 日本語
Arbitragem triangular

Arbitragem triangular

MetaTrader 5Negociação | 23 novembro 2017, 06:23
7 231 5
Alexey Oreshkin
Alexey Oreshkin

Descrição da ideia

A arbitragem triangular é um tópico que com frequência invejável é abordado nos fóruns de discussão na web. De que se trata?

A palavra "arbitragem" neste âmbito implica uma certa neutralidade no mercado. "Triangular" se refere ao fato de o portfólio ser composto por três instrumentos.

Tomemos o exemplo mais popular, isto é, o triângulo "euro - libra - dólar. Ele é descrito em pares de moedas assim: EURUSD + GBPUSD + EURGBP. A neutralidade exigida consiste em tentar comprar e vender ao mesmo tempo os mesmos instrumentos, ganhando lucro. 

Isto tem a seguinte aparência. Apresentamos qualquer par deste exemplo por meio dos dois restantes:

EURUSD=GBPUSD*EURGBP~,

ou GBPUSD=EURUSD/EURGBP,

ou EURGBP=EURUSD/GBPUSD.

Todas estas variantes são idênticas, no entanto abaixo vamos dar uma olhada na escolha de qualquer uma delas. Por ora, vamos nos concentrar na primeira variante.

Agora tentemos nos aperceber dos preços bid e ask. O processo será como se segue:

  1. Compramos EURUSD, ou seja, usamos o preço ask. No saldo, temos mais euros e menos dólares. 
  2. Expressamos o EURUSD por meio dos outros dois pares.
  3. GBPUSD: aqui não há euro, mas dólar sim (devemos vender os dólares). Para vender dólares em GBPUSD, é preciso comprar este par. Então, usamos ask. Ao comprar, temos no saldo mais libras e menos dólares.
  4. EURGBP: necessitamos comprar euros, enquanto vendemos a libra, que não precisamos. Compramos EURGBP, usamos ask. No saldo, temos mais euros e menos libras. Bem, está tudo certo por aqui.

No total, temos: (ask) EURUSD = (ask) GBPUSD * (ask) EURGBP. Nós obtivemos a igualdade necessária. Agora, para ganhar nela, temos que comprar um lado e vender o outro. Aqui há duas possibilidades:

  1. Comprar EURUSD mais barato do que podemos vendê-lo, mas expresso de uma forma diferente: (ask) EURUSD < (bid) GBPUSD * (bid) EURGBP 
  2. Vender EURUSD mais caro do que podemos comprá-lo, mas expresso de uma forma diferente: (bid) EURUSD > (ask) GBPUSD * (ask) EURGBP 

Só resta uma coisa pequena, isto é, encontrar uma tal situação e fazer dinheiro com isso.

Chamo a atenção para a hipótese de formar o triângulo de uma outra maneira, movendo todos os três pares em uma direção e comparando a 1. Todas as variantes são idênticas, mas a acima mostrada, na minha opinião, é mais facilmente compreendida e explicada.

E mais uma observação importante: acompanhando esta situação podemos descobrir o momento de compra e venda simultânea. Neste caso, o lucro será imediato, mas esses momentos são muitíssimos raros.
Com um pouco mais de frequência costuma acontecer que podemos comprar um lado mais barato do que o outro, mas não se pode vender com lucro. Então, aguardamos que esse desequilíbrio desapareça. Estamos seguros estando no trade, porque a nossa posição é quase zero, ou seja, estamos fora do mercado. Porém, você tem que entender de onde vem esse quase. Para uma ideal regularização dos volumes de negociação, é necessária uma exatidão que não está à nossa disposição. Lembro-lhe que geralmente os volumes de negociação são arredondados para 2 casas decimais, e, para nossa estratégia, isso é um arredondamento sem precisão.

Já aprofundamos a teoria, é o momento de programar o robô. O expert advisor é escrito em um estilo processual, portanto, é claro para os programadores novatos e aqueles que por qualquer motivo não gostam da programação orientada a objetos 


Breve descrição do robô

Primeiro, criamos todos os triângulos possíveis, organizamo-los corretamente e obtemos todos os dados necessários para cada par de moedas.

Todas essas informações são armazenadas na matriz de estruturas MxThree. Cada triângulo tem um campo status. Seu valor inicial = 0. Se for necessário abrir o triângulo, ao status será atribuído o valor = 1. Depois de confirmar que o triângulo foi completamente aberto, o seu status é alterado para 2. Se o triângulo não se abrir completamente ou já estiver na hora de ser fechado, o status é alterado para 3. Uma vez que o triângulo fecha com sucesso, o status retorna para a posição 0.

O robô grava a abertura e fechamento dos triângulos em um arquivo de log para validar a correção da ação e restaurar o histórico. Nome do arquivo de log: Three Point Arbitrage Control YYYY.DD.MM.csv.

Para testar, carregue no Testador todos os pares de moedas necessários. Para fazer isso, antes de iniciar o Testador, é necessário executar o robô no modo Create file with symbols. Si não estiver este arquivo, o robô desencadeia o teste com base no testador padrão EUR+GBP+USD. 


Variáveis utilizadas

O código de qualquer robô, quando eu trabalho, começa com a inclusão do arquivo de cabeçalho. Ele lista todos os incodes, bibliotecas, etc. Este robô não é excepção: imediatamente após o bloco da descrição segue a linha #include "head.mqh" etc.:

#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>  
#include <Trade\TerminalInfo.mqh> 

#include "var.mqh"
#include "fnWarning.mqh"
#include "fnSetThree.mqh"
#include "fnSmbCheck.mqh"
#include "fnChangeThree.mqh"
#include "fnSmbLoad.mqh"
#include "fnCalcDelta.mqh"
#include "fnMagicGet.mqh"
#include "fnOpenCheck.mqh"
#include "fnCalcPL.mqh"
#include "fnCreateFileSymbols.mqh"
#include "fnControlFile.mqh"
#include "fnCloseThree.mqh"
#include "fnCloseCheck.mqh"
#include "fnCmnt.mqh"
#include "fnRestart.mqh"
#include "fnOpen.mqh"

Agora, esta lista não é totalmente clara para o leitor, mas o artigo está escrito no modo de acompanhamento de código, de forma que a estrutura do programa não é perturbada. Enquanto você lê tudo se torna claro. Por uma questão de comodidade, todas as funções, classes, unidades de código estão organizados em arquivos individuais. No meu caso, cada arquivo incluído, com exceção da biblioteca padrão, também começa com a linha #include "head.mqh". Isso permite usar IntelliSense nos arquivos incluídos e não manter em mente os nomes de todas as entidades necessárias.

Em seguida, conectamos o arquivo para o Testador. Em nenhum outro lugar pode-se fazer isso, portanto declaramo-lo aqui. Esta linha é necessária para carregar os símbolos no testador multimoeda:

#property tester_file FILENAME

Em seguida, descrevemos as variáveis ​​utilizadas no programa. Sua descrição também está contida em um arquivo separado var.mqh:

// macros
#define DEVIATION       3                                                                 // Derrapagem máxima possível
#define FILENAME        "Three Point Arbitrage.csv"                                       // Aqui é armazenado o símbolo de trabalho
#define FILELOG         "Three Point Arbitrage Control "                                  // Parte do nome do arquivo de log
#define FILEOPENWRITE(nm)  FileOpen(nm,FILE_UNICODE|FILE_WRITE|FILE_SHARE_READ|FILE_CSV)  // Abertura do arquivo para gravação
#define FILEOPENREAD(nm)   FileOpen(nm,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV)   // Abertura do arquivo para leitura
#define CF              1.2                                                               // Coeficiente crescente para a margem
#define MAGIC           200                                                               // Faixa de magics usados
#define MAXTIMEWAIT     3                                                                 // Tempo máximo de espera em segundos após a abertura do triângulo

// estrutura para o par de moedas
struct stSmb
   {
      string            name;            // Par de moedas
      int               digits;          // Número de casa decimais na cotação
      uchar             digits_lot;      // Número de casa decimais no lote, para arredondamento
      int               Rpoint;          // 1/point, a fim de, nas fórmulas, multiplicar por esse valor, em vez de dividir
      double            dev;             // Derrapagem possível. Convertemos diretamente no número de pontos
      double            lot;             // Volume de negociação para o par de moedas
      double            lot_min;         // Volume mínimo
      double            lot_max;         // Volume máximo
      double            lot_step;        // Incremento do lote
      double            contract;        // Tamanho do contrato
      double            price;           // Preço de abertura do preço no triângulo. Necessário para compensação
      ulong             tkt;             // Bilhete da ordem que abriu o negócio. É necessário por conveniência nas contas de cobertura
      MqlTick           tick;            // Preços atuais do par
      double            tv;              // Custo atual do tick
      double            mrg;             // Margem atual necessário para abertura
      double            sppoint;         // Spread em pontos inteiros
      double            spcost;          // Spread em dinheiro para o lote atual a ser aberto
      stSmb(){price=0;tkt=0;mrg=0;}   
   };

// Estrutura para o triângulo
struct stThree
   {
      stSmb             smb1;
      stSmb             smb2;
      stSmb             smb3;
      double            lot_min;          // Volume mínimo para todo o triângulo
      double            lot_max;          // Volume máximo para todo o triângulo     
      ulong             magic;            // Número mágico do triângulo
      uchar             status;           // Status do triângulo 0 - não se usa. 1 - enviado para abertura. 2 - aberto com sucesso. 3- enviado para fechamento
      double            pl;               // Lucro do triângulo
      datetime          timeopen;         // Hora de envio do triângulo para abertura
      double            PLBuy;            // Quanto você pode ganhar se comprar um triângulo
      double            PLSell;           //  Quanto você pode ganhar se vender triângulo
      double            spread;           //  Custo total de todos os três spreads (com a comissão!)
      stThree(){status=0;magic=0;}
   };

  
// Modo de trabalho do expert advisor  
enum enMode
   {
      STANDART_MODE  =  0, /*Symbols from Market Watch*/                  // Modo de trabalho padrão. Símbolos da Observação do mercado
      USE_FILE       =  1, /*Symbols from file*/                          // Usar o arquivo de símbolos
      CREATE_FILE    =  2, /*Create file with symbols*/                   // Criar arquivo para teste ou para trabalho
      //END_ADN_CLOSE  =  3, /*Not open, wait profit, close & exit*/      // Fechar todos os negócios e terminar o trabalho
      //CLOSE_ONLY     =  4  /*Not open, not wait profit, close & exit*/
   };


stThree  MxThree[];           // Matriz principal, onde se armazenam os triângulos de trabalho e todos os dados adicionais necessário

CTrade         ctrade;        // Classe CTrade da biblioteca padrão
CSymbolInfo    csmb;          // Classe CSymbolInfo da biblioteca padrão
CTerminalInfo  cterm;         // Classe CTerminalInfo da biblioteca padrão

int         glAccountsType=0; // Tipo de conta: cobertura ou compensação
int         glFileLog=0;      // Identificador do arquivo de log


// Parâmetros de entrada

sinput      enMode      inMode=     0;          // Modo de trabalho
input       double      inProfit=   0;          // Comissão
input       double      inLot=      1;          // Volume de negociação
input       ushort	inMaxThree= 0;          // Triângulos abertos
sinput      ulong       inMagic=    300;        // Número mágico do expert advisor
sinput      string      inCmnt=     "R ";       // Comentários

Primeiro vão os define, eles são simples e têm muitos comentários. Eu acho que não haverá problemas para compreendê-los.

Em seguida, vão duas estruturas stSmb e stThree. Sua lógica é a seguinte: qualquer triângulo consiste em três pares de moedas. Portanto, descrevendo um deles e usando-o três vezes temos um triângulo. stSmb é a estrutura que descreve o par de moedas e suas especificações: possíveis volumes de negociação, variáveis _Digits e _Point, preços atuais na abertura e outros. Na estrutura stThree, stSmb é usado três vezes, ele é nosso triângulo. Além disso, aqui são adicionadas certas propriedades pertencentes ao triângulo: o lucro atual, magic, tempo de abertura e assim por diante. Em seguida, temos os modos de trabalho, sobre os quais falaremos mais tarde, e as variáveis de entrada. As variáveis de entrada também são descritas nos comentários, porém, concentraremo-nos em duas delas:

No parâmetro inMaxThree é armazenado o número máximo de triângulos abertos simultaneamente. Quando é 0, ele não se usa. Por exemplo, se o parâmetro definido como 2, não mais de 2 triângulos podem ser abertos simultaneamente.

Parâmetro inProfit contém o tamanho da configurações, se existir.


Configuração inicial

Bem, os arquivos incluídos e variáveis usadas estão descritos. A seguir, abordamos o bloco OnInint().

Antes de iniciar o expert advisor, é necessário verificar se os parâmetros inseridos são corretos e obter os dados iniciais, onde necessário. Se tudo for bem-sucedido, começamos a trabalhar. Nos experts, eu procuro definir o mínimo de configurações de entrada, e este robô não é a exceção.

Somente um dos 6 parâmetros de entrada pode fazer com que o expert advisor não seja capaz de trabalhar, nomeadamente o volume de negociação. Não podemos abrir um negócio com volume negativo. Todas as outras configurações para saber se o trabalho é correto não influenciam. A verificação é realizada na primeira função do bloco OnInit().

Vejamos seu código.

void fnWarning(int &accounttype, double lot, int &fh)
   {   
      // Verificamos se o posicionamento do volume da ordem é correto; não podemos negociar com um volume negativo
      if (lot<0)
      {
         Alert("Trade volume < 0");  
         ExpertRemove();         
      }      
      
      // Se o volume de negociação = 0, antecipamos que o robô irá automaticamente utilizar o mínimo volume possível.
      if (lot==0) Alert("Always use the same minimum trading volume");  

Como o robô é escrito em um estilo processual, é necessário implementar algumas variáveis globais. Uma delas é o identificador do arquivo de log. O nome consiste na parte e data fixas do robô. Isso é feito para facilitar o controle, a fim de evitar a busca em um só arquivo onde começa no log um certo ponto de partida. Observe que o nome muda a cada inicialização, e o último arquivo com o mesmo nome será excluído se existir.

Durante o trabalho, o expert advisor utiliza dois arquivos, isto é: o arquivo com os triângulos encontrados (cria-se apenas durante a seleção adequada do usuário) e o arquivo de log onde se grava o tempo de abertura e fechamento do triângulo, os preços de abertura e certas informações adicionais para facilitar o controle. O arquivo de log sempre é realizado.

      // Criamos o arquivo de log, somente se não tiver sido selecionado o modo de criação do arquivo dos triângulos, uma vez que, neste caso, é irrelevante.                                  
      if(inMode!=CREATE_FILE)
      {
         string name=FILELOG+TimeToString(TimeCurrent(),TIME_DATE)+".csv";      
         FileDelete(name);      
         fh=FILEOPENWRITE(name);
         if (fh==INVALID_HANDLE) Alert("The log file is not created");      
      }   
      
      // Na esmagadora maioria, o tamanho do contrato para os pares de moedas que possuem as corretoras é de = 100 000, mas às vezes há exceções.
      // Elas são tão raras que é mais fácil, uma vez, no início, verificar este valor, e se não é igual a 100 000, então, fazer saber sobre isso,
      // para que o usuário tome sua própria decisão. Mais tarde, ele continua trabalhando sem descrever mais momentos em que  
      // no triângulo podem se encontrar pares com diferentes tamanhos de contrato.
      for(int i=SymbolsTotal(true)-1;i>=0;i--)
      {
         string name=SymbolName(i,true);
         
         // a função para verificar a disponibilidade do comércio também é usada na composição dos triângulos.
         // Aqui examinamo-la em mais detalhes
         if(!fnSmbCheck(name)) continue;
         
         double cs=SymbolInfoDouble(name,SYMBOL_TRADE_CONTRACT_SIZE);
         if(cs!=100000) Alert("Attention: "+name+", contract size = "+DoubleToString(cs,0));      
      }
      
      // Obtemos o tipo de conta com cobertura ou compensação
      accounttype=(int)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   }

Composição dos triângulos

Para elaborar triângulos, precisamos examinar os seguintes aspetos:

  1. Qual a fonte dos triângulos, quer dizer, vindos da janela "Observação do mercado" ou a partir de um arquivo previamente preparado.
  2. Estamos no testador ou não? Se for assim, será necessário carregar os símbolos na Observação do mercado. Não faz sentido carregar tudo quanto possível, uma vez que um computador de desktop simplesmente não é capaz de lidar com tal carga. Vamos procurar o arquivo previamente preparado contendo os símbolos para o teste. Se ele não existir, testaremos a estratégia no triângulo padrão: EUR+USD+GBP.
  3. Para simplificar o código, impomos uma restrição, isto é, todos os símbolos no triângulo devem ter o mesmo tamanho de contrato.
  4. Não esqueçamos que os triângulos podem ser formados apenas a partir de pares de moedas.

A primeira função necessária é a composição dos triângulos a partir da Observação do mercado.

void fnGetThreeFromMarketWatch(stThree &MxSmb[])
   {
      // Obtemos o número total de símbolos
      int total=SymbolsTotal(true);
      
      // Variáveis para comparar o tamanho dos contratos    
      double cs1=0,cs2=0;              
      
      // No primeiro ciclo tomamos o primeiro símbolo da lista
      for(int i=0;i<total-2 && !IsStopped();i++)    
      {//1
         string sm1=SymbolName(i,true);
         
         // Verificamos se o símbolo tem algum tipo de restrição
         if(!fnSmbCheck(sm1)) continue;      
              
         // Obtemos o tamanho do contrato e imediatamente normalizamo-lo, uma vez que vamos comparar esse valor 
         if (!SymbolInfoDouble(sm1,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) continue; 
         cs1=NormalizeDouble(cs1,0);
         
         // Obtemos a moeda base e a moeda do lucro, pois realizamos a comparação com base nelas, e não pelo nome do par
         string sm1base=SymbolInfoString(sm1,SYMBOL_CURRENCY_BASE);     
         string sm1prft=SymbolInfoString(sm1,SYMBOL_CURRENCY_PROFIT);
         
         // No segundo ciclo tomamos o seguinte símbolo da lista
         for(int j=i+1;j<total-1 && !IsStopped();j++)
         {//2
            string sm2=SymbolName(j,true);
            if(!fnSmbCheck(sm2)) continue;
            if (!SymbolInfoDouble(sm2,SYMBOL_TRADE_CONTRACT_SIZE,cs2)) continue;
            cs2=NormalizeDouble(cs2,0);
            string sm2base=SymbolInfoString(sm2,SYMBOL_CURRENCY_BASE);
            string sm2prft=SymbolInfoString(sm2,SYMBOL_CURRENCY_PROFIT);
            // No primeiro e o segundo par deve haver uma ocorrência em qualquer uma das moedas.
            // Se não for assim, não podemos formar um triângulo a partir deles.    
            // Neste caso, não faz sentido verificar se há paridade absoluta, porque, por exemplo, a partir de 
            // eurusd e eurusd.xxx, seja como for, não se pode formar um triângulo.
            if(sm1base==sm2base || sm1base==sm2prft || sm1prft==sm2base || sm1prft==sm2prft); else continue;
                  
            // Os tamanhos dos contratos devem ser idênticos            
            if (cs1!=cs2) continue;
            
            // No terceiro ciclo, procuramos o último símbolo para o triângulo
            for(int k=j+1;k<total && !IsStopped();k++)
            {//3
               string sm3=SymbolName(k,true);
               if(!fnSmbCheck(sm3)) continue;
               if (!SymbolInfoDouble(sm3,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) continue;
               cs1=NormalizeDouble(cs1,0);
               string sm3base=SymbolInfoString(sm3,SYMBOL_CURRENCY_BASE);
               string sm3prft=SymbolInfoString(sm3,SYMBOL_CURRENCY_PROFIT);
               
               // Sabemos que o primeiro e segundo símbolo tem uma moeda comum. Para compor formar o triângulo, é necessário encontrar
               // um tal terceiro par de moedas, onde uma moeda é a mesma no primeiro par e outra que é igual
               // no segundo par. Se não houver coincidência, o par não servirá.
               if(sm3base==sm1base || sm3base==sm1prft || sm3base==sm2base || sm3base==sm2prft);else continue;
               if(sm3prft==sm1base || sm3prft==sm1prft || sm3prft==sm2base || sm3prft==sm2prft);else continue;
               if (cs1!=cs2) continue;
               
               // Se nós alcançamos esta etapa, já todas as verificações estão feitas, e a partir destes três pares é possível formar um triângulo
               // Gravamo-lo em nossa matriz
               int cnt=ArraySize(MxSmb);
               ArrayResize(MxSmb,cnt+1);
               MxSmb[cnt].smb1.name=sm1;
               MxSmb[cnt].smb2.name=sm2;
               MxSmb[cnt].smb3.name=sm3;
               break;
            }//3
         }//2
      }//1    
   }

A segunda função necessária é a leitura dos triângulos a partir do arquivo

void fnGetThreeFromFile(stThree &MxSmb[])
   {
      // Se o arquivo com os símbolos não for encontrado, imprimimos uma mensagem sobre isto e concluímos o trabalho
      int fh=FileOpen(FILENAME,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV);
      if(fh==INVALID_HANDLE)
      {
         Print("File with symbols not read!");
         ExpertRemove();
      }
      
      // Deslocamos o carro para o início do arquivo
      FileSeek(fh,0,SEEK_SET);
      
      // Ignoramos o cabeçalho (a primeira linha do arquivo)      
      while(!FileIsLineEnding(fh)) FileReadString(fh);
      
      
      while(!FileIsEnding(fh) && !IsStopped())
      {
         // Obtemos os três símbolo do triângulo. Realizamos uma verificação básica quando a disponibilidade dos dados
         // o robô pode formar automaticamente um arquivo com triângulos. Se de repente o usuário
         // o ter mudado sozinho e de maneira incorreta, por isso assumimos que ele fez isso deliberadamente
         string smb1=FileReadString(fh);
         string smb2=FileReadString(fh);
         string smb3=FileReadString(fh);
         
         // Se os dados dos símbolos estão disponíveis, após ir até o fim da linha, gavamo-los em nossa matriz de triângulos
         if (!csmb.Name(smb1) || !csmb.Name(smb2) || !csmb.Name(smb3)) {while(!FileIsLineEnding(fh)) FileReadString(fh);continue;}
         
         int cnt=ArraySize(MxSmb);
         ArrayResize(MxSmb,cnt+1);
         MxSmb[cnt].smb1.name=smb1;
         MxSmb[cnt].smb2.name=smb2;
         MxSmb[cnt].smb3.name=smb3;
         while(!FileIsLineEnding(fh)) FileReadString(fh);
      }
   }

A última função, que é necessária nesta seção, é um encapsulamento das duas funções anteriores. Ela responde pela seleção da fonte dos triângulos dependendo das configurações de entrada do robô. Além disso, verificamos nele onde é executado o robô. Se for no Testador, independentemente da escolha do usuário, carregamos os triângulos a partir do arquivo. Se não houver arquivo, carregamos o triângulo padrão EURUSD+GBPUSD+EURGBP.

void fnSetThree(stThree &MxSmb[],enMode mode)
   {
      // Redefinimos nossa matriz de triângulos
      ArrayFree(MxSmb);
      
      // Conferimos se estamos no testador ou não
      if((bool)MQLInfoInteger(MQL_TESTER))
      {
         // Ser for assim, procuramos o arquivo dos símbolos e iniciamos o download de símbolos desde o arquivo
         if(FileIsExist(FILENAME)) fnGetThreeFromFile(MxSmb);
         
         // Se o arquivo não for encontrado, percorremos todos os símbolos disponíveis e encontre entre eles o triângulo padrão EURUSD+GBPUSD+EURGBP
         else{               
            char cnt=0;         
            for(int i=SymbolsTotal(false)-1;i>=0;i--)
            {
               string smb=SymbolName(i,false);
               if ((SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="EUR" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="GBP") ||
               (SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="EUR" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="USD") ||
               (SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="GBP" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="USD"))
               {
                  if (SymbolSelect(smb,true)) cnt++;
               }               
               else SymbolSelect(smb,false);
               if (cnt>=3) break;
            }  
            
            // Depois do download do triângulo padrão na Observação do mercado, executamos a composição do triângulo         
            fnGetThreeFromMarketWatch(MxSmb);
         }
         return;
      }
      
      // Se não estamos no testador, conferimos qual modo de trabalho escolheu o usuário: 
      // tomar os símbolos a partir da Observação do mercado ou desde o arquivo
      if(mode==STANDART_MODE || mode==CREATE_FILE) fnGetThreeFromMarketWatch(MxSmb);
      if(mode==USE_FILE) fnGetThreeFromFile(MxSmb);     
   }

Aqui usamos uma função auxiliar, nomeadamente, fnSmbCheck(). Nela é verificado se existe restrição para trabalhar com o símbolo. Se houver, ignoramo-la. Este é seu código.

bool fnSmbCheck(string smb)
   {
      // O triângulo pode ser formado apenas de pares de moedas
      if(SymbolInfoInteger(smb,SYMBOL_TRADE_CALC_MODE)!=SYMBOL_CALC_MODE_FOREX) return(false);
      
      // Se existirem restrições de comércio, então pulamos este símbolo
      if(SymbolInfoInteger(smb,SYMBOL_TRADE_MODE)!=SYMBOL_TRADE_MODE_FULL) return(false);   
      
      // Se houver uma data de início ou fim do contrato, também pulamos, já que este parâmetro não é usado na moeda
      if(SymbolInfoInteger(smb,SYMBOL_START_TIME)!=0)return(false);
      if(SymbolInfoInteger(smb,SYMBOL_EXPIRATION_TIME)!=0) return(false);
      
      // Disponibilidade para tipos de ordens. Embora o robô opere apenas ordens a mercado, não devia haver restrições
      int som=(int)SymbolInfoInteger(smb,SYMBOL_ORDER_MODE);
      if((SYMBOL_ORDER_MARKET&som)==SYMBOL_ORDER_MARKET); else return(false);
      if((SYMBOL_ORDER_LIMIT&som)==SYMBOL_ORDER_LIMIT); else return(false);
      if((SYMBOL_ORDER_STOP&som)==SYMBOL_ORDER_STOP); else return(false);
      if((SYMBOL_ORDER_STOP_LIMIT&som)==SYMBOL_ORDER_STOP_LIMIT); else return(false);
      if((SYMBOL_ORDER_SL&som)==SYMBOL_ORDER_SL); else return(false);
      if((SYMBOL_ORDER_TP&som)==SYMBOL_ORDER_TP); else return(false);
       
      // Verifica-se se a biblioteca padrão tem disponibilidade de dados         
      if(!csmb.Name(smb)) return(false);
      
      // A validação abaixo só é necessária, no trabalho real, porque às vezes por alguma razão SymbolInfoTick funciona e os preços, por assim dizer, 
      // são recebidos, porém, na verdade, Ask ou Bid=0.
      // No testador, desligamos, uma vez que os preços podem aparecer mais tarde.
      if(!(bool)MQLInfoInteger(MQL_TESTER))
      {
         MqlTick tk;      
         if(!SymbolInfoTick(smb,tk)) return(false);
         if(tk.ask<=0 ||  tk.bid<=0) return(false);      
      }

      return(true);
   }

Bem, os triângulos estão formados. As funções para sua composição estão no arquivo incluído fnSetThree.mqh. A função para verificar a restrição do símbolo se encontra no arquivo separado fnSmbCheck.mqh.

Nós compomos todos os triângulos possíveis. Os pares contidos neles podem se encontrar aleatoriamente, o que causa inúmeros incômodos, uma vez que precisamos definir como expressar um par de moedas por meio de outras. Providenciando pôr ordem, no exemplo euro-dólar-libra estudamos todas as possíveis variantes de disposição:

símbolo 1 símbolo 2
símbolo 3
1 EURUSD = GBPUSD  х EURGBP
2 EURUSD = EURGBP  х GBPUSD
3 GBPUSD = EURUSD  / EURGBP
4 GBPUSD = EURGBP  0 EURUSD
5 EURGBP = EURUSD  / GBPUSD
6 EURGBP = GBPUSD  0 EURUSD

'x' = multiplicar , '/' = dividir. '0' = ação impossível

Na tabela acima, pode se ver que o triângulo pode ser composto de 6 maneiras, mas duas delas - linhas 4 e 6 - não permitem expressar o primeiro símbolo por meio dos dois restantes. Então, essas variantes caem. As restantes 4 são idênticas. Acima de tudo, o que tem importância para nos é a velocidade. A operação de divisão é mais lenta do que a multiplicação, por isso pomos de lado as variantes 3 e 5. Restam duas variantes: linhas 1 e 2.

Devido a uma cômoda visualização, concentramo-nos na variante da lina 2. Assim, não teremos de inserir caixas de edição adicionais no robô para o primeiro, segundo e terceiro símbolos, na verdade, isso é possível, uma vez que negociamos não apenas um, mas sim todos os possíveis.

A comodidade depende de nossa escolha, e uma vez que operamos arbitragem, esta estratégia pressupõe a posição neutra, portanto devemos comprar e vender o mesmo. Exemplo: Buy 0.7 do lote EURUSD e Sell 0.7 do lote EURGBP - nós compramos e vendemos 70000€. Ou seja, temos uma posição, apesar do fato de que estamos fora do mercado, uma vez que durante a compra e venda figurava um volume, mas expresso em dinheiro diferente. Precisamos ajustar ir isso realizando um negócio em GBPUSD. Em outras palavras, de vez sabemos que os símbolos 1 e 2 devem ter sempre o mesmo volume, mas uma direção diferente. Além disso, sabe-se que o volume do terceiro par é igual ao preço do segundo par.

Função que coloca pares no triângulo corretamente:

void fnChangeThree(stThree &MxSmb[])
   {
      int count=0;
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for         
         // Primeiro, definimos o que está em terceiro lugar. 
         // Este par é uma moeda base que não conincide com as outras duas moedas
         string sm1base="",sm2base="",sm3base="";
         
         // Se, de repente, por alguma razão não conseguimos obter a moeda base, então não usamos esse triângulo
         if(!SymbolInfoString(MxSmb[i].smb1.name,SYMBOL_CURRENCY_BASE,sm1base) ||
         !SymbolInfoString(MxSmb[i].smb2.name,SYMBOL_CURRENCY_BASE,sm2base) ||
         !SymbolInfoString(MxSmb[i].smb3.name,SYMBOL_CURRENCY_BASE,sm3base)) {MxSmb[i].smb1.name="";continue;}
                  
         // Se, na moeda base, 1 e 2 símbolos coincidem, em seguida, pulamos esta etapa. Se não, alteramos o lugar dos par
         if(sm1base!=sm2base)
         {         
            if(sm1base==sm3base)
            {
               string temp=MxSmb[i].smb2.name;
               MxSmb[i].smb2.name=MxSmb[i].smb3.name;
               MxSmb[i].smb3.name=temp;
            }
            
            if(sm2base==sm3base)
            {
               string temp=MxSmb[i].smb1.name;
               MxSmb[i].smb1.name=MxSmb[i].smb3.name;
               MxSmb[i].smb3.name=temp;
            }
         }
         
         // Agora definimos o primeiro e segundo lugar. 
         // No segundo lugar o par cuja moeda de lucro coincide com a base da terceira moeda. 
         // Neste caso, usamos sempre multiplicação.
         sm3base=SymbolInfoString(MxSmb[i].smb3.name,SYMBOL_CURRENCY_BASE);
         string sm2prft=SymbolInfoString(MxSmb[i].smb2.name,SYMBOL_CURRENCY_PROFIT);
         
         // Alteramos os lugares do primeiro e segundo par. 
         if(sm3base!=sm2prft)
         {
            string temp=MxSmb[i].smb1.name;
            MxSmb[i].smb1.name=MxSmb[i].smb2.name;
            MxSmb[i].smb2.name=temp;
         }
         
         // Imprimimos uma mensagem sobre o triângulo processado. 
         Print("Use triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name);
         count++;
      }//
      // Relatamos sobre o número total de triângulos no trabalho. 
      Print("All used triangles: "+(string)count);
   }

A função é inteiramente localizada em um arquivo separado fnChangeThree.mqh.

E a última etapa necessário para completar a preparação de triângulos: carregamos Imediatamente todos os dados dos pares usados, ​​para não gastar tempo chamando-os. Nós precisamos o seguinte:

  1. volumes máximo e mínimo de negociação para cada símbolo;
  2. número de caracteres no preço e volume para o arredondamento;
  3. variável Point e Ticksize. Eu nunca deparei com uma situação em que os pares de moedas fossem diferentes, mas, mesmo assim, obtemos todos os dados e utilizamo-los nos lugares certos.
void fnSmbLoad(double lot,stThree &MxSmb[])
   {
      
      // Macros simples para imprimir   
      #define prnt(nm) {nm="";Print("NOT CORRECT LOAD: "+nm);continue;}
      
      // Percorremos no ciclo todos os triângulos formados. Neste caso, vamos ter um gasto excessivo de tempo para solicitar várias vezes os dados do mesmo 
      //  símbolo, mas, como esta operação só ocorre quando o robô é baixado, pode-se atuar dessa maneira para reduzir o código.
      // Para obter os dados, usamos a biblioteca padrão. 
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // Após transferir o símbolo para a classe CSymbolInfo, inicializamos a coleta de todos os dados que precisamos
         // e ao mesmo tempo e verificamos sua disponibilidade. Se algo está errado, marcamos o triângulo como inoperante.                  
         if (!csmb.Name(MxSmb[i].smb1.name))    prnt(MxSmb[i].smb1.name); 
         
         // Obtemos o número de dígitos de cada símbolo
         MxSmb[i].smb1.digits=csmb.Digits();
         
         //Convertemos a derrapagem de pontos inteiros em decimais. Mais tarde vamos precisar deste formato para realizar os cálculos
         MxSmb[i].smb1.dev=csmb.TickSize()*DEVIATION;         
         
         // Para transformar as cotações em pontos, geralmente temos que dividir o preço pelo valor _Point.
         // O melhor é representar este valor como 1/Point, e então substituímos a divisão e multiplicamos. 
         // Aqui não se verifica se csmb.Point() é igual a 0, porque ela não pode ser igual a 0, mas se por algum motivo  
         // não for obtido o parâmetro, este triângulo será eliminado pela linha if (!csmb.Name(MxSmb[i].smb1.name)).            
         MxSmb[i].smb1.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         
         // Arredondamos o lote, uma vez que há muitos caracteres. 
         MxSmb[i].smb1.digits_lot=csup.NumberCount(csmb.LotsStep());
         
         // As restrições de volume são normalizadas imediatamente
         MxSmb[i].smb1.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb1.digits_lot);
         MxSmb[i].smb1.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb1.digits_lot);
         MxSmb[i].smb1.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb1.digits_lot); 
         
         //Tamanho do contrato 
         MxSmb[i].smb1.contract=csmb.ContractSize();
         
         // O mesmo que acima, mas levado para o símbolo 2
         if (!csmb.Name(MxSmb[i].smb2.name))    prnt(MxSmb[i].smb2.name);
         MxSmb[i].smb2.digits=csmb.Digits();
         MxSmb[i].smb2.dev=csmb.TickSize()*DEVIATION;
         MxSmb[i].smb2.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         MxSmb[i].smb2.digits_lot=csup.NumberCount(csmb.LotsStep());
         MxSmb[i].smb2.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb2.digits_lot);
         MxSmb[i].smb2.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb2.digits_lot);
         MxSmb[i].smb2.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb2.digits_lot);         
         MxSmb[i].smb2.contract=csmb.ContractSize();
         
         // O mesmo que acima, mas levado para o símbolo 3
         if (!csmb.Name(MxSmb[i].smb3.name))    prnt(MxSmb[i].smb3.name);
         MxSmb[i].smb3.digits=csmb.Digits();
         MxSmb[i].smb3.dev=csmb.TickSize()*DEVIATION;
         MxSmb[i].smb3.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         MxSmb[i].smb3.digits_lot=csup.NumberCount(csmb.LotsStep());
         MxSmb[i].smb3.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb3.digits_lot);
         MxSmb[i].smb3.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb3.digits_lot);
         MxSmb[i].smb3.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb3.digits_lot);           
         MxSmb[i].smb3.contract=csmb.ContractSize();   
         
         // Ajustamos o volume de negociação. Aqui há restrições para cada par de moedas e para todo o triângulo. 
         // Restrições para o par gravadas aqui: MxSmb[i].smbN.lotN
         // Restrições para o triângulo  gravadas aqui: MxSmb[i].lotN
         
         // Selecionamos - de todos os valores mínimos - o valor máximo. Neste caso, arredondamos com base no valor maior.
         // Todo este bloco de código é feito apenas se em termos de volume acontecer que: 0,01 + 0,01 + 0,1. 
         // Neste caso, o menor volume de negociação possível será definido como 0,1 e arredondado para 1 casa decimal.
         double lt=MathMax(MxSmb[i].smb1.lot_min,MathMax(MxSmb[i].smb2.lot_min,MxSmb[i].smb3.lot_min));
         MxSmb[i].lot_min=NormalizeDouble(lt,(int)MathMax(MxSmb[i].smb1.digits_lot,MathMax(MxSmb[i].smb2.digits_lot,MxSmb[i].smb3.digits_lot)));
         
         // Também tomamos - de todos os valores máximos - o valor mínimo, e imediatamente arredondamos. 
         lt=MathMin(MxSmb[i].smb1.lot_max,MathMin(MxSmb[i].smb2.lot_max,MxSmb[i].smb3.lot_max));
         MxSmb[i].lot_max=NormalizeDouble(lt,(int)MathMax(MxSmb[i].smb1.digits_lot,MathMax(MxSmb[i].smb2.digits_lot,MxSmb[i].smb3.digits_lot)));
         
         // Se, nos parâmetros de entrada, o volume de negociação é definido como 0, usamos a menor volume possível, no entanto, não tomamos nenhum mínimo para cada par, 
         // em vez disso, tomamos o mínimo para todos. 
         if (lot==0)
         {
            MxSmb[i].smb1.lot=MxSmb[i].lot_min;
            MxSmb[i].smb2.lot=MxSmb[i].lot_min;
            MxSmb[i].smb3.lot=MxSmb[i].lot_min;
         } else
         {
            // Se é necessário ajustar o volume, nos pares 1 e 2 ele é conhecido, e o volume do terceiro par será calculado diretamente na entrada. 
            MxSmb[i].smb1.lot=lot;  
            MxSmb[i].smb2.lot=lot;
            
            // Se o volume de negociação de entrada não se enquadrar nas restrições atuais, não vamos usar o triângulo durante o trabalho. 
            // Notificamos sobre isto usando um alerta
            if (lot<MxSmb[i].smb1.lot_min || lot>MxSmb[i].smb1.lot_max || lot<MxSmb[i].smb2.lot_min || lot>MxSmb[i].smb2.lot_max) 
            {
               MxSmb[i].smb1.name="";
               Alert("Triangle: "+MxSmb[i].smb1.name+" "+MxSmb[i].smb2.name+" "+MxSmb[i].smb3.name+" - not correct the trading volume");
               continue;
            }            
         }
      }
   }

A função é localizada em um arquivo separado fnSmbLoad.mqh

Nesta seção sobre a formação de triângulos podemos considerar que o tópico está fechado. Seguimos em frente.

Modo de trabalho do expert advisor

Ao executar o robô, podemos selecionar um dos modos disponíveis:
  1. Symbols from Market Watch.
  2. Symbols from file.
  3. Create file with symbols.

Modo "Symbols from Market Watch" assume que nós iniciamos o robô no símbolo atual e formamos triângulos de trabalho a partir da janela Observação do mercado. Este é o modo básico de funcionamento, e não necessita de tratamento adicional.

Modo "Symbols from file" se distingue do primeiro pela fonte dos triângulos, isto é, a partir do arquivo previamente preparado.

Modo "Create file with symbols" cria oportunamente o arquivo com triângulos, que, mais para frente, vamos usar quer no segundo modo de trabalho ou no testador. Este modo implica apenas a composição dos triângulos, após a qual o trabalho do expert advisor é concluído.

Descrevemos esta lógica:

      if(inMode==CREATE_FILE)
      {
         // Removemos o arquivo, se ele existir.
         FileDelete(FILENAME);  
         int fh=FILEOPENWRITE(FILENAME);
         if (fh==INVALID_HANDLE) 
         {
            Alert("File with symbols not created");
            ExpertRemove();
         }
         // Escrevemos os triângulos e algumas informações adicionais no arquivo
         fnCreateFileSymbols(MxThree,fh);
         Print("File with symbols created");
         
         // Fechamos o arquivo e completamos o trabalho do Expert Advisor
         FileClose(fh);
         ExpertRemove();
      }

A função de armazenamento de dados é simples e não requer comentários específicos:

void fnCreateFileSymbols(stThree &MxSmb[], int filehandle)
   {
      // Definimos os cabeçalhos no arquivo
      FileWrite(filehandle,"Symbol 1","Symbol 2","Symbol 3","Contract Size 1","Contract Size 2","Contract Size 3",
      "Lot min 1","Lot min 2","Lot min 3","Lot max 1","Lot max 2","Lot max 3","Lot step 1","Lot step 2","Lot step 3",
      "Common min lot","Common max lot","Digits 1","Digits 2","Digits 3");
      
      // Preenchemos o arquivo de cabeçalho em conformidade com os cabeçalhos mencionados acima
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         FileWrite(filehandle,MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name,
         MxSmb[i].smb1.contract,MxSmb[i].smb2.contract,MxSmb[i].smb3.contract,
         MxSmb[i].smb1.lot_min,MxSmb[i].smb2.lot_min,MxSmb[i].smb3.lot_min,
         MxSmb[i].smb1.lot_max,MxSmb[i].smb2.lot_max,MxSmb[i].smb3.lot_max,
         MxSmb[i].smb1.lot_step,MxSmb[i].smb2.lot_step,MxSmb[i].smb3.lot_step,
         MxSmb[i].lot_min,MxSmb[i].lot_max,
         MxSmb[i].smb1.digits,MxSmb[i].smb2.digits,MxSmb[i].smb3.digits);         
      }
      FileWrite(filehandle,"");      
      // Deixamos uma linha em branco depois de todos os símbolos
      
      // Depois de completar o trabalho, transferimos todos os dados para o disco, por segurança 
      FileFlush(filehandle);
   }

Além dos triângulos, gravamos informações adicionais: o volume de negociação permitido, o tamanho do contrato, o número de caracteres nas cotações. Precisamos esses dados a partir do arquivo apenas para um controle visual das propriedades dos símbolos.

Essa função está disponível no arquivo separado fnCreateFileSymbols.mqh


Reinicio do robô

Temos quase completado a configuração de inicialização do expert advisor. Nós ainda temos que responder a outra pergunta: como lidar com a recuperação de pane? Se ocorrer uma perda de Internet curta, não há problema. O robô vai continuar a funcionar corretamente após a reconexão à rede. Mas se tivermos que reiniciar o robô, então precisamos encontrar suas posições e continuar a trabalhar com els.

Função que resolve os problemas com a reinicialização do robô:

void fnRestart(stThree &MxSmb[],ulong magic,int accounttype)
   {
      string   smb1,smb2,smb3;
      long     tkt1,tkt2,tkt3;
      ulong    mg;
      uchar    count=0;    //Contador de triângulos restaurados
      
      switch(accounttype)
      {
         // Com cobertura, restaurar a posição é simples, basta percorrer por todas as posições abertas, e mediante o magic encontrar as próprias e 
         // formar com elas triângulos.
         // Com a cobertura é mais difícil, porque é necessário acessar seu próprio banco de dados, em que armazenada a posição aberta pelo robô. 
         
         // Algoritmo para buscar posições e recuperação sem caminhos difíceis e 
         // otimização. Mas, como em esta etapa é necessária raramente, pode-se ignorar o desempenho para
         // redução do código. 
         
         case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
            // Percorremos todas as posições abertas e vemos a ocorrência pelo magic. 
            // Lembramos o magic da primeira posição encontrada, uma vez que com base nele vamos buscas os dois restantes. 

            
            for(int i=PositionsTotal()-1;i>=2;i--)
            {//for i
               smb1=PositionGetSymbol(i);
               mg=PositionGetInteger(POSITION_MAGIC);
               if (mg<magic || mg>(magic+MAGIC)) continue;
               
               // Lembramos o bilhete para que mais tarde seja mais simples acessar essa posição. 
               tkt1=PositionGetInteger(POSITION_TICKET);
               
               // Buscamos a segunda posição com esse mesmo magic. 
               for(int j=i-1;j>=1;j--)
               {//for j
                  smb2=PositionGetSymbol(j);
                  if (mg!=PositionGetInteger(POSITION_MAGIC)) continue;  
                  tkt2=PositionGetInteger(POSITION_TICKET);          
                    
                  // Buscamos a última posição.
                  for(int k=j-1;k>=0;k--)
                  {//for k
                     smb3=PositionGetSymbol(k);
                     if (mg!=PositionGetInteger(POSITION_MAGIC)) continue;
                     tkt3=PositionGetInteger(POSITION_TICKET);
                     
                     // Se chegamos até esta linha, significa que há um triângulo aberto. Os dados sobre ele já estão carregados. O robô contará tudo o resto com a próximo tick.
                     
                     for(int m=ArraySize(MxSmb)-1;m>=0;m--)
                     {//for m
                        // Percorremos a matriz de triângulos, pulando todos os abertos.
                        if (MxSmb[m].status!=0) continue; 
                        
                        // A pesquisa detalhada é feita no cabeçalho. À primeira vista, pode parecer que, ao acontecer isto, podemos algumas vezes 
                        // consultar o mesmo par de moedas. No entanto, não é assim, porque nos ciclos de pesquisa detalhada 
                        // depois de encontrar o seguinte par de moedas, continuamos a procurar desde o próximo par e não desde o início

                        if (  (MxSmb[m].smb1.name==smb1 || MxSmb[m].smb1.name==smb2 || MxSmb[m].smb1.name==smb3) &&                               (MxSmb[m].smb2.name==smb1 || MxSmb[m].smb2.name==smb2 || MxSmb[m].smb2.name==smb3) &&                               (MxSmb[m].smb3.name==smb1 || MxSmb[m].smb3.name==smb2 || MxSmb[m].smb3.name==smb3)); else continue;                                                  // Encontramos este triângulo, e atribuímos o estatuto adequado                         MxSmb[m].status=2;                         MxSmb[m].magic=magic;                         MxSmb[m].pl=0;                                                  // Organizamos os bilhetes na sequência correta. Triângulo novamente em funcionamento.                         if (MxSmb[m].smb1.name==smb1) MxSmb[m].smb1.tkt=tkt1;                         if (MxSmb[m].smb1.name==smb2) MxSmb[m].smb1.tkt=tkt2;                         if (MxSmb[m].smb1.name==smb3) MxSmb[m].smb1.tkt=tkt3;                                if (MxSmb[m].smb2.name==smb1) MxSmb[m].smb2.tkt=tkt1;                         if (MxSmb[m].smb2.name==smb2) MxSmb[m].smb2.tkt=tkt2;                         if (MxSmb[m].smb2.name==smb3) MxSmb[m].smb2.tkt=tkt3;                                  if (MxSmb[m].smb3.name==smb1) MxSmb[m].smb3.tkt=tkt1;                         if (MxSmb[m].smb3.name==smb2) MxSmb[m].smb3.tkt=tkt2;                         if (MxSmb[m].smb3.name==smb3) MxSmb[m].smb3.tkt=tkt3;                                                    count++;                                                 break;                        }//for m                                 }//for k                              }//for j                     }//for i                  break;          default:          break;       }              if (count>0) Print("Restore "+(string)count+" triangles");                }

Como antes, essa função está em um arquivo separado: fnRestart.mqh

Últimas etapas:

      ctrade.SetDeviationInPoints(DEVIATION);
      ctrade.SetTypeFilling(ORDER_FILLING_FOK);
      ctrade.SetAsyncMode(true);
      ctrade.LogLevel(LOG_LEVEL_NO);
      
      EventSetTimer(1);

Preste atenção ao modo assíncrono de envio de ordens. A estratégia envolve uma ação mais rápida, portanto usamos este modo de posicionamento. Haverá dificuldades: precisamos de código extra para acompanhar se a posição é aberta com sucesso. Considere tudo isso abaixo.

Por aí fica concluído o bloco OnInit(), podemos avançar para o corpo do robô.

OnTick

Primeiro, vemos se, nas configurações, temos restrições no número máximo de triângulos abertos possível. O fato de houver indica que atingimos o spread definido, e neste tick, pode-se ignorar parte substancial do código:

      ushort OpenThree=0;                          // Número de triângulos abertos
      for(int j=ArraySize(MxThree)-1;j>=0;j--)
      if (MxThree[j].status!=0) OpenThree++;       // Contamos os que não estão fechados também         

A verificação é simples. Nós declaramos a variável local para contagem de triângulos abertos e percorremos nossa matriz principal. Se o status do triângulo não for 0, ele está funcionando. 

Após a contagem de triângulos abertos, se a restrição permitir, começamos a examinar todos os triângulos restantes e acompanhar seu status. A função fnCalcDelta() é responsável por isso:

      if (inMaxThree==0 || (inMaxThree>0 && inMaxThree>OpenThree))
         fnCalcDelta(MxThree,inProfit,inCmnt,inMagic,inLot,inMaxThree,OpenThree);   // Calculamos a divergência e imediatamente abrimos

Examinamos seu código:

void fnCalcDelta(stThree &MxSmb[],double prft, string cmnt, ulong magic,double lot, ushort lcMaxThree, ushort &lcOpenThree)
   {     
      double   temp=0;
      string   cmnt_pos="";
      
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for i
         // Se o triângulo está trabalhando, então ignoramo-lo
         if(MxSmb[i].status!=0) continue; 
         
         // Mais uma vez, vamos verificar a disponibilidade de todos os três pares: se pelo menos um deles não estiver disponível,
         // então não fará sentido calcular todo o triângulo
         if (!fnSmbCheck(MxSmb[i].smb1.name)) continue;  
         if (!fnSmbCheck(MxSmb[i].smb2.name)) continue;  //e se de repente tivermos fechado a negociação em algum par
         if (!fnSmbCheck(MxSmb[i].smb3.name)) continue;     
         
         // Calculamos o número de triângulos abertos no início de cada tick,
         // eles podem ser abertos dentro do tick. Por isso, constantemente acompanhamos seu número
         if (lcMaxThree>0) {if (lcMaxThree>lcOpenThree); else continue;}     

         
         // Em seguida, obtemos todos os dados necessários para os cálculos. 
         
         // Obtemos o valor de tick para cada par.
         if(!SymbolInfoDouble(MxSmb[i].smb1.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb1.tv)) continue;
         if(!SymbolInfoDouble(MxSmb[i].smb2.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb2.tv)) continue;
         if(!SymbolInfoDouble(MxSmb[i].smb3.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb3.tv)) continue;
         
         // Obtemos os preços atuais.
         if(!SymbolInfoTick(MxSmb[i].smb1.name,MxSmb[i].smb1.tick)) continue;
         if(!SymbolInfoTick(MxSmb[i].smb2.name,MxSmb[i].smb2.tick)) continue;
         if(!SymbolInfoTick(MxSmb[i].smb3.name,MxSmb[i].smb3.tick)) continue;
         
         // Verificamos se Ask ou Bid são iguais a 0.
         if(MxSmb[i].smb1.tick.ask<=0 || MxSmb[i].smb1.tick.bid<=0 || MxSmb[i].smb2.tick.ask<=0 || MxSmb[i].smb2.tick.bid<=0 || MxSmb[i].smb3.tick.ask<=0 || MxSmb[i].smb3.tick.bid<=0) continue;
         
         // Calculamos o volume para o terceiro par. O volume dos dois primeiros pares é conhecido, ele é o mesmo e é fixo.
         // O volume do terceiro par sempre muda. Porém, ele é calculado só se, nas variáveis de início, o valor do lote é igual a 0.
         // Quando o lote é zero em todos os lugares, será utilizado o volume mínimo
         // A lógica de cálculo de volume é simples. Lembramos nossa variação de triângulo: EURUSD=EURGBP*GBPUSD. Quantidade de libras compradas ou vendidas
         // depende das cotações do EURGBP, e no terceiro par, esta terceira moeda está em primeiro lugar. Isso nós liberta da parte dos cálculos,
         // tomando como volume o preço do segundo par. Eu tomei a média entre o Ask e o Bid.
         // Não esquecemos a alteração do volume de negociação de entrada.
         
         if (lot>0)
         MxSmb[i].smb3.lot=NormalizeDouble((MxSmb[i].smb2.tick.ask+MxSmb[i].smb2.tick.bid)/2*MxSmb[i].smb1.lot,MxSmb[i].smb3.digits_lot);
         
         // Se o volume calculado estiver fora dos limites admissíveis, informamos o usuário sobre isso.
         // Marcamos esse triângulo como não funcional
         if (MxSmb[i].smb3.lot<MxSmb[i].smb3.lot_min || MxSmb[i].smb3.lot>MxSmb[i].smb3.lot_max)
         {
            Alert("The calculated lot for ",MxSmb[i].smb3.name," is out of range. Min/Max/Calc: ",
            DoubleToString(MxSmb[i].smb3.lot_min,MxSmb[i].smb3.digits_lot),"/",
            DoubleToString(MxSmb[i].smb3.lot_max,MxSmb[i].smb3.digits_lot),"/",
            DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot)); 
            Alert("Triangle: "+MxSmb[i].smb1.name+" "+MxSmb[i].smb2.name+" "+MxSmb[i].smb3.name+" - DISABLED");
            MxSmb[i].smb1.name="";   
            continue;  
         }
         
         // Calculamos nossas despesas, ou seja, spread+comissão. pr = spread em pontos inteiros.
         // O spread não nós permite ganhar com essa estratégia, por isso é preciso calculá-lo. 
         // Pode-se tomar de vez o spread em pontos, em vez de utilizar a diferença de preços multiplicada pelo ponto inverso.

         
         MxSmb[i].smb1.sppoint=NormalizeDouble(MxSmb[i].smb1.tick.ask-MxSmb[i].smb1.tick.bid,MxSmb[i].smb1.digits)*MxSmb[i].smb1.Rpoint;
         MxSmb[i].smb2.sppoint=NormalizeDouble(MxSmb[i].smb2.tick.ask-MxSmb[i].smb2.tick.bid,MxSmb[i].smb2.digits)*MxSmb[i].smb2.Rpoint;
         MxSmb[i].smb3.sppoint=NormalizeDouble(MxSmb[i].smb3.tick.ask-MxSmb[i].smb3.tick.bid,MxSmb[i].smb3.digits)*MxSmb[i].smb3.Rpoint;
         if (MxSmb[i].smb1.sppoint<=0 || MxSmb[i].smb2.sppoint<=0 || MxSmb[i].smb3.sppoint<=0) continue;
         
         // Agora calculamos o spread na moeda do depósito. 
         // Na moeda, o custo de 1 tick é sempre igual ao parâmetro SYMBOL_TRADE_TICK_VALUE.
         // Além disso, não nós esquecemos dos volumes de negociação
         MxSmb[i].smb1.spcost=MxSmb[i].smb1.sppoint*MxSmb[i].smb1.tv*MxSmb[i].smb1.lot;
         MxSmb[i].smb2.spcost=MxSmb[i].smb2.sppoint*MxSmb[i].smb2.tv*MxSmb[i].smb2.lot;
         MxSmb[i].smb3.spcost=MxSmb[i].smb3.sppoint*MxSmb[i].smb3.tv*MxSmb[i].smb3.lot;
         
         // Bem, essas são nossas despesas para o volume de negociação definido acrescentando a comissão estabelecida pelo usuário
         MxSmb[i].spread=MxSmb[i].smb1.spcost+MxSmb[i].smb2.spcost+MxSmb[i].smb3.spcost+prft;
         
         // Podemos monitorar quando o Ask < Bid, embora seja uma situação muito rara, 
         // ela pode ser acompanhada separadamente. Além disso, a arbitragem, espalhada no tempo, também lida com essa situação.
         // Assim, a permanência na posição é livre de riscos, e por isso: nós compramos eurusd,
         // e de vez vendemos através de eurgbp e gbpusd. 
         // Ou seja, nós vimos ask eurusd< bid eurgbp * bid gbpusd. Essas situações costumam ser muitas, mas, para entrar no mercado com sucesso, isso é pouco.
         // Calculamos também os custos do spread. É necessário entrar não só quando Ask < Bid, mas também quando a diferença entre 
         // o custo do spread.          
         
         // Concordamos em que a compra significa comprar o primeiro símbolo e vender os dois restantes,
         // enquanto a venda é vender o primeiro par e comprar os dois restantes.
         
         temp=MxSmb[i].smb1.tv*MxSmb[i].smb1.Rpoint*MxSmb[i].smb1.lot;
         
         // Examinemos a fórmula de cálculo. 
         // 1. Entre parênteses, em cada preço, a derrapagem é ajustada para o pior lado: MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev
         // 2. Como mostrado, na fórmula acima , bid eurgbp * bid gbpusd, são os preços do segundo e terceiro símbolo que multiplicamos:
         //    (MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.bid-MxSmb[i].smb3.dev)
         // 3. A seguir, calculamos a diferença entre o Ask e o Bid
         // 4. Obtivemos a diferença em pontos que é necessário converter em pontos, isto é, multiplicar 
         // custo do ponto e volume de negociação. Para fazer isso, tomamos os valores do primeiro par. 
         // Se quisermos construir um triângulo movendo todos os pares na mesma direção e comparando com 1, os cálculos serão maiores. 

         MxSmb[i].PLBuy=((MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.bid-MxSmb[i].smb3.dev)-(MxSmb[i].smb1.tick.ask+MxSmb[i].smb1.dev))*temp;
         MxSmb[i].PLSell=((MxSmb[i].smb1.tick.bid-MxSmb[i].smb1.dev)-(MxSmb[i].smb2.tick.ask+MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.ask+MxSmb[i].smb3.dev))*temp;
         
         // Obtivemos o cálculo do montante que podemos ganhar ou perder, se compramos ou vendemos o triângulo. 
         // Resta compará-lo com o custo para decidir se entramos em uma transação. Ajustamos para dois 2 caracteres. 
         MxSmb[i].PLBuy=   NormalizeDouble(MxSmb[i].PLBuy,2);
         MxSmb[i].PLSell=  NormalizeDouble(MxSmb[i].PLSell,2);
         MxSmb[i].spread=  NormalizeDouble(MxSmb[i].spread,2);                  
         
         // Se houver um lucro potencial, devemos verificar se há suficiente dinheiro para a abertura.         
         if (MxSmb[i].PLBuy>MxSmb[i].spread || MxSmb[i].PLSell>MxSmb[i].spread)
         {
            // Eu só considerei a margem inteira para compra. Como ela, seja como for, é maior do que para venda, pode-se não levar em conta a direção do negócio.  
            // Observemos o coeficiente crescente. Não se deve abrir o triângulo quando a margem é ligeiramente suficiente. O coeficiente crescente por padrão é = 20%

            if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb1.name,MxSmb[i].smb1.lot,MxSmb[i].smb1.tick.ask,MxSmb[i].smb1.mrg))
            if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb2.name,MxSmb[i].smb2.lot,MxSmb[i].smb2.tick.ask,MxSmb[i].smb2.mrg))
            if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb3.name,MxSmb[i].smb3.lot,MxSmb[i].smb3.tick.ask,MxSmb[i].smb3.mrg))
            if(AccountInfoDouble(ACCOUNT_MARGIN_FREE)>((MxSmb[i].smb1.mrg+MxSmb[i].smb2.mrg+MxSmb[i].smb3.mrg)*CF))  //verificamos a margem livre
            {
               // Estamos quase prontos para abrir, resta encontrar um magic livre em nossa gama. 
               // O magic inicial é definido nos parâmetros da entrada na variável inMagic, por padrão é 300. 
               // O intervalo de magics é definido em MAGIC, por padrão é 200.
               MxSmb[i].magic=fnMagicGet(MxSmb,magic);   
               if (MxSmb[i].magic<=0)
               { // Se retornar 0, todos os magics estão ocupados. Enviamos uma mensagem sobre isto e imprimimos.
                  Print("Free magic ended\nNew triangles will not open");
                  break;
               }  
               
               // Definimos o magic encontrado para o robô
               ctrade.SetExpertMagicNumber(MxSmb[i].magic); 
               
               // Criamos o comentário para o triângulo
               cmnt_pos=cmnt+(string)MxSmb[i].magic+" Open";               
               
               // Abrimos, memorizando o tempo de envio do triângulo para a abertura. 
               // Isso é para não estar esperando. 
               // Por padrão, no define MAXTIMEWAIT é definido o tempo de espera antes da abertura, isto é, 3 segundos.
               // Se durante este tempo nós não abrimos, enviamos para fechamento o que teve tempo para se abrir.
               
               MxSmb[i].timeopen=TimeCurrent();
               
               if (MxSmb[i].PLBuy>MxSmb[i].spread)    fnOpen(MxSmb,i,cmnt_pos,true,lcOpenThree);
               if (MxSmb[i].PLSell>MxSmb[i].spread)   fnOpen(MxSmb,i,cmnt_pos,false,lcOpenThree);               
               
               // Imprimimos a mensagem sobre a abertura do triângulo. 
               if (MxSmb[i].status==1) Print("Open triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name+" magic: "+(string)MxSmb[i].magic);
            }
         }         
      }//for i
   }

Eu minuciosamente provi a função de comentários e explicações. Espero que o leitor não fique com dúvidas sobre ela. Por baixo dos panos, restam dois momentos: o mecanismo de seleção do magic disponível e abertura direta do triângulo.

Assim escolhemos o magic disponível:

ulong fnMagicGet(stThree &MxSmb[],ulong magic)
   {
      int mxsize=ArraySize(MxSmb);
      bool find;
      
      // Pode-se percorrer todos os triângulos abertos em nossa matriz. 
      // Mas eu escolhi outra opção, isto é, percorrer o intervalo do magic,
      // e disparar o já escolhido na matriz. 
      for(ulong i=magic;i<magic+MAGIC;i++)
      {
         find=false;
         
         // Magic em i. Verifique se ele é atribuído a algum triângulo dos abertos.
         for(int j=0;j<mxsize;j++)
         if (MxSmb[j].status>0 && MxSmb[j].magic==i)
         {
            find=true;
            break;   
         }   
         
         // Se o magic não é usado,  saímos do ciclo, sem esperar pela sua conclusão.    
         if (!find) return(i);            
      }  
      return(0);  
   }

Assim abrimos o triângulo:

bool fnOpen(stThree &MxSmb[],int i,string cmnt,bool side, ushort &opt)
   {
      // Sinalizador de abertura de primeira ordem. 
      bool openflag=false;
      
      // Se não tivermos permissão para negociar, não operamos. 
      if (!cterm.IsTradeAllowed())  return(false);
      if (!cterm.IsConnected())     return(false);
      
      switch(side)
      {
         case  true:
         
         // Se, após enviar a ordem para abertura, for retornado false, não fará sentido enviar para abertura os 2 pares restantes. 
         // Melhor tentemos novamente no próximo tick. O robô também não se ocupa de acabar de abrir o triângulo. 
         // Se, após enviar ordens, algo não se abrir,
         // então após o tempo especificado no define MAXTIMEWAIT, fechamos o triângulo que não é aberto. 
         if(ctrade.Buy(MxSmb[i].smb1.lot,MxSmb[i].smb1.name,0,0,0,cmnt))
         {
            openflag=true;
            MxSmb[i].status=1;
            opt++;
            // Em seguida, a lógica é a mesma, se não conseguimos abrir, o triângulo deverá ser fechado. 
            if(ctrade.Sell(MxSmb[i].smb2.lot,MxSmb[i].smb2.name,0,0,0,cmnt))
            ctrade.Sell(MxSmb[i].smb3.lot,MxSmb[i].smb3.name,0,0,0,cmnt);               
         }            
         break;
         case  false:
         
         if(ctrade.Sell(MxSmb[i].smb1.lot,MxSmb[i].smb1.name,0,0,0,cmnt))
         {
            openflag=true;
            MxSmb[i].status=1;  
            opt++;        
            if(ctrade.Buy(MxSmb[i].smb2.lot,MxSmb[i].smb2.name,0,0,0,cmnt))
            ctrade.Buy(MxSmb[i].smb3.lot,MxSmb[i].smb3.name,0,0,0,cmnt);         
         }           
         break;
      }      
      return(openflag);
   }

Geralmente, as funções acima estão localizadas nos arquivos separados fnCalcDelta.mqh, fnMagicGet.mqh e fnOpen.mqh.

Então, nós encontramos o triângulo necessário e enviamo-lo para a abertura. No MetaTrader 4 ou em contas de cobertura no MetaTrader 5, isso na verdade marca o fim do trabalho do expert advisor. Mas ainda temos de rastrear o sucesso da abertura do triângulo. Para fazer isso, eu não uso os eventos OnTrade e OnTradeTransaction, porque, em seu caso, não há nenhuma garantia de receber um bom resultado. Em vez deles, eu verifico o número de posições existentes.

Consideremos a função de controle de abertura da posição:

void fnOpenCheck(stThree &MxSmb[], int accounttype, int fh)
   {
      uchar cnt=0;       // Contador de posições abertas no triângulo
      ulong   tkt=0;     // Tick atual
      string smb="";     // Símbolo atual
      
      // Verificamos nossa matriz de triângulos
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // Analisamos apenas os triângulos com status 1, ou seja, enviados para abertura
         if(MxSmb[i].status!=1) continue;
                          
         if ((TimeCurrent()-MxSmb[i].timeopen)>MAXTIMEWAIT)
         {     
            // Se exceder o tempo previsto para a abertura, rotularemos o triângulo como um pronto para ser fechado         
            MxSmb[i].status=3;
            Print("Not correct open: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name);
            continue;
         }
         
         cnt=0;
         
         switch(accounttype)
         {
            case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
            
            // Verificamos todas as posições abertas. Esta verificação é feita para cada triângulo. 

            for(int j=PositionsTotal()-1;j>=0;j--)
            if (PositionSelectByTicket(PositionGetTicket(j)))
            if (PositionGetInteger(POSITION_MAGIC)==MxSmb[i].magic)
            {
               // Obtemos o símbolo e bilhete da posição em consideração. 

               tkt=PositionGetInteger(POSITION_TICKET);                smb=PositionGetString(POSITION_SYMBOL);                               // Verificamos se, no triângulo examinado, está a posição atual entre as que precisamos.                // Se houver, então podemos aumentar o contador e memorizamos o bilhete e preço de abertura.                if (smb==MxSmb[i].smb1.name){ cnt++;   MxSmb[i].smb1.tkt=tkt;  MxSmb[i].smb1.price=PositionGetDouble(POSITION_PRICE_OPEN);} else                if (smb==MxSmb[i].smb2.name){ cnt++;   MxSmb[i].smb2.tkt=tkt;  MxSmb[i].smb2.price=PositionGetDouble(POSITION_PRICE_OPEN);} else                if (smb==MxSmb[i].smb3.name){ cnt++;   MxSmb[i].smb3.tkt=tkt;  MxSmb[i].smb3.price=PositionGetDouble(POSITION_PRICE_OPEN);}                               // Se encontrarmos as três posições necessárias, nosso triângulo será aberto com sucesso. Mudamos seu status para 2 (aberta).                // Gravamos os dados sobre a abertura no arquivo de log                if (cnt==3)                {                   MxSmb[i].status=2;                   fnControlFile(MxSmb,i,fh);                   break;                  }             }             break;             default:             break;          }       }    }

A a função de gravação no arquivo de log usada é simples:

void fnControlFile(stThree &MxSmb[],int i, int fh)
   {
      FileWrite(fh,"============");
      FileWrite(fh,"Open:",MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name);
      FileWrite(fh,"Tiket:",MxSmb[i].smb1.tkt,MxSmb[i].smb2.tkt,MxSmb[i].smb3.tkt);
      FileWrite(fh,"Lot",DoubleToString(MxSmb[i].smb1.lot,MxSmb[i].smb1.digits_lot),DoubleToString(MxSmb[i].smb2.lot,MxSmb[i].smb2.digits_lot),DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot));
      FileWrite(fh,"Margin",DoubleToString(MxSmb[i].smb1.mrg,2),DoubleToString(MxSmb[i].smb2.mrg,2),DoubleToString(MxSmb[i].smb3.mrg,2));
      FileWrite(fh,"Ask",DoubleToString(MxSmb[i].smb1.tick.ask,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tick.ask,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tick.ask,MxSmb[i].smb3.digits));
      FileWrite(fh,"Bid",DoubleToString(MxSmb[i].smb1.tick.bid,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tick.bid,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tick.bid,MxSmb[i].smb3.digits));               
      FileWrite(fh,"Price open",DoubleToString(MxSmb[i].smb1.price,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.price,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.price,MxSmb[i].smb3.digits));
      FileWrite(fh,"Tick value",DoubleToString(MxSmb[i].smb1.tv,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tv,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tv,MxSmb[i].smb3.digits));
      FileWrite(fh,"Spread point",DoubleToString(MxSmb[i].smb1.sppoint,0),DoubleToString(MxSmb[i].smb2.sppoint,0),DoubleToString(MxSmb[i].smb3.sppoint,0));
      FileWrite(fh,"Spread $",DoubleToString(MxSmb[i].smb1.spcost,3),DoubleToString(MxSmb[i].smb2.spcost,3),DoubleToString(MxSmb[i].smb3.spcost,3));
      FileWrite(fh,"Spread all",DoubleToString(MxSmb[i].spread,3));
      FileWrite(fh,"PL Buy",DoubleToString(MxSmb[i].PLBuy,3));
      FileWrite(fh,"PL Sell",DoubleToString(MxSmb[i].PLSell,3));      
      FileWrite(fh,"Magic",string(MxSmb[i].magic));
      FileWrite(fh,"Time open",TimeToString(MxSmb[i].timeopen,TIME_DATE|TIME_SECONDS));
      FileWrite(fh,"Time current",TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS));
      
      FileFlush(fh);       
   }

Bem, encontramos o triângulo para entrar, abrimos com sucesso a posição com base nele. Agora temos que calcular o quanto ganhamos graças a ele.

void fnCalcPL(stThree &MxSmb[], int accounttype, double prft)
   {
      // Novamente revemos nossa matriz de triângulos. 
      // A velocidade de abertura e fechamento são pontos extremamente importantes desta estratégia. 
      // Então, assim que encontrarmos o triângulo para fechamento, realizamos o fechamento imediatamente.
      
      bool flag=cterm.IsTradeAllowed() & cterm.IsConnected();      
      
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for
         // Estamos apenas interessado no triângulos com o status 2 ou 3.
         // Estado 3 - fechar o triângulo - pode ser obtido se o triângulo não é completamente aberto
         if(MxSmb[i].status==2 || MxSmb[i].status==3); else continue;                             
         
         // Calculamos ganhou o triângulo 
         if (MxSmb[i].status==2)
         {
            MxSmb[i].pl=0;         // Redefinimos o lucro
            switch(accounttype)
            {//switch
               case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:  
                
               if (PositionSelectByTicket(MxSmb[i].smb1.tkt)) MxSmb[i].pl=PositionGetDouble(POSITION_PROFIT);
               if (PositionSelectByTicket(MxSmb[i].smb2.tkt)) MxSmb[i].pl+=PositionGetDouble(POSITION_PROFIT);
               if (PositionSelectByTicket(MxSmb[i].smb3.tkt)) MxSmb[i].pl+=PositionGetDouble(POSITION_PROFIT);                           
               break;
               default:
               break;
            }//switch
            
            // Redondeamos para 2 caracteres
            MxSmb[i].pl=NormalizeDouble(MxSmb[i].pl,2);
            
            // No encerramento deixamo-nós estar um pouco mais. Eu costumo usar a seguinte lógica:
            // situação de arbitragem não é normal e não deve ocorrer, ou seja, quando ela surge podemos esperar que volte para o 
            // status quando não havia arbitragem. Será que vamos ser capazes de ganhar? Em outras palavras, não podemos dizer se 
            // continua ou não a obtenção do lucro. Então eu prefiro fechar a posição imediatamente após cobertos a comissão e o spread. 
            // O cálculo na arbitragem triangular é sobre os pontos, aqui não tem que contar com grandes movimentos. 
            // No entanto, na variável "Comissão", nos parâmetros de entrada, você pode aguardar a chegada do lucro desejado. 
            // Assim, se nós ganhamos mais do que gastamos, atribuímos à posição o status de "enviar para o fechamento".

            if (flag && MxSmb[i].pl>prft) MxSmb[i].status=3;                    
         }
         
         // Encerramento do triângulo somente se permitido o comércio.
         if (flag && MxSmb[i].status==3) fnCloseThree(MxSmb,accounttype,i); 
      }//for         
   }

Uma função simples responde pelo fechamento do triângulo:

void fnCloseThree(stThree &MxSmb[], int accounttype, int i)
   {
      // Antes de encerrar, é necessário verificar a disponibilidade de todos os pares no triângulo. 
      // Quebrar o triângulo é muito errado e perigoso, e se você trabalhar na conta de compensação,
      // mais tarde será gerada confusão nas posições. 
      
      if(fnSmbCheck(MxSmb[i].smb1.name))
      if(fnSmbCheck(MxSmb[i].smb2.name))
      if(fnSmbCheck(MxSmb[i].smb3.name))          
      
      // Se tudo estiver disponível, fechamos todas as 3 posições usando a biblioteca padrão. 
      // Após o fechamento, é necessário verificar o sucesso da ação. 
      switch(accounttype)
      {
         case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:     
         
         ctrade.PositionClose(MxSmb[i].smb1.tkt);
         ctrade.PositionClose(MxSmb[i].smb2.tkt);
         ctrade.PositionClose(MxSmb[i].smb3.tkt);              
         break;
         default:
         break;
      }       
   }  

Praticamente, temos chegado quase ao fim, resta verificar o sucesso do fechamento e imprimir qualquer coisa na tela. Quando o robô não escreve nada, é provável que não este funcionando.

Aqui está nossa verificação do sucesso do fechamento. A propósito, para abertura e fechamento pode ser implementada uma única função, simplesmente mudando a direção do negócio, mas não gosto muito desta opção, porque há ainda umas pequenas diferenças processuais entre a abertura e o fechamento. 

Verificamos o sucesso do fechamento: 

void fnCloseCheck(stThree &MxSmb[], int accounttype,int fh)
   {
      // Percorremos a matriz de triângulos. 
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // Estamos interessados ​​em apenas aqueles cujo status = 3, ou seja, já fechados ou enviados para fechamento. 
         if(MxSmb[i].status!=3) continue;
         
         switch(accounttype)
         {
            case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: 
            
            // O fato de não conseguir identificar um único par de todo o triângulo indica que encerramos com sucesso. Retornamos o status para 0
            if (!PositionSelectByTicket(MxSmb[i].smb1.tkt))
            if (!PositionSelectByTicket(MxSmb[i].smb2.tkt))
            if (!PositionSelectByTicket(MxSmb[i].smb3.tkt))
            {  // Significa  que encerramos com sucesso
               MxSmb[i].status=0;   
               
               Print("Close triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name+" magic: "+(string)MxSmb[i].magic+"  P/L: "+DoubleToString(MxSmb[i].pl,2));
               
                
               if (fh!=INVALID_HANDLE)
               {
                  FileWrite(fh,"============");
                  FileWrite(fh,"Close:",MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name);
                  FileWrite(fh,"Lot",DoubleToString(MxSmb[i].smb1.lot,MxSmb[i].smb1.digits_lot),DoubleToString(MxSmb[i].smb2.lot,MxSmb[i].smb2.digits_lot),DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot));
                  FileWrite(fh,"Tiket",string(MxSmb[i].smb1.tkt),string(MxSmb[i].smb2.tkt),string(MxSmb[i].smb3.tkt));
                  FileWrite(fh,"Magic",string(MxSmb[i].magic));
                  FileWrite(fh,"Profit",DoubleToString(MxSmb[i].pl,3));
                  FileWrite(fh,"Time current",TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS));
                  FileFlush(fh);               
               }                   
            }                  
            break;
         }            
      }      
   }

E, finalmente, imprimimos algo na forma de comentários na tela. Isso é um tipo de "cosmético" para acompanhamento visual. Exibimos o seguinte:

  1. Número de triângulos acompanhados
  2. Triângulos abertos
  3. os 5 triângulos mais mais próximo da abertura
  4. Triângulos abertos, se houver

Aqui está o código desta função:

void fnCmnt(stThree &MxSmb[], ushort lcOpenThree)
   {     
      int total=ArraySize(MxSmb);
      
      string line="=============================\n";
      string txt=line+MQLInfoString(MQL_PROGRAM_NAME)+": ON\n";
      txt=txt+"Total triangles: "+(string)total+"\n";
      txt=txt+"Open triangles: "+(string)lcOpenThree+"\n"+line;
      
      // Número máximo de triângulos que é exibido na tela
      short max=5;
      max=(short)MathMin(total,max);
      
      // Exibir os próximos 5 para abertura 
      short index[];                    // Matriz de índices
      ArrayResize(index,max);
      ArrayInitialize(index,-1);        // Não é usado
      short cnt=0,num=0;
      while(cnt<max && num<total)       // Para começar, tomamos os primeiros max dos índices fechados dos triângulos
      {
         if(MxSmb[num].status!=0)  {num++;continue;}
         index[cnt]=num;
         num++;cnt++;         
      }
      
      // Somente faz sentido classificar e buscar se há mais elementos do que é possível exibir na tela. 
      if (total>max) 
      for(short i=max;i<total;i++)
      {
         // Os triângulos abertos são mostrados abaixo.
         if(MxSmb[i].status!=0) continue;
         
         for(short j=0;j<max;j++)
         {
            if (MxSmb[i].PLBuy>MxSmb[index[j]].PLBuy)  {index[j]=i;break;}
            if (MxSmb[i].PLSell>MxSmb[index[j]].PLSell)  {index[j]=i;break;}
         }   
      }
      
      // Exibimos os triângulos mais próximos da abertura.
      bool flag=true;
      for(short i=0;i<max;i++)
      {
         cnt=index[i];
         if (cnt<0) continue;
         if (flag)
         {
            txt=txt+"Smb1           Smb2           Smb3         P/L Buy        P/L Sell        Spread\n";
            flag=false;
         }         
         txt=txt+MxSmb[cnt].smb1.name+" + "+MxSmb[cnt].smb2.name+" + "+MxSmb[cnt].smb3.name+":";
         txt=txt+"      "+DoubleToString(MxSmb[cnt].PLBuy,2)+"          "+DoubleToString(MxSmb[cnt].PLSell,2)+"            "+DoubleToString(MxSmb[cnt].spread,2)+"\n";      
      }            
      
      // Exibimos os triângulos abertos. 
      txt=txt+line+"\n";
      for(int i=total-1;i>=0;i--)
      if (MxSmb[i].status==2)
      {
         txt=txt+MxSmb[i].smb1.name+"+"+MxSmb[i].smb2.name+"+"+MxSmb[i].smb3.name+" P/L: "+DoubleToString(MxSmb[i].pl,2);
         txt=txt+"  Time open: "+TimeToString(MxSmb[i].timeopen,TIME_DATE|TIME_MINUTES|TIME_SECONDS);
         txt=txt+"\n";
      }   
      Comment(txt);
   }

Teste


Pode-se, por curiosidade, realizar o teste no modo de simulação de ticks, comparar com o teste em ticks reais. É possível ir ainda mais longe, ou seja, comparar os resultados de teste em ticks reais com o trabalho real, e concluir que o multitestador está muito longe da realidade. 

Os resultados de trabalho mostram que se pode contar com 3-4 negócios por semana, em média. Na maioria das vezes, a posição é aberta durante a noite e, como regra, no triângulo há moedas menos líquidas, como TRY, NOK, SEK e outras similares. O lucro do robô depende do volume negociado, e como os trades ocorrem com pouca frequência, este expert advisor pode facilmente manipular grandes quantidades, operando em paralelo com outros robôs.

O risco do robô pode ser calculado facilmente: 3 spreads * número de triângulos abertos.

Para a preparação dos pares de moedas que podem ser usados para trabalhar, eu recomendo, em primeiro lugar, abrir todos os símbolos e, em seguida, ocultar aqueles cuja negociação é proibida e que não são pares de moedas. Esta operação pode ser realizada mais rapidamente usando o script: https://www.mql5.com/pt/market/product/25256

Lembre que o histórico no testador não é baixado a partir do servidor da corretora, uma vez que ele tem que ser pré-carregado no terminal do cliente. Por isso, isso deve ser feito quer por conta própria antes do iniciar o teste ou re-utilizando o script acima.

Perspetivas de desenvolvimento

Será que pode ser aprimorado o desempenho?  Claro que é possível. Para fazer isso, você precisa fazer seu agregador de liquidez, mas a desvantagem dessa abordagem é a necessidade de abrir contas em várias corretoras.

Também pode-se acelerar os resultados do teste. Para fazer isso, existem duas maneiras que se podem combinar. O primeiro passo é realizar o cálculo discreto, constantemente rastreando apenas os triângulos onde a probabilidade de entrada é muito alta. A segunda maneira consiste em usar OpenCL, o que é muito razoável para este robô.

Arquivos usados no artigo

Nome do arquivo Descrição
1 var.mqh Descritas todas as variáveis, defines e inputs.
2 fnWarning.mqh Verificação das condições iniciais para o funcionamento correto do Expert Advisor: variáveis ​​de entrada, configurações, ambiente.
3 fnSetThree.mqh Formamos triângulos de pares de moedas. Aqui é selecionada a fonte dos pares, isto é, Observação do mercado ou um arquivo previamente preparado.
4 fnSmbCheck.mqh Função para verificar se o símbolo está disponível para negociação e outras restrições. NB: Sessões de negociação e cotação não são verificadas no robô
5 fnChangeThree.mqh Alteramos a localização dos pares de moedas no triângulo, de modo que sejam construídos seguindo o mesmo princípio.
6 fnSmbLoad.mqh Transferimos diferentes dados de símbolos, preços, pontos, restrições de volumes e assim por diante.
7 fnCalcDelta.mqh Calculamos todas as separações no triângulo, aqui consideramos todo os gastos adicionais: spread, comissão, derrapagem.
8 fnMagicGet.mqh Procuramos o magic que pode ser usado para o triângulo atual
fnOpenCheck.mqh Verifique se o triângulo foi aberto com sucesso
10 fnCalcPL.mqh  Calculamos o lucro/prejuízo do triângulo
11  fnCreateFileSymbols.mqh A função que cria um arquivo com triângulos para operar. No arquivo há mais e mais dados (para fins de informação).
12  fnControlFile.mqh Função que realiza o arquivo de log. Aqui estão descritas todas as aberturas e fechamentos com os dados necessários. 
13  fnCloseThree.mqh Fechamento do triângulo 
14  fnCloseCheck.mqh Verificamos se o triângulo foi fechado completamente
15  fnCmnt.mqh Exibição de comentários na tela 
16  fnRestart.mqh Ao iniciar o robô, verificamos se há um triângulo previamente aberto, e se houver, continuamos imprimindo-os. 
17  fnOpen.mqh Abrimos o triângulo
18 Support.mqh Classe de apoio adicional. Nela há apenas uma função que se encarrega de contar o número de casas decimais do número fracionário.
19 head.mqh Este arquivo descreve os cabeçalhos de todos os arquivos acima.

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/3150

Arquivos anexados |
MQL5.ZIP (235.99 KB)
Últimos Comentários | Ir para discussão (5)
Fabio Rocha
Fabio Rocha | 27 set 2021 em 02:42
Olá meus parabéns pelo EA, mas eu percebi que em conta demonstrativa ele não abre ordens, deixei mais de uma semana e não abriu nenhuma ordem, existe algo que eu possa fazer para resolver isto?
Alexey Oreshkin
Alexey Oreshkin | 27 set 2021 em 09:31
Fabio Rocha #:
Olá meus parabéns pelo EA, mas eu percebi que em conta demonstrativa ele não abre ordens, deixei mais de uma semana e não abriu nenhuma ordem, existe algo que eu possa fazer para resolver isto?

Boa tarde. O código do EA está aberto, você precisa olhar nele. Eu não uso mais essa estratégia.

sergio.carlos
sergio.carlos | 14 abr 2023 em 21:20
Alexey Oreshkin #:

Boa tarde. O código do EA está aberto, você precisa olhar nele. Eu não uso mais essa estratégia.

Olá, Qual a razão de não utilizar mais essa estratégia, ela não se mostrou lucrativa?
Qual estrategia esta utilizando no momento?

Fernando Carreiro
Fernando Carreiro | 14 abr 2023 em 21:25
@sergio.carlos #: hello, for what reason do you not use this strategy and which one are you using at the moment?

Este fórum é em Português, não Inglês. Por favor honrar o idioma.

sergio.carlos
sergio.carlos | 15 abr 2023 em 16:56
Fernando Carreiro #:

Este fórum é em Português, não Inglês. Por favor honrar o idioma.

Opá fernando, desculpe mesmo.

Eu sou brasileiro mas estou acostumado em usar a tradução do google nas paginas dos foruns que nem percebi que esse era em português.

Mas obrigado pela correção.

Lógica Difusa nas estratégias de negociação Lógica Difusa nas estratégias de negociação
O artigo considera um exemplo de aplicação da lógica difusa para construir um sistema de negociação simples, usando a biblioteca Fuzzy. São propostas melhorias ao sistema através da combinação da lógica difusa, algoritmos genéticos e redes neurais.
Uma Nova Abordagem para a Interpretação da Divergência Clássica e Oculta Uma Nova Abordagem para a Interpretação da Divergência Clássica e Oculta
O artigo considera o método clássico para a construção de divergências e fornece um método adicional de interpretação de divergência. Uma estratégia de negociação foi desenvolvida com base neste novo método de interpretação. Esta estratégia também é descrita no artigo.
Comparação de diferentes tipos de médias móveis durante a negociação Comparação de diferentes tipos de médias móveis durante a negociação
São examinados 7 tipos de médias móveis (MA), é criada uma estratégia de negociação para trabalhar com eles. É levado a cabo o teste e comparação de diferentes MA numa mesma estratégia de negociação, são apresentadas as características comparativas quanto a eficiência de cada média móvel.
Otimizando uma estratégia usando o gráfico do saldo e comparando os resultados com o critério "Balance + max Sharpe Ratio" Otimizando uma estratégia usando o gráfico do saldo e comparando os resultados com o critério "Balance + max Sharpe Ratio"
Neste artigo, nós ainda consideramos um outro critério personalizado de otimização de uma estratégia de negociação com base na análise do gráfico de saldo. A regressão linear é calculada usando a função da biblioteca ALGLIB.