Arbitraje triangular

15 noviembre 2017, 14:01
Alexey Oreshkin
0
2 149

Descripción de la idea

El tema del arbitraje triangular se aborda con una periodicidad envidiable en los foros temáticos de la Red. ¿Pero de qué se trata?

La palabra «arbitraje» en este término supone una cierta neutralidad respecto al mercado. La palabra «triangular» significa que la cartera se compone de tres instrumentos financieros.

Vamos a usar el ejemplo más popular: el triangulo «euro — libra — dólar». En los pares de divisas, se escribe de la siguiente manera: EURUSD + GBPUSD + EURGBP. La neutralidad requerida consiste en intentar comprar y vender simultáneamente los mismos símbolos, obteniendo el beneficio como resultado. 

Eso se ve de la siguiente manera. Cualquier par de este ejemplo se representa a través de otros dos:

EURUSD=GBPUSD*EURGBP,

o GBPUSD=EURUSD/EURGBP,

o EURGBP=EURUSD/GBPUSD.

Todas estas variantes son idénticas, más abajo estudiaremos en detalle la selección de alguna de ellas. Por ahora nos centraremos en la primera variante.

Ahora nos aclararemos con los precios Bid y Ask. La secuencia de acciones será la siguiente:

  1. Compramos EURUSD, es decir, usamos el precio ask. En el balance tenemos más euros y menos dólares. 
  2. Expresamos EURUSD a través de otros pares.
  3. GBPUSD: aquí no hay euros, pero hay dólares, y los tenemos que vender. Para vender dólares en GBPUSD, hay que comprar este par. Por eso usamos ask. Al realizar la compra, obtenemos más libra y menos dólar en el balance.
  4. EURGBP: tenemos que comprar el euro, y vender la libra que no necesitamos. Compramos EURGBP, usamos ask. En el balance, tenemos más euros y menos libras. Todo se concuerda.

Al final, tenemos lo siguiente: (ask) EURUSD = (ask) GBPUSD * (ask) EURGBP. Hemos obtenido la igualdad necesaria. Para ganar con eso, tenemos que comprar un lado y vender el otro. Aquí, hay dos opciones:

  1. Comprar EURUSD más barato de lo que podemos vender, pero expresado de otra manera: (ask) EURUSD < (bid) GBPUSD * (bid) EURGBP 
  2. Vender EURUSD más caro de lo que podemos comprar, pero expresado de otra manera: (bid) EURUSD > (ask) GBPUSD * (ask) EURGBP 

Nos queda sólo encontrar esta situación y ganar con ella.

Nótese: podemos componer el triángulo de otra manera, desplazando los tres pares a un lado y comparando con 1. Todas las variantes son idénticas, pero la opción descrita más arriba, según mi opinión, se comprende y se explica mejor.

Otra cosa importante: siguiendo esta situación, podemos buscar el momento para la compra y la venta simultánea. En este caso, el beneficio será instantáneo, pero estos momentos ocurren muy raras veces.
Con poca más frecuencia, sucede la situación cuando podemos comprar un lado más barato que otro, pero no podemos vender ahora mismo con ganancia. Entonces, esperamos a que este desequilibrio desaparezca. Encontrarse en la operación no nos supone ningún peligro, porque nuestra posición es casi nula, o sea, estamos fuera del mercado. Pero hay que entender de dónde aparece este «casi». Para una alineación ideal de los volúmenes comerciales, hace falta una precisión que no está disponible para nosotros. Voy a recordar que en mayoría de los casos los volúmenes comerciales se redondean hasta 2 dígitos tras la coma, y éste es un redondeo muy burdo para nuestra estrategia.

Pues, nos hemos aclarado con la teoría, ha llegado el momento para escribir un robot. El Asesor Experto (EA) está escrito en el estilo procesal, por eso lo entienden tanto los programadores novatos, como aquéllos a quienes no les gusta la POO. 


Descripción breve del robot

Primero, creamos todos los triángulos posibles, los colocamos correctamente y obtenemos todos los datos necesarios sobre cada par de divisas.

Toda esta información se almacena en el array de estructuras MxThree. Cada triángulo tiene el campo status. Su valor inicial es igual a 0. Si hay que abrir el triángulo, al estatus se le asigna el valor 1. Una vez confirmado el hecho de que el triángulo se ha abierto por completo, su estatus se cambia por 2. Si el triángulo no se ha abierto completamente o hay que cerrarlo, el estatus se cambia por 3. En cuanto el triángulo se cierre con éxito, el estatus se vuelve de nuevo a la posición 0.

El EA escribe la apertura y el cierre de los triángulos en el archivo log, el cual permite comprobar la corrección de las acciones y recuperar el historial. El nombre del archivo es Three Point Arbitrage Control YYYY.DD.MM.csv.

Para realizar la prueba, cargue todos los pares de divisas necesarias en el Probador de estrategias. Por eso, antes de iniciar el Probador, hay que iniciar el robot en el modo Create file with symbols. Si no hay este archivo, el robot realizará la prueba por el triángulo predefinido EUR+GBP+USD. 


Variables usadas

El código de cualquier robot en mi ejecución se empieza con la inclusión del archivo de cabecera. Ahí se enumeran todos los archivos incluidos, librerías, etc. Este robot no es una excepción: después del bloque de la descripción vemos la línea #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"

Por ahora, el lector no comprende muy bien esta lista, pero el artículo está escrito en el modo de seguimiento del código, por eso la estructura del programa no se altera. Según vayamos leyendo, todo se quedará bien claro. Todas las funciones, clases, unidades del código están colocadas en archivos separados por motivos de conveniencia. Cada archivo de inclusión, salvo la librería estándar, se empieza con la línea #include "head.mqh". Eso permite usar IntelliSense en los archivos de inclusión y no guardar en la memoria los nombres de todas las entidades necesarias.

Luego, incluimos el archivo para el Probador. No se puede hacer eso en ningún otro sitio, por eso lo declaramos aquí. Esta línea es necesaria para cargar los símbolos en el Probador multidivisas:

#property tester_file FILENAME

A continuación, describiremos las variables que se utilizan en el programa. Su descripción también se encuentra en el archivo separado var.mqh:

// macros
#define DEVIATION       3                                                                 // Deslizamiento máximo permitido
#define FILENAME        "Three Point Arbitrage.csv"                                       // Aquí se almacenan los símbolos para el trabajo
#define FILELOG         "Three Point Arbitrage Control "                                  // Parte del nombre del archivo log
#define FILEOPENWRITE(nm)  FileOpen(nm,FILE_UNICODE|FILE_WRITE|FILE_SHARE_READ|FILE_CSV)  // Apertura del archivo para la escritura
#define FILEOPENREAD(nm)   FileOpen(nm,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV)   // Apertura del archivo para la lectura
#define CF              1.2                                                               // Coeficiente de aumento para el margen
#define MAGIC           200                                                               // Diapasón de los magic usados
#define MAXTIMEWAIT     3                                                                 // Tiempo máximo de la espera de la apertura del triángulo en segundos

// estructura para el par de divisas
struct stSmb
   {
      string            name;            // Par de divisas
      int               digits;          // Número de dígitos después de la coma en la cotización
      uchar             digits_lot;      // Número de dígitos después de la coma en el lote, para el redondeo
      int               Rpoint;          // 1/point, para multiplicar por este valor y no dividir en las fórmulas
      double            dev;             // Posible deslizamiento. Pasamos inmediatamente en el número de puntos
      double            lot;             // Volumen del trading para el par de divisas
      double            lot_min;         // Volumen mínimo
      double            lot_max;         // Volumen máximo
      double            lot_step;        // Paso del lote
      double            contract;        // Tamaño del contrato
      double            price;           // Precio de la apertura del par en el triángulo. Es necesaria para la compensación (netting)
      ulong             tkt;             // Ticket de la orden con la que ha sido abierta la transacción. Es necesario sólo para la comodidad en las cuentas de cobertura (hedging)
      MqlTick           tick;            // Precios actuales del par
      double            tv;              // Coste actual del tick
      double            mrg;             // Margen actual necesario para la apertura
      double            sppoint;         // Spread en puntos enteros
      double            spcost;          // Spread en dinero para el lote actual que se abre
      stSmb(){price=0;tkt=0;mrg=0;}   
   };

// Estructura para el triángulo
struct stThree
   {
      stSmb             smb1;
      stSmb             smb2;
      stSmb             smb3;
      double            lot_min;          // Volumen mínimo para el triángulo entero
      double            lot_max;          // Volumen máximo para el triángulo entero     
      ulong             magic;            // Magic del triángulo
      uchar             status;           // Estatus del triángulo. 0 - no se usa. 1 - enviado a la apertura. 2 - abierto con éxito. 3 - enviado al cierre.
      double            pl;               // Beneficio del triángulo
      datetime          timeopen;         // Hora del envío del triángulo para la apertura
      double            PLBuy;            // Cuánto se puede ganar en caso de comprar el triángulo
      double            PLSell;           // Cuánto se puede ganar en caso de vender el triángulo
      double            spread;           // Coste total de todos los spread (¡con comisión!)
      stThree(){status=0;magic=0;}
   };

  
// Modos de trabajo del EA  
enum enMode
   {
      STANDART_MODE  =  0, /*Symbols from Market Watch*/                  // Modo normal. Símbolos de la Observación del mercado
      USE_FILE       =  1, /*Symbols from file*/                          // Usar el archivo de símbolos
      CREATE_FILE    =  2, /*Create file with symbols*/                   // Crear el archivo para el Probador o para el trabajo
      //END_ADN_CLOSE  =  3, /*Not open, wait profit, close & exit*/      // Cerrar todas sus transacciones y terminar el trabajo
      //CLOSE_ONLY     =  4  /*Not open, not wait profit, close & exit*/
   };


stThree  MxThree[];           // Archivo principal donde se almacenan los triángulos de trabajo y todos los datos necesarios

CTrade         ctrade;        // Clase CTrade de la librería estándar
CSymbolInfo    csmb;          // Clase CSymbolInfo de la librería estándar
CTerminalInfo  cterm;         // Clase CTerminalInfo de la librería estándar

int         glAccountsType=0; // Tipo de cuenta: cobertura o compensación
int         glFileLog=0;      // Handle del archivo log


// Parámetros de entrada

sinput      enMode      inMode=     0;          // Modo de trabajo
input       double      inProfit=   0;          // Comisión
input       double      inLot=      1;          // Volumen comercial
input       ushort	inMaxThree= 0;          // Triángulos abiertos
sinput      ulong       inMagic=    300;        // Magic del asesor experto
sinput      string      inCmnt=     "R ";       // Comentario

Primero, van las directivas define, son simples y van acompañadas con comentarios. No creo que surjan problemas con su comprensión.

Luego van dos estructuras stSmb y stThree. Su lógica es la siguiente: cualquier triángulo se compone de tres pares de divisas. Por eso, al describir uno de ellos una vez, y al usarlo tres veces, obtenemos el triángulo. stSmb es la estructura que describe el par de divisas y su especificación: posibles volúmenes comerciales, variables _Digits y _Point, precios actuales en el momento de la apertura, etc. Mientras que en la estructura stThree se utiliza tres veces stSmb, precisamente éste es nuestro triángulo. Además, aquí han sido añadidas algunas propiedades referentes únicamente al triángulo: beneficio actual, magic, hora de apertura, etc. Luego, van los modos del trabajo sobre los cuales hablaremos más tarde, así como las variables. Las variables de entrada también están descritas en los comentarios, pero hablaremos con más detalles sobre dos de ellas:

En el parámetro inMaxThree se guarda el número máximo posible de triángulos abiertos a la vez. Si es 0, no se usa. Por ejemplo, si el parámetro es 2, al mismo tiempo pueden estar abiertos sólo 2 triángulos.

El parámetro inProfit contiene el tamaño de la comisión si existe.


Configuración inicial

Pues bien, los archivos de inclusión y las variables usadas están descritos. Pasamos al bloque OnInint().

Antes de iniciar el EA, hay que comprobar la corrección de los parámetros introducidos y obtener los datos iniciales donde es necesario. Si todos está bien, empezamos el trabajo. Yo procuro establecer el mínimo de los ajustes de entrada en los EAs, y este robot tampoco es una excepción.

Sólo uno de seis parámetros de entrada puede imposibilitar el trabajo del EA —el volumen comercial. No podemos abrir las transacciones con un volumen negativo. El resto de los ajustes no afectan el trabajo correcto del EA. Las comprobaciones se realizan en la primera función del bloque OnInit().

Vamos a examinar su código.

void fnWarning(int &accounttype, double lot, int &fh)
   {   
      // Comprobamos la corrección del establecimiento del volumen comercial, no podemos tradear con un volumen negativo
      if (lot<0)
      {
         Alert("Trade volume < 0");  
         ExpertRemove();         
      }      
      
      // Si el volumen es igual a 0, advertimos que el robot va a usar automáticamente el volumen mínimo posible.
      if (lot==0) Alert("Always use the same minimum trading volume");  

Puesto que robot está escrito en el estilo procesal, habrá que tener varias variables globales. Una de ellas es el manejador (handle) del archivo log. El nombre se compone de una parte fija y la fecha del inicio del robot. Eso ha sido hecho para simplificar el control, para no tener que buscar luego dentro de un archivo a partir de qué momento se empieza el log para algún inicio. Nótese que el nombre se cambia cada vez durante el nuevo inicio, y el archivo con el mismo nombre (si existe) se elimina.

El EA utiliza dos archivos: el archivo con los triángulos encontrados (que se crea si el usuario ha elegido la opción correspondiente) y el archivo log en el que se escribe la hora de la aopertura y cierre del triángulo, los precios de apertura y alguna información adicional para mejorar el control. El archivo log se hace siempre.

      // Creamos el archivo log sólo si no tenemos seleccionado el modo de la creación del archivo de triángulos, porque en este caso no es necesario.                                  
      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");      
      }   
      
      // En la mayoría de los casos, el tamaño del contrato para los pares de divisas de los brókers es igual a 100000, aunque a veces hay excepciones.
      // Son casos tan raros que es mejor comprobar este valor durante el inicio, y si no es igual a 100000, avisar de ello,
      // para que el usuario tome decisión si es importante o no. Luego, seguirá trabajando sin describir más los momentos cuando 
      // en un triángulo se encuentren los pares con diferentes volúmenes del contrato.
      for(int i=SymbolsTotal(true)-1;i>=0;i--)
      {
         string name=SymbolName(i,true);
         
         // La función para comprobar la disponibilidad del símbolo para el trading también se usa en la construcción de los triángulos.
         // Ahí la estudiaremos con más detalles
         if(!fnSmbCheck(name)) continue;
         
         double cs=SymbolInfoDouble(name,SYMBOL_TRADE_CONTRACT_SIZE);
         if(cs!=100000) Alert("Attention: "+name+", contract size = "+DoubleToString(cs,0));      
      }
      
      // Obtenemos el tipo de la cuenta, de cobertura o de compensación
      accounttype=(int)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
   }

Construcción de triángulos

Para construir los triángulos, tenemos que analizar los siguientes momentos:

  1. La fuente de los triángulos es la ventana «Observación del mercado» o un archivo preparado de antemano.
  2. ¿Nos encontramos en el Probador de estrategias? Si es así, hay que cargar los símbolos en la Observación del mercado. No tiene sentido cargar todos los símbolos disponibles, porque un ordenador de casa normal y corriente simplemente no podrá con este trabajo. Vamos a buscar un archivo preparado previamente con los símbolos para el Probador. Si no lo tenemos, vamos a probar la estrategia usando el triángulo estándar: EUR+USD+GBP.
  3. Para simplificar el código, introduciremos algunas limitaciones: todos los símbolos del triángulo tienen que tener el mismo tamaño del contrato.
  4. No olvidemos que los triángulos pueden componerse sólo de los pares de divisas.

La primera función necesaria es la composición de los triángulos desde la Observación del mercado.

void fnGetThreeFromMarketWatch(stThree &MxSmb[])
   {
      // Obtenemos el número total de los símbolos
      int total=SymbolsTotal(true);
      
      // Variables para comparar los tamaños de los contratos    
      double cs1=0,cs2=0;              
      
      // En el primer ciclo, cogemos el primer símbolo de la lista
      for(int i=0;i<total-2 && !IsStopped();i++)    
      {//1
         string sm1=SymbolName(i,true);
         
         // Comprobamos el símbolo respecto a diferentes limitaciones
         if(!fnSmbCheck(sm1)) continue;      
              
         // Obtenemos el tamaño del contrato y en seguida lo normalizamos porque luego vamos a comparar este valor 
         if (!SymbolInfoDouble(sm1,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) continue; 
         cs1=NormalizeDouble(cs1,0);
         
         // Obtenemos la divisa base y la divisa del beneficio, porque la comparación vamos a realizar usando ellas y no el nombre del par
         string sm1base=SymbolInfoString(sm1,SYMBOL_CURRENCY_BASE);     
         string sm1prft=SymbolInfoString(sm1,SYMBOL_CURRENCY_PROFIT);
         
        // En el segundo ciclo, cogemos el siguiente símbolo de la 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);
            // El primer y el segundo par debe tener una coincidencia por cualquiera de divisas.
            // Si no es así, entonces no podemos componer el triángulo con ellos.    
            // Además, no tiene sentido realizar la comprobación de la coincidencia completa porque, por ejemplo, de 
            // eurusd y eurusd.xxx no se puede componer el triángulo.
            if(sm1base==sm2base || sm1base==sm2prft || sm1prft==sm2base || sm1prft==sm2prft); else continue;
                  
            // Los tamaños de los contratos tienen que ser los mismos            
            if (cs1!=cs2) continue;
            
            // En el tercer ciclo, buscamos el último símbolo para el 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 el primer y el segundo símbolo tienen la misma divisa. Para componer el triángulo, hay que encontrar
               // el tercer par cuya divisa coincida con cualquier divisa del primer par, y la otra,
               // con cualquier divisa del segundo. Si no hay coincidencia, este par no vale.
               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;
               
               // Si hemos llegado a este paso, entonces todas las comprobaciones han sido superadas, y podemos componer el triángulo usando estos pares encontrados
               // Lo escribimos en nuestro array
               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    
   }

La segunda función necesaria es la lectura de los triángulos desde el archivo

void fnGetThreeFromFile(stThree &MxSmb[])
   {
      // Si el archivo con los símbolos no ha sido encontrado, mostramos el mensaje sobre ello y terminamos el trabajo
      int fh=FileOpen(FILENAME,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV);
      if(fh==INVALID_HANDLE)
      {
         Print("File with symbols not read!");
         ExpertRemove();
      }
      
      // Nos movemos al principio del archivo
      FileSeek(fh,0,SEEK_SET);
      
      // Saltamos el encabezado (la primera línea del archivo)      
      while(!FileIsLineEnding(fh)) FileReadString(fh);
      
      
      while(!FileIsEnding(fh) && !IsStopped())
      {
         // Obtenemos tres símbolos del triángulo. Hacemos la comprobación base de la disponibilidad de los datos
         // El robot sabe componer el archivo con los triángulos automáticamente. Si el usuario
         // lo ha cambiado personalmente y de forma incorrecta, vamos a considerar que lo ha hecho con razón
         string smb1=FileReadString(fh);
         string smb2=FileReadString(fh);
         string smb3=FileReadString(fh);
         
         // Si los datos de los símbolos están disponible, pasando al final de la línea, los escribimos en nuestro array 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);
      }
   }

La última función que necesitamos en este apartado es el envoltorio de dos funciones anteriores. Se encarga de la selección de la fuente de los triángulos dependiendo de los ajustes de entrada del robot. Ahí mismo comprobamos dónde se inicia el robot. Si se inicia en el Probador, cargamos los triángulos desde el archivo independientemente de la selección del usuario. Si el archivo no existe, cargamos el triángulo predefinido EURUSD+GBPUSD+EURGBP.

void fnSetThree(stThree &MxSmb[],enMode mode)
   {
      // Reseteamos nuestro array de los triangulos
      ArrayFree(MxSmb);
      
      // Comprobamos si estamos en el Probador o no
      if((bool)MQLInfoInteger(MQL_TESTER))
      {
         // Si es así, buscamos el archivo de los símbolos e iniciamos la descarga de los triángulos desde el archivo
         if(FileIsExist(FILENAME)) fnGetThreeFromFile(MxSmb);
         
         // Si el archivo no ha sido encontrado, repasamos todos los símbolos disponibles y buscamos entre ellos el triángulo predefinido 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;
            }  
            
            // Después de cargar el triángulo predefinido en la Observación del mercado, iniciamos la composición deltriángulo         
            fnGetThreeFromMarketWatch(MxSmb);
         }
         return;
      }
      
      // Si no estamos en el Probador, vemos qué modo de trabajo ha sido elegido por el usuario: 
      // coger los símbolos desde la Observación del mercado o desde el archivo
      if(mode==STANDART_MODE || mode==CREATE_FILE) fnGetThreeFromMarketWatch(MxSmb);
      if(mode==USE_FILE) fnGetThreeFromFile(MxSmb);     
   }

Aquí, hemos usado una función auxiliar, fnSmbCheck(). En ella se comprueba si hay limitaciones para el trabajo con el símbolo. Si es así, lo saltamos. Es su código.

bool fnSmbCheck(string smb)
   {
      // El triángulo puede componerse sólo de los pares de divisas
      if(SymbolInfoInteger(smb,SYMBOL_TRADE_CALC_MODE)!=SYMBOL_CALC_MODE_FOREX) return(false);
      
      // Si hay limitaciones para el trading, saltamos este símbolo
      if(SymbolInfoInteger(smb,SYMBOL_TRADE_MODE)!=SYMBOL_TRADE_MODE_FULL) return(false);   
      
      // Si hay fecha del inicio o finalización del contrato, también omitimos porque las divisas no utilizan este parámetro
      if(SymbolInfoInteger(smb,SYMBOL_START_TIME)!=0)return(false);
      if(SymbolInfoInteger(smb,SYMBOL_EXPIRATION_TIME)!=0) return(false);
      
      // Disponibilidad de los tipos de órdenes. Aunque el robot tradea sólo con órdenes de mercado, igualmente no tiene que haber limitaciones
      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);
       
      // Comprobación de la disponibilidad de datos usando la librería estándar         
      if(!csmb.Name(smb)) return(false);
      
      // La comprobación de abajo es necesaria sólo en el trabajo real, ya que a veces, por alguna razón, SymbolInfoTick trabaja, los precio parece que  
      // hayan sido obtenidos, pero en realidad el bid=0.
      // En el Probador, desactivamos, porque ahí los precios pueden aparecer más 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);
   }

Pues bien, los triángulos han sido compuestos. Las funciones para su construcción han sido colocadas en el archivo de inclusión fnSetThree.mqh. La función para comprobar las limitaciones del símbolo ha sido colocada en el archivo separado fnSmbCheck.mqh.

Hemos compuesto todos los triángulos posibles. Los pares dentro de ellos pueden ubicarse en un orden aleatorio, y eso provoca muchas inconveniencias, ya que necesitamos determinar cómo expresar un par de divisas a través de otros. Para arreglar esta situación, veremos todas las opciones disponibles usando el ejemplo eur-usd-gbp:

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' = acción inválida

En esta tabla podemos observar que el triángulo puede componerse de 6 modos diferentes, pero dos de ellos —líneas 4 y 6— no permiten expresar el primer símbolo a través de dos restantes. Entonces, estas dos opciones no valen. Otras 4 variantes son idénticas. No importa qué símbolo y a través de qué demás expresamos, pero nos importa la velocidad. La operación de la división puede ser más lenta que la multiplicación, por eso rechazamos la opción 3 y 5. Nos quedan dos variantes: las líneas 1 y 2.

Vamos a usar la variante de la línea 2 por motivos de la mejor persepción. De esta manera, no tendremos que introducir en el robot los campos adicionales para el primer, el segundo y el tercer símbolo. Aparte es imposible, porque tradeamos no con un triángulo, sino con todos los posibles.

La conveniencia de nuestra elección consiste en lo siguiente: ya que operamos con el arbitraje, y esta estrategia supone una posición neutral, entonces tenemos que comprar y vender lo mismo. Ejemplo: Buy 0.7 del lote EURUSD y Sell 0.7 del lote EURGBP — hemos comprado y vendido 70000€. Es decir, tenemos una posición a pesar de que estamos fuera del mercado porque en la compra y en la venta figuraba el mismo volumen, pero expresado en monedas diferentes. Tenemos que corregirlos, realizando la transacción por GBPUSD. En otras palabras, sabemos de antemano que los símbolos 1 y 2 tienen que tener el volumen igual, pero la dirección diferente. También se sabe de antemano que el volumen del tercer par es igual al precio del segundo par.

Es la función que coloca los pares dentro del triángulo de forma correcta:

void fnChangeThree(stThree &MxSmb[])
   {
      int count=0;
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for         
         // Primero, veremos qué es lo que está el tercer lugar. 
         // Es el par cuya divisa base no coincide con otras dos divisas base
         string sm1base="",sm2base="",sm3base="";
         
         // Si por alguna razón no hemos podido obtener la divisa base, no usamos este triángulo en el trabajo
         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;}
                  
         // Si la divisa base del 1 y el 2 símbolo coincide, saltamos este paso. Si no, alternamos los pares
         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;
            }
         }
         
         // Ahora, determinamos el primer y el segundo puesto. 
         // En el segundo puesto se encuentra el par cuya divisa de beneficio coincide con la divisa base del tercer par. 
         // En este caso, siempre usamos la multiplicación.
         sm3base=SymbolInfoString(MxSmb[i].smb3.name,SYMBOL_CURRENCY_BASE);
         string sm2prft=SymbolInfoString(MxSmb[i].smb2.name,SYMBOL_CURRENCY_PROFIT);
         
         // Alternamos el primer y el segundo par. 
         if(sm3base!=sm2prft)
         {
            string temp=MxSmb[i].smb1.name;
            MxSmb[i].smb1.name=MxSmb[i].smb2.name;
            MxSmb[i].smb2.name=temp;
         }
         
         // Mostramos el mensaje sobre el triángulo procesado. 
         Print("Use triangle: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name);
         count++;
      }//
      // Avisamos sobre el número total de los triángulos en el trabajo. 
      Print("All used triangles: "+(string)count);
   }

La función se ubica por completo en el archivo separado fnChangeThree.mqh.

Y el último paso necesario para terminar la preparación de los triángulos es el siguiente: cargamos desde el principio todos los datos para los pares utilizados, para luego no perder el tiempo para acceder a ellos. Necesitaremos lo siguiente:

  1. volumen máximo y mínimo del trading para cada símbolo;
  2. número de dígitos en el precio y en el volumen para el redondeo;
  3. variable Point y Ticksize. No me he encontrado con las situaciones cuando los pares de divisas los tenían diferentes, pero de cualquier manera, vamos a obtener todos los datos y usarlos donde es preciso.
void fnSmbLoad(double lot,stThree &MxSmb[])
   {
      
      // Una simple macro par el print   
      #define prnt(nm) {nm="";Print("NOT CORRECT LOAD: "+nm);continue;}
      
      // Recorremos en el ciclo todos los triángulos construidos. Aquí tendremos el consumo excesivo del tiempo para las solicitudes repetidas para los 
      // mismos símbolos, pero dado a que esta operación se ejecuta sólo durante el inicio del robot, para reducir el código podemos hacer así.
      // Para obtener los datos, usamos la librería estándar. 
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // Cargando el símbolo en la clase CSymbolInfo, inicializamos la recopilación de todos los datos necesarios
         // y de paso comprobamos su disponibilidad. Si algo falla, marcamos el triángulo como inválido.                  
         if (!csmb.Name(MxSmb[i].smb1.name))    prnt(MxSmb[i].smb1.name); 
         
         // Hemos obtenido la dimensionalidad para cada símbolo
         MxSmb[i].smb1.digits=csmb.Digits();
         
         //Pasamos el deslizamiento de puntos enteros en decimales. Vamos a necesitar este formato para los siguientes cálculos
         MxSmb[i].smb1.dev=csmb.TickSize()*DEVIATION;         
         
         // Para pasar las cotizaciones en el número de puntos, a menudo tendremos que dividir el precio por el valor _Point.
         // Es mejor representar este valor como 1/Point, entonces podremos sustituir la división por la multiplicación. 
         // Aquí no se verifica el 0 de csmb.Point(), no puede ser igual a 0, pero si por alguna razón 
         // el parámetro no se obtiene, este triángulo será rechazado con la línea if (!csmb.Name(MxSmb[i].smb1.name)).            
         MxSmb[i].smb1.Rpoint=int(NormalizeDouble(1/csmb.Point(),0));
         
         // Hasta cuántos dígitos redondeamos el lote. 
         MxSmb[i].smb1.digits_lot=csup.NumberCount(csmb.LotsStep());
         
         // Limitaciones para los volúmenes, normalizados de antemano
         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); 
         
         //Tamaño del contrato 
         MxSmb[i].smb1.contract=csmb.ContractSize();
         
         // Lo mismo que arriba, pero para el 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();
         
         // Lo mismo que arriba, pero para el 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();   
         
         // Alineamos el volumen del trading. Aquí hay limitaciones para cada par de divisas, así como para el triángulo en sí. 
         // Las limitaciones para el par están escritas aquí: MxSmb[i].smbN.lotN
         // Las limitaciones para el triángulo están escritas aquí: MxSmb[i].lotN
         
         // Seleccionamos el valor máximo de los valores mínimos. Aquí mismo redondeamos según el valor más grande
         // Este bloque del código ha sido hecho sólo para el caso cuando surge aproximadamente la siguiente situación con los volúmenes: 0.01+0.01+0.1. 
         // En este caso, el volumen mínimo posible será establecido en 0,1 y redondeado a 1 dígito tras la coma.
         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)));
         
         // Cogemos el valor mínimo de los máximos y también redondeamos. 
         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)));
         
         // Si en los parámetros de entrada del volumen comercial figura 0, entonces usamos el volumen mínimo posible, pero cogemos no el mínimo para cada par,
         // sino el 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
         {
            // Si hay que alinear el volumen, es conocido para el par 1 y 2, y el volumen del par 3 va a calcularse directamente antes de la entrada. 
            MxSmb[i].smb1.lot=lot;  
            MxSmb[i].smb2.lot=lot;
            
            // Si el volumen de entrada no entra en las limitaciones actuales, no usamos el triángulo en el trabajo. 
            // Avisamos de ello
            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;
            }            
         }
      }
   }

La función se ubica en el archivo separado fnSmbLoad.mqh

Pues, aquí podemos considerar terminado el apartado sobre la composición de los triángulos. Sigamos.

Modos de trabajo del EA

Al iniciar el robot, podemos elegir uno de los modos de trabajo disponibles:
  1. Symbols from Market Watch.
  2. Symbols from file.
  3. Create file with symbols.

El modo "Symbols from Market Watch" supone que iniciamos el robot en el símbolo actual y componemos los triángulos desde la ventana Observación del mercado. Es el modo principal y no requiere el procesamiento adicional.

El modo "Symbols from file" se diferencia del primero sólo por la fuente de la obtención de los triángulos (desde el archivo preparado previamente).

El modo "Create file with symbols" precisamente crea el archivo con los triángulos que vamos a usar en el futuro en el segundo modo de trabajo o en el Probador de estrategias. Este modo supone sólo la construcción de los triángulos, después del cual el trabajo del EA se termina.

Describiremos esta lógica:

      if(inMode==CREATE_FILE)
      {
         // Eliminamos el archivo si existe.
         FileDelete(FILENAME);  
         int fh=FILEOPENWRITE(FILENAME);
         if (fh==INVALID_HANDLE) 
         {
            Alert("File with symbols not created");
            ExpertRemove();
         }
         // Escribimos los triángulos y alguna información adicional en el archivo
         fnCreateFileSymbols(MxThree,fh);
         Print("File with symbols created");
         
         // Cerramos el archivo y terminamos el trabajo del EA
         FileClose(fh);
         ExpertRemove();
      }

La función de la escritura de los datos en el archivo es simple y no requiere comentarios especiales:

void fnCreateFileSymbols(stThree &MxSmb[], int filehandle)
   {
      // Determinamos los encabezados en el archivo
      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");
      
      // Llenamos el archivo de acuerdo con los encabezados mencionados
      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,"");      
      // Dejamos una línea en blanco después de todos los símbolos
      
      // Después de terminar el trabajo, pasamos todos los datos en el disco por motivos de seguridad 
      FileFlush(filehandle);
   }

Aparte de los mismos triángulos, escribiremos la información adicional: volúmenes comerciales permitidos, tamaño del contrato, número de dígitos en las cotizaciones. Necesitamos estos datos sólo para el control visual de las propiedades de los símbolos.

Esta función se ubica en el archivo separado fnCreateFileSymbols.mqh


Reinicio del robot

Prácticamente, hemos terminado los ajustes iniciales del EA. Nos queda responder a una pregunta más, ¿cómo procesar la recuperación tras un fallo? Si se ha cortado la conexión con Internet por un rato, no pasa nada. El robot seguirá trabajando sin problema después de que la conexión se recupere. Pero si necesitamos reiniciar el robot, tendremos que encontrar nuestras posiciones y seguir trabajando con ellas.

Es la función que soluciona los problemas con el reinicio del robot:

void fnRestart(stThree &MxSmb[],ulong magic,int accounttype)
   {
      string   smb1,smb2,smb3;
      long     tkt1,tkt2,tkt3;
      ulong    mg;
      uchar    count=0;    //Contador de los triángulos recuperados
      
      switch(accounttype)
      {
         // No es tan difícil recuperar las posiciones en caso de la cuenta de cobertura: hay que recorrer todas las posiciones abiertas, encontrar las suyas usando el magic y 
         // formarlas en los triángulos.
         // Con la compensación la cosa es más complicada, hay que dirigirse a su propia base donde se almacenan las posiciones abiertas por el robot. 
         
         // El algoritmo de la búsqueda de sus posiciones y la recuperación de los triángulos está realizado «a quema ropa», sin floreo 
         // ni optimización. Pero dado que esta fase no es necesaria con frecuencia, menospreciar el rendimiento
         // a favor de la reducción del código. 
         
         case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
            // Recorremos todas las posiciones abiertas y buscamos las coincidencias por el magic. 
            // Recordamos el magic de la primera posición encontrada: los usaremos para buscar otras dos. 

            
            for(int i=PositionsTotal()-1;i>=2;i--)
            {//for i
               smb1=PositionGetSymbol(i);
               mg=PositionGetInteger(POSITION_MAGIC);
               if (mg<magic || mg>(magic+MAGIC)) continue;
               
               // Recordamos el ticket para que luego sea más fácil acceder a esta posición. 
               tkt1=PositionGetInteger(POSITION_TICKET);
               
               // Buscamos la segunda posición con el mismo 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 la última posición.
                  for(int k=j-1;k>=0;k--)
                  {//for k
                     smb3=PositionGetSymbol(k);
                     if (mg!=PositionGetInteger(POSITION_MAGIC)) continue;
                     tkt3=PositionGetInteger(POSITION_TICKET);
                     
                     // Si hemos llegado a este punto, entonces hemos encontrado el triángulo abierto. Los datos sobre él ya están cargados. El robot calculará lo demás en el siguiente tick.
                     
                     for(int m=ArraySize(MxSmb)-1;m>=0;m--)
                     {//for m
                        // Recorremos el array de los triángulos, ignorando los que ya están abiertos.
                        if (MxSmb[m].status!=0) continue; 
                        
                        // El recorrido se hace «a quema ropa». A primera vista, puede parecer que podemos acceder varias veces 
                        // al mismo par de divisas. Pero no es así, porque en los ciclos del recorrido 
                        // después de encontrar un par, continuamos la búsqueda no desde el principio, sino desde el siguiente par

                        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;                                                  // Hemos encontrado este triángulo y le asignamos el estatus correspondiente                         MxSmb[m].status=2;                         MxSmb[m].magic=magic;                         MxSmb[m].pl=0;                                                  // Colocamos los tickets en secuencia necesaria. Tenemos nuestro triángulo funcionando de nuevo.                         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, esta función se encuentra en el archivo separado: fnRestart.mqh

Últimos pasos:

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

Fíjese en el modo asincrónico del envío de las órdenes. La estrategia supone las acciones operativas al máximo, por eso usa este modo de colocación. También habrá problemas: necesitaremos el código adicional para monitorear si la posición ha sido abierta con éxito. Vamos a analizar eso más abajo.

Pues, el bloque OnInit() está terminado, podemos pasar al cuerpo del robot.

OnTick

Primero, veremos si tenemos en nuestras ajustes alguna limitación para el número máximo posible de los triángulos abiertos. Si es así y hemos llegado al límite establecido, podemos saltar la parte considerable del código en este tick:

      ushort OpenThree=0;                          // Número de triángulos abiertos
      for(int j=ArraySize(MxThree)-1;j>=0;j--)
      if (MxThree[j].status!=0) OpenThree++;       // Считаем незакрытые тоже         

La verificación es simple. Hemos declarado una variable local para calcular los triángulos abiertos y hemos recorrido cíclicamente nuestro array principal. Si el estatus del triángulo no es 0, entonces está en el trabajo. 

Después de calcular los triángulos abiertos, si las limitaciones lo permiten, ponemos a ver los demás triángulos y monitorear su estado. De eso se encarga la función fnCalcDelta().

      if (inMaxThree==0 || (inMaxThree>0 && inMaxThree>OpenThree))
         fnCalcDelta(MxThree,inProfit,inCmnt,inMagic,inLot,inMaxThree,OpenThree);   // Calculamos la divergencia y en seguida abrimos

Vemos su código en detalle:

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
         // Si el triángulo está en el trabajo, lo omitimos
         if(MxSmb[i].status!=0) continue; 
         
         // Volvemos a comprobar la disponibilidad de los tres pares: si uno de ellos no está disponible,
         // no tiene sentido calcular el triángulo entero
         if (!fnSmbCheck(MxSmb[i].smb1.name)) continue;  
         if (!fnSmbCheck(MxSmb[i].smb2.name)) continue;  //si por algún par han cerrado la negociación
         if (!fnSmbCheck(MxSmb[i].smb3.name)) continue;     
         
         // El número de los triángulos abiertos se calcula al principio de cada tick,
         // pero se puede abrirlos dentro del tick también. Por eso monitoreamos constantemente su cantidad
         if (lcMaxThree>0) {if (lcMaxThree>lcOpenThree); else continue;}     

         
         // Luego obtenemos todos los datos para el cálculo. 
         
         // Hemos obtenido el coste del 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;
         
        //--- Obtenemos los precios actuales
         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;
         
         // Comprobamos si el ask o bid es igual 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 el volumen para el tercer par. Sabemos el volumen para dos primeros pares, es igual y fijo.
         // El volumen del tercer par se cambia constantemente. Pero él se calcula sólo si el valor del lote no es 0 en las variables iniciales.
         // Si el lote es 0, va a  usarse el volumen mínimo igual.
         // La lógica del cálculo es simple. Recordamos nuestra versión del triángulo: EURUSD=EURGBP*GBPUSD. El número de libras compradas o vendidas
         // depende directamente de la cotización EURGBP, mientras que el tercer par esta divisa se encuentra en el primer lugar. Nos libramos de una parte de los cálculos
         // tomando como volumen el precio del segundo par. He cogido la media entre ask y bid
         // No olvidamos de la corrección respecto al volumen comercial 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);
         
         // Si el volumen calculado excede los límites permitidos, avisamos al usuario.
         // Marcamos este triángulo como no válido
         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 nuestros gastos, es decir, spread+comisión. pr = spread en puntos enteros.
         // Precisamente el spread nos impide ganar con esta estrategia, por eso hay que tomar lo cuenta obligatoriamente. 
         // En vez de usar la diferencia de los precios multiplicada por el punto opuesto, se puede coger el spread en puntos.

         
         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;
         
         // Ahora calculamos el spread en la moneda del depósito. 
         // En la divisa, el coste de 1 tick es siempre igual al parámetro SYMBOL_TRADE_TICK_VALUE.
         // No olvidamos de los volúmenes comerciales
         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;
         
         // Son nuestros gastos respecto al volumen indicado con la comisión añadida que indica el usuario
         MxSmb[i].spread=MxSmb[i].smb1.spcost+MxSmb[i].smb2.spcost+MxSmb[i].smb3.spcost+prft;
         
         // Podemos monitorear la situación cuando el ask de la cartera < del bid, pero estas situaciones son muy raras, 
         // y se puede no considerarlas separadamente. Además, el arbitraje distribuido por el tiempo, también procesará esta situación.
         // Pues bien, la ubicación dentro de la posición está libre de riesgos, y por eso, por ejemplo hemos comprado eurusd,
         // y en seguida lo hemos vendido a través de eurgbp y gbpusd. 
         // Es decir, hemos visto que ask eurusd< bid eurgbp * bid gbpusd. Estas situaciones son frecuentes, pero para una entrada exitosa, eso no es suficiente.
         // Calcularemos además los gastos para el spread. Hay que entrar no sólo cuando ask < bid, sino cuando la diferencia entre
         // ellos supera los gastos para el spread.          
         
         // Vamos a acordar que la compra significa que hemos comprado el primer símbolo y hemos vendido otros dos,
         // y la venta es cuando hemos vendido el primer par y hemos comprado otros dos.
         
         temp=MxSmb[i].smb1.tv*MxSmb[i].smb1.Rpoint*MxSmb[i].smb1.lot;
         
         // Vamos a considerar en detalle la fórmula del cálculo. 
         // 1. Entre paréntesis, cada precio se corrige por el deslizamiento en el lado peor: MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev
         // 2. Como se muestra en la fórmula de arriba, bid eurgbp * bid gbpusd - multiplicamos los precios del segundo y tercer símbolo:
         //    (MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.bid-MxSmb[i].smb3.dev)
         // 3. Luego, calculamos la diferencia entre ask y bid
         // 4. Hemos obtenido la diferencia en puntos la que ahora hay que pasar en dinero: multiplicar 
         // el coste del punto y volumen comercial. Para este propósito, cogemos los valores del primer par.
         // Si estuviéramos construyendo el triángulo, moviendo todos los pares al mismo lado y realizando la comparación con 1, habría más cálculos. 

         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;
         
         // Hemos obtenido el cálculo del importe que podemos ganar o perder si compramos o vendemos el triángulo. 
         // Nos queda compararlo con los gastos para decidir si entrar en la transacción. Vamos a normalizar todo hasta el 2 dígito. 
         MxSmb[i].PLBuy=   NormalizeDouble(MxSmb[i].PLBuy,2);
         MxSmb[i].PLSell=  NormalizeDouble(MxSmb[i].PLSell,2);
         MxSmb[i].spread=  NormalizeDouble(MxSmb[i].spread,2);                  
         
         // Si hay beneficio potencial, comprobamos la disponibilidad de fondos para la apertura.         
         if (MxSmb[i].PLBuy>MxSmb[i].spread || MxSmb[i].PLSell>MxSmb[i].spread)
         {
            // Simplemente he contado el margen para la compra. Puesto que es superior que para la venta, se puede no tomar en cuenta la dirección de la transacción
            // Fíjese en el coeficiente de aumento. No se puede abrir el triángulo cuando el margen apenas es suficiente. El coeficiente de aumento por defecto es de un 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))  //hemos comprobado el margen libre
            {
               //Estamos casi a punto para la apertura, queda sólo encontrar un magic libre de nuestro diapasón. 
               // El magic inicial se indica en los parámetros de entrad, en la variable inMagic y por defecto es igual a 300. 
               // El diapasón de los magic se indica en la directiva define MAGIC, por defecto es 200.
               MxSmb[i].magic=fnMagicGet(MxSmb,magic);   
               if (MxSmb[i].magic<=0)
               { // Si obtenemos 0, todos los magic están ocupados. Enviamos el mensaje de ello y salimos.
                  Print("Free magic ended\nNew triangles will not open");
                  break;
               }  
               
               // Colocamos el magic encontrado para el trabajo
               ctrade.SetExpertMagicNumber(MxSmb[i].magic); 
               
               // Creamos el comentario para el triángulo
               cmnt_pos=cmnt+(string)MxSmb[i].magic+" Open";               
               
               // Nos abrimos, recordando de paso la hora del envío del triángulo para la apertura. 
               // Eso es necesario para no estar a la espera. 
               // Por defecto en la define MAXTIMEWAIT se pone el tiempo de espera hasta la apertura total en 3 segundos.
               // Si no nos hemos abierto durante este tiempo, enviamos lo que ha logrado abrirse para el cierre.
               
               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 el mensaje sobre la apertura del 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
   }

He puesto para esta función unos comentarios muy detallados. Espero que el lector no tenga preguntas. Queda revelar otras dos cosas: el mecanismo de selección del magic disponible y la misma apertura del triángulo.

Así seleccionamos el magic disponible:

ulong fnMagicGet(stThree &MxSmb[],ulong magic)
   {
      int mxsize=ArraySize(MxSmb);
      bool find;
      
      // Se puede repasar todos los triángulos abiertos en nuestro array. 
      // Pero he elegido otra opción, recorrer el diapasón de los magic,
      // y repasar el magic seleccionado por el array. 
      for(ulong i=magic;i<magic+MAGIC;i++)
      {
         find=false;
         
         // Magic en i. Comprobamos si está asignado a algún triángulo de los abiertos.
         for(int j=0;j<mxsize;j++)
         if (MxSmb[j].status>0 && MxSmb[j].magic==i)
         {
            find=true;
            break;   
         }   
         
         // Si el magic no se usa, salimos del ciclo sin esperar la terminación.    
         if (!find) return(i);            
      }  
      return(0);  
   }

Así abrimos el triángulo:

bool fnOpen(stThree &MxSmb[],int i,string cmnt,bool side, ushort &opt)
   {
      // Bandera de apertura de la primera orden. 
      bool openflag=false;
      
      // Si no hay permiso para tradear, no tradeamos. 
      if (!cterm.IsTradeAllowed())  return(false);
      if (!cterm.IsConnected())     return(false);
      
      switch(side)
      {
         case  true:
         
         // Si después del envío de la orden de apertura, es devuelto false, está claro que no hay sentido enviar otros dos pares para la apertura. 
         // Mejor lo intentamos en el siguiente tick. Tampoco el robot se encarga de completar la apertura del triángulo. 
         // Si después del envío de las órdenes, algo ha quedado sin abrirse, después de la espera
         // indicada en MAXTIMEWAIT, cerramos el triángulo abierto incompletamente. 
         if(ctrade.Buy(MxSmb[i].smb1.lot,MxSmb[i].smb1.name,0,0,0,cmnt))
         {
            openflag=true;
            MxSmb[i].status=1;
            opt++;
            // Luego, la lógica es la misma, si no hemos podido abrir, el triángulo irá en los cerrados. 
            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);
   }

Como antes, las funciones de arriba se encuentran en los archivos separados fnCalcDelta.mqh, fnMagicGet.mqh y fnOpen.mqh.

Pues bien, hemos encontrado el triángulo necesario y lo hemos enviado para la apertura. En MetaTrader 4 o en las cuentas de cobertura en MetaTrader 5, eso prácticamente significa el fin del trabajo del EA. Pero todavía necesitamos monitorear el éxito de la apertura del triángulo. Para eso no utilizo los eventos OnTrade y OnTradeTransaction, porque en su caso no hay garantías para obtener un resultado exitoso. En vez de ellos, compruebo el número de las posiciones actuales, es un indicador eficaz a 100%.

Vamos a ver las funciones del control de las posiciones abiertas:

void fnOpenCheck(stThree &MxSmb[], int accounttype, int fh)
   {
      uchar cnt=0;       // Contador de las posiciones abiertas en el triángulo
      ulong   tkt=0;     // Ticket actual
      string smb="";     // Símbolo actual
      
      // Comprobamos nuestro array de triangulos
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // Consideramos sólo los triángulos con el estatus 1, es decir enviados para la apertura
         if(MxSmb[i].status!=1) continue;
                          
         if ((TimeCurrent()-MxSmb[i].timeopen)>MAXTIMEWAIT)
         {     
            // Si el tiempo dedicado para la apertura ha sido superado, marcamos el triángulo como listo para el cierre         
            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:
            
            // Comprobamos todas las posiciones abiertas. Lo hacemos para cada triángulo. 

            for(int j=PositionsTotal()-1;j>=0;j--)
            if (PositionSelectByTicket(PositionGetTicket(j)))
            if (PositionGetInteger(POSITION_MAGIC)==MxSmb[i].magic)
            {
               // Obtenemos el símbolo y el ticket de la posición en cuestión. 

               tkt=PositionGetInteger(POSITION_TICKET);                smb=PositionGetString(POSITION_SYMBOL);                               // Comprobamos si hay posición actual entre las que necesitamos en el triángulo considerado.                // Si existe, aumentamos el contador y recordamos el ticket y el precio de la apertura.                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);}                               // Si hemos encontrado tres posiciones necesarias, entonces nuestro triángulo ha sido abierto con éxito. Cambiamos su estatus por 2 (abierto).                // Escribimos los datos sobre la apertura en el archivo log                if (cnt==3)                {                   MxSmb[i].status=2;                   fnControlFile(MxSmb,i,fh);                   break;                  }             }             break;             default:             break;          }       }    }

La función para escribir en el archivo log es simple:

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);       
   }

Pues, hemos encontrado el triángulo para la entrada, y hemos abierto las posiciones con éxito. Ahora, hay que calcular cuánto hemos ganado.

void fnCalcPL(stThree &MxSmb[], int accounttype, double prft)
   {
      // De nuevo recorremos nuestro array de triángulos
      // La velocidad de la apertura y del cierre es muy importante para esta estrategia. 
      // Por eso, en cuanto encontramos un triángulo para el cierre, lo cerramos inmediatamente.
      
      bool flag=cterm.IsTradeAllowed() & cterm.IsConnected();      
      
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {//for
         // Nos interesan solamente los triángulos con el estatus 2 o 3.
         // El estatus 3 (cerrar el triángulo) hemos podido obtener si el triángulo no se abierto completamente
         if(MxSmb[i].status==2 || MxSmb[i].status==3); else continue;                             
         
         // Calculamos cuánto ha ganado el triángulo 
         if (MxSmb[i].status==2)
         {
            MxSmb[i].pl=0;         // Reseteamos el beneficio
            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 hasta el dígito 2. 
            MxSmb[i].pl=NormalizeDouble(MxSmb[i].pl,2);
            
            // El cierre lo vamos a analizar más detalladamente. Yo uso la siguiente lógica:
            // la situación con el arbitraje no es normal y no debe surgir, es decir, cuando aparece podemos aspirar a la vuelta 
            // en el estado cuando no hay arbitraje. ¿Podremos ganar? En otras palabras, no podemos decir 
            // si la obtención del beneficio continua. Por eso yo prefiero cerrar la posición inmediatamente después de que el spred y la comisión queden cubiertos. 
            // La cuenta en el arbitraje triangular va en puntos, aquí no hay que esperar grandes movimientos. 
            // No obstante, puede poner el beneficio deseado en la variable «Comisión» en los parámetros de entrada, y esperar a que llegue. 
            // Concluyendo, si hemos ganado más de que hemos gastado, asignamos a la posición el estatus «enviar para el cierre».

            if (flag && MxSmb[i].pl>prft) MxSmb[i].status=3;                    
         }
         
         // Cerrar el triángulo sólo si el trading está permitido.
         if (flag && MxSmb[i].status==3) fnCloseThree(MxSmb,accounttype,i); 
      }//for         
   }

Del cierre del triángulo responde la función:

void fnCloseThree(stThree &MxSmb[], int accounttype, int i)
   {
      // Antes del cierre, comprobamos obligatoriamente la disponibilidad de todos los pares en el triángulo. 
      // Es sumamente incorrecto y peligroso romper el triángulo, y en caso de trabajar con una cuenta de compensación,
      // luego, habrá un lío en las posiciones. 
      
      if(fnSmbCheck(MxSmb[i].smb1.name))
      if(fnSmbCheck(MxSmb[i].smb2.name))
      if(fnSmbCheck(MxSmb[i].smb3.name))          
      
      // Si todo está disponible, usamos la librería estándar para cerrar todos tres posiciones. 
      // Después del cierre, es necesario comprobar el éxito de estas acciones. 
      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;
      }       
   }  

Prácticamente, hemos llegado al final, queda sólo comprobar el éxito del cierre y visualizar algo en la pantalla. Si el robot no escribe nada, parece que no trabaja.

Aquí tenemos la verificación del éxito del cierre. Por cierto, para la apertura y el cierre podíamos usar una función, simplemente cambiando la dirección de la transacción. Pero eso a mí no me gusta mucho porque entre estas dos acciones hay ciertas diferencias procesales. 

Comprobamos el éxito del cierre: 

void fnCloseCheck(stThree &MxSmb[], int accounttype,int fh)
   {
      // Recorremos el array de los triángulos.
      for(int i=ArraySize(MxSmb)-1;i>=0;i--)
      {
         // Nos interesan sólo aquéllos que tienen el estatus 3, es decir ya cerrados o enviados para el cierre. 
         if(MxSmb[i].status!=3) continue;
         
         switch(accounttype)
         {
            case  ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: 
            
            // Si no hemos podido destacar ningún par del triángulo, entonces hemos cerrado con éxito. Devolvemos el estatus 0
            if (!PositionSelectByTicket(MxSmb[i].smb1.tkt))
            if (!PositionSelectByTicket(MxSmb[i].smb2.tkt))
            if (!PositionSelectByTicket(MxSmb[i].smb3.tkt))
            {  // Entonces, hemos cerrado con éxito
               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));
               
               // Escribimos en el archivo log la información sobre el cierre. 
               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;
         }            
      }      
   }

Para terminar, visualizaremos algo en la pantalla en forma de coemntarios. Es una especie del «maquillaje» para el seguimiento visual. Mostramos lo siguiente:

  1. Se sigue el total de triángulos
  2. Triángulos abiertos
  3. 5 triángulos más cercanos para la apertura
  4. Triángulos abiertos, si hay

El código de esta función:

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 a mostrar en la pantalla
      short max=5;
      max=(short)MathMin(total,max);
      
      // Mostrar 5 más cercanos para la apertuar 
      short index[];                    // Array de índices
      ArrayResize(index,max);
      ArrayInitialize(index,-1);        // No se usan
      short cnt=0,num=0;
      while(cnt<max && num<total)       // Cogidos para el inicio los primeros max de los índices de los triángulos no abiertos
      {
         if(MxSmb[num].status!=0)  {num++;continue;}
         index[cnt]=num;
         num++;cnt++;         
      }
      
      // Ordenar y buscar sólo tiene sentido si los elementos son más que pueden mostrarse en la pantalla. 
      if (total>max) 
      for(short i=max;i<total;i++)
      {
         // Los triángulos abiertos se muestran más abajo.
         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;}
         }   
      }
      
      // MOstramos los triángulos más cercanos para la apertura.
      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";      
      }            
      
      // Mostramos los triángulos abiertos. 
      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);
   }

Simulación


Si le interesa, puede hacer la prueba en el modo de modelado de ticks, comparando con la simulación en ticks reales. Se puede hacer más: comparar lo resultados de las pruebas en los ticks reales con el trabajo real y hacer conclusión que el probador multidivisas está muy lejos de la realidad. 

Los resultados del trabajo real demuestran que como media se puede contar con 3-4 transacciones a la semana. Los más común es que la posición se abre por la noche, y habitualmente, en el triángulo aparece la divisa de baja liquidez tipo TRY, NOK, SEK y parecidas. El beneficio del robot depende del volumen manejado, y puesto que las transacciones surgen con poca frecuencia, este EA puede manejar volúmenes grandes, trabajando al mismo tiempo con otros robots.

El riesgo del robot se calcula fácilmente: 3 spreads * el número de triángulos abiertos.

Para preparar los pares de divisas con los que se puede trabajar, recomiendo abrir primero todos los símbolos, luego cerrar los que tienen el trading prohibido y los que no son pares de divisas. Puede realizar esta operación con más rapidez usando el script imprescindible para los aficionados a las estrategias multidivisas: https://www.mql5.com/en/market/product/25256

También recordaré que el historial en el Probador no se carga del servidor del broker, lo tiene que tener descargado previamente en el terminal de cliente. Por eso, tiene que hacerlo personalmente antes de iniciar la prueba, o bien usar el script mencionado arriba.

Perspectivas del desarrollo

¿Se puede mejorar los resultados del trabajo?  Claro que es posible. Para eso, hay que hacer su propio agregador de la liquidez, pero la desventaja de este enfoque consiste en la necesidad de abrir las cuentes con muchos brokers.

Además, se puede acelerar los esultados de la prueba. Para eso hay dos maneras que se puede combinar. El primer paso es introducir la discreción del cálculo, monitoreando constantemente sólo los triángulos donde la probabilidad de la entrada es muy elevada. La segunda opción es usar OpenCL, lo que es bastante lógico para este robot.

Archivos usados en el artículo

Nombre del archivo Descripción
1 var.mqh Contiene la descripción de todas las variables, directivas y los inputs.
2 fnWarning.mqh Comprobaciones de las condiciones iniciales para el trabajo correcto del EA: variables de entrada, entorno, ajustes.
3 fnSetThree.mqh Componemos los triángulos de los pares de divisas. Aquí mismo, se realiza la selección de la fuente de los pares: Observación del mercado o archivo preparado previamente.
4 fnSmbCheck.mqh La función para comprobar la disponibilidad del símbolo, y otras limitaciones. NB: La sesión de trading y de cotización o se comprueban en el robot
5 fnChangeThree.mqh Cambiamos la posición de los pares en el triángulo para que estén construidos según el mismo principio.
6 fnSmbLoad.mqh Cargamos diferentes datos para los símbolos: precios, puntos, limitaciones de volúmenes, etc.
7 fnCalcDelta.mqh Calculamos las separaciones en el triángulo, y tomamos en cuenta todos los gastos adicionales: spread, comisión, deslizamiento.
8 fnMagicGet.mqh Buscamos el magic que se puede usar para el triángulo actual
 9 fnOpenCheck.mqh Comprobamos el éxito de la apertura del triángulo
10 fnCalcPL.mqh  Calculamos el beneficio/pérdida del triángulo
11  fnCreateFileSymbols.mqh La función que crea el archivo con los triángulos para el trading. El archivo también contiene los datos adicionales (para propósitos informativos).
12  fnControlFile.mqh La función se encarga del archivo log. Aquí se describen todas las aperturas y cierres con datos necesarios. 
13  fnCloseThree.mqh Cierre del triángulo 
14  fnCloseCheck.mqh Comprueba si el triángulo ha cerrado completamente
15  fnCmnt.mqh Mostrar comentarios en la pantalla 
16  fnRestart.mqh Comprueba en el inicio del robot si hay triángulos abiertos previamente, y si es así, los sigue vigilando. 
17  fnOpen.mqh Abrimos el triángulo
18 Support.mqh Clase adicional del soporte. Contiene la única función: cálculo del número de dígitos tras la coma para un número quebrado.
19 head.mqh En este archivo se describen los encabezados de todos los archivos mencionados arriba.

Traducción del ruso hecha por MetaQuotes Software Corp.
Artículo original: https://www.mql5.com/ru/articles/3150

Archivos adjuntos |
MQL5.ZIP (235.99 KB)
Asesor Experto Multiplataforma: Stops Personalizados, Ausencia de Pérdidas y Trailing Asesor Experto Multiplataforma: Stops Personalizados, Ausencia de Pérdidas y Trailing

En el artículo se discute la colocación de niveles stop personalizados en el asesor multiplataforma. Asimiso, se describe un método estrechamente relacionado con ellos, que ayuda a definir los cambios de los niveles stop a lo largo del tiempo.

Lógica difusa en las estrategias comerciales Lógica difusa en las estrategias comerciales

En este artículo, se analiza el ejemplo del uso de la lógica difusa (fuzzy logic) para la construcción de un sistema comercial simple con la aplicación de la librería Fuzzy. Han sido propuestas las opciones de la mejora del sistema mediante la combinación de la lógica difusa, algoritmos genéticos y redes neuronales.

Nuevo enfoque a la interpretación de la divergencia clásica e inversa Nuevo enfoque a la interpretación de la divergencia clásica e inversa

En este artículo se considera el método clásico de la construcción de la divergencia y el modo de la interpretación distinto de él. Este nuevo método de la interpretación ha sido puesto como base de la estrategia comercial descrita en el presente artículo.

Usando el filtro de Kalman en la predicción del precio Usando el filtro de Kalman en la predicción del precio

Para un trading de éxito, casi siempre son necesarios los indicadores destinados a separar el movimiento principal de precios de las fluctuaciones ruidosas. En este artículo se considera uno de los filtros digitales más avanzados, el filtro de Kalman. Se describe su construcción y el uso en la práctica.