Análisis de múltiples símbolos con Python y MQL5 (Parte 3): Tipos de cambio triangulares
Los mercados financieros son intrínsecamente ruidosos. Es habitual que los operadores sufran pérdidas innecesarias causadas por señales de trading falsas que les animan a abrir sus posiciones prematuramente. Se han desarrollado muchas estrategias y principios comerciales diferentes a la luz de esta cuestión. La mayoría de estos axiomas enseñan esencialmente al operador a esperar antes de actuar y, en su lugar, buscar fuentes alternativas de confirmación o señales adicionales de fortaleza.
Por lo general, estas reglas no especifican un horizonte temporal en el que deben encontrarse las señales de confirmación, lo que en ocasiones puede suponer un motivo de arrepentimiento para el operador, ya que quienes siguen estas reglas empíricas tienden a perder buenos niveles de entrada en la mayoría de las operaciones en las que finalmente deciden participar.
Es evidente para el lector que existe una clara necesidad de que intentemos formular estrategias comerciales capaces de medir la fortaleza de un mercado lo más cerca posible del tiempo real. Es posible que lo logremos si intentamos aprovechar los mercados conectados y buscamos patrones predecibles que tengan sentido para nosotros. Podemos seguir buscando patrones transversales en los mercados que consideremos predecibles, en lugar de perder potencialmente pips en cada operación que nos interese, lo que agravaría nuestras pérdidas con el tiempo.
Para los lectores que quizá no estén familiarizados con la escuela de pensamiento sobre los patrones entre mercados, ofreceremos una breve introducción al tema, para que puedan comprender por qué algunos operadores afirman que estos patrones entre mercados suelen ser predecibles y que, si se interpretan correctamente, pueden resultar muy sólidos.
Alrededor del 90 % de las materias primas del mundo se cotizan en dólares estadounidenses. Sin embargo, algunos productos básicos se negocian con tanta frecuencia que a menudo se cotizan en varias divisas a la vez.
Los metales preciosos, como la plata, suelen cotizarse simultáneamente en dólares estadounidenses y en euros. Hoy nos centraremos en la plata como ejemplo. Es posible que su corredor ofrezca cotizaciones del precio de la plata en dólares (XAGUSD) y en euros (XAGEUR) simultáneamente.
Si queremos operar con XAGUSD y sabemos que está fundamentalmente relacionado con XAGEUR, ¿cómo podemos aprovechar este conocimiento en nuestro beneficio para aceptar o rechazar las oportunidades de negociación que se nos ofrecen, sin tener que esperar indefinidamente a recibir una confirmación?
Teniendo en cuenta el tipo de cambio vigente entre el euro y el dólar (EURUSD), se puede elaborar una estrategia comercial triangular que permita detectar patrones predecibles entre el precio de la plata en dólares, el euro y el tipo de cambio justo entre el euro y el dólar (EURUSD). Nuestro objetivo es proporcionar al lector una estrategia de trading resistente al ruido y que le ayude a descubrir el sentimiento «oculto» del mercado a partir de las cotizaciones habituales que recibe en su terminal MetaTrader 5.
Resumen de nuestra estrategia comercial
Antes de entrar en detalles sobre nuestra estrategia de trading, asegurémonos primero de que todos los lectores tengan una comprensión básica de los conceptos de base y cotización en un par negociado. Si tomamos como ejemplo el EURUSD, el EUR (euro) es nuestra base. La primera abreviatura a la izquierda del par es la base. A medida que el tipo de cambio que se muestra en un gráfico se aleja de 0, el valor de la moneda base aumenta. Por lo tanto, si observamos un gráfico del tipo de cambio EURUSD y el gráfico sube, esto indica que debemos vender más dólares estadounidenses para ganar 1 euro en el mercado de divisas al contado.

Figura 1: Comprender la diferencia entre la base y la cotización en un par.
Por otro lado, el segundo símbolo que aparece en la lista es la comilla. El valor de la cotización aumenta a medida que el tipo de cambio mostrado en el gráfico se acerca a 0. Lo que significa que tendremos que vender cada vez menos dólares estadounidenses para obtener 1 euro.

Figura 2: Centrándonos en la cotización.
Ahora podemos considerar nuestra configuración comercial particular sabiendo que ningún lector se quedará atrás. Estamos considerando tres mercados diferentes, con el objetivo de operar solo en uno. Nuestro objetivo es el XAGUSD. Si queremos comprender en qué dirección podría moverse el XAGUSD, nuestra estrategia comenzará por comprobar primero el tipo de cambio del EURUSD.
Si el tipo de cambio EURUSD está subiendo, entonces el euro (cotización) se está apreciando y el dólar (base) se está depreciando. Cabría esperar que un producto básico cuyo precio se fija tanto en euros como en dólares se abarate en euros, dado que el euro ha ganado poder adquisitivo, y se encarezca en dólares, dado que el dólar ha perdido valor en el mercado de divisas.
Esperamos que este experimento mental haya dado a los lectores que quizá no estén familiarizados con este tipo de estrategias bursátiles una idea de cómo se podría esperar que tres mercados se «reflejaran» entre sí bajo ciertas hipótesis.
Al analizar el rendimiento de estos tres mercados al mismo tiempo, podemos crear una estrategia que, en esencia, no siempre requiera esperar una confirmación. Si nuestras hipótesis sobre la naturaleza interconectada de estos mercados se mantienen, entonces deberíamos tener un marco viable que pueda adoptarse a otros mercados en los que el lector esté interesado en operar.
En la figura 4 se ilustra un mapa mental visual de la estrategia comercial. En términos generales, si el tipo de cambio del EURUSD está bajando, cabría esperar que los productos cuyo precio se fija en euros se encarecieran y que los productos cuyo precio se fija en dólares se abarataran. Nuestra estrategia analizará el tipo de cambio EURUSD, el precio de la plata en euros y el precio de la plata en dólares, con el fin de encontrar este patrón concreto. Si se detecta este patrón cruzado en el mercado, venderemos en corto el precio de la plata en dólares.

Figura 3: Visualización de nuestras reglas para operar en corto con el XAUUSD.
Aplicamos el mismo conjunto de reglas en sentido contrario cuando queremos apostar por una subida del precio de la plata en dólares. Deseamos observar fortaleza en el mercado de divisas, respaldando la evolución observada en los precios tanto europeos como estadounidenses de esta materia prima. De lo contrario, los movimientos que no están respaldados por patrones cruzados del mercado pueden ser frágiles y revertirse fácilmente. Si nuestras suposiciones sobre el comportamiento del mercado son ciertas, entonces nuestra estrategia debería ser sólida. Y puede sustituir la necesidad de «esperar la confirmación», si el análisis cruzado de mercados tiene sentido para su cartera individual.

Figura 4: Visualización de nuestras reglas de negociación para ir en largo en XAGUSD.
El lector debe ser consciente de que, en realidad, es muy probable que existan numerosos factores complejos que influyen en la subida o bajada del precio de la plata. Solo estamos tratando de resumir estas relaciones complicadas utilizando relaciones más simples.
Resumen del periodo de prueba retrospectiva
Probaremos nuestra estrategia desde el 1 de noviembre de 2023 hasta el 1 de enero de 2025. Los datos restantes que tenemos, desde el 1 de noviembre de 2022 hasta finales de octubre de 2023, se utilizarán para entrenar nuestra aplicación. En futuras versiones del artículo, sustituiremos nuestro modelo simple de la naturaleza interconectada de estos mercados por modelos que nuestra computadora pueda aprender de forma independiente. Por lo tanto, aunque no utilizaremos la partición de entrenamiento en este debate, la utilizaremos en debates futuros.

Figura 5: Comprensión del periodo de prueba retrospectiva para nuestro análisis.
Introducción a MQL5
Nuestra estrategia requiere que sigamos tres mercados a la vez. Por lo tanto, comencemos por crear constantes del sistema para realizar un seguimiento de cada mercado en el que tenemos interés abierto. Esto nos permite cambiar fácilmente entre estos mercados y comparar su rendimiento.
//+------------------------------------------------------------------+ //| Baseline Model.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System Constants | //+------------------------------------------------------------------+ #define SYMBOL_ONE "XAGUSD" //--- Our primary symbol, the price of Silver in USD #define SYMBOL_TWO "XAGEUR" //--- Our secondary symbol, the price of Silver in EUR #define SYMBOL_THREE "EURUSD" //--- Our EURUSD exchange rate. #define FETCH 24 //--- How many bars of data should we fetch? #define TF_1 PERIOD_H1 //--- Our intended time frame #define VOLUME SymbolInfoDouble(SYMBOL_ONE,SYMBOL_VOLUME_MIN) * 10 //--- Our trading volume
Utilizaremos el tipo vectorial MQL5 para obtener datos del mercado y transformarlos rápidamente.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ vector eurusd,xagusd,xageur; double eurusd_growth,xagusd_growth,xageur_growth,bid,ask; double sl_width = 3e2 * _Point;
La biblioteca de operaciones nos permite abrir y gestionar nuestras operaciones, la necesitamos para nuestro ejercicio de hoy.
//+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
El lenguaje MQL5 está diseñado para ayudarnos a operar de manera eficaz. Cada evento que se pueda imaginar que ocurra en el mercado se asigna a eventos específicos. Recibir nuevos precios es todo un acontecimiento. Cuando se registra el evento, se llama al controlador de eventos OnTick(). Se ejecutarán las funciones dentro de este controlador de eventos.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Setup our technical indicators setup(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- New prices have been quoted new_quotes_received(); } //+------------------------------------------------------------------+
Cada vez que recibimos nuevas cotizaciones, primero queremos verificar si se ha formado una vela completa. En los casos en que se haya formado una nueva vela, actualizaremos nuestras variables del sistema e intentaremos encontrar una oportunidad comercial.
//+------------------------------------------------------------------+ //| Custom functions | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Updates system variables accordingly | //+------------------------------------------------------------------+ void new_quotes_received(void) { static datetime time_stamp; datetime time = iTime(SYMBOL_ONE,TF_1,0); if(time_stamp != time) { time_stamp = time; update(); } }
Cuando nuestro sistema se carga por primera vez, queremos asegurarnos de que todos los mercados que necesitamos estén activados y disponibles.
//+------------------------------------------------------------------+ //| Setup our technical indicators and select the symbols we need | //+------------------------------------------------------------------+ void setup(void) { //--- Select the symbols we need SymbolSelect(SYMBOL_ONE,true); SymbolSelect(SYMBOL_TWO,true); SymbolSelect(SYMBOL_THREE,true); }
Una vez que se forma una nueva vela, leeremos los precios actualizados de cada uno de los mercados que estamos siguiendo. Podemos medir el crecimiento del mercado dividiendo su valor actual por su valor pasado. Si la división produce una cantidad menor a 1, el valor del mercado disminuye. De lo contrario, el mercado se está apreciando.
//+------------------------------------------------------------------+ //| Update our system setup | //+------------------------------------------------------------------+ void update(void) { //--- Fetch updated prices xagusd.CopyRates(SYMBOL_ONE,TF_1,COPY_RATES_CLOSE,1,FETCH); xageur.CopyRates(SYMBOL_TWO,TF_1,COPY_RATES_CLOSE,1,FETCH); eurusd.CopyRates(SYMBOL_THREE,TF_1,COPY_RATES_CLOSE,1,FETCH); //--- Calculate the growth in market prices eurusd_growth = eurusd[0] / eurusd[FETCH - 1]; xageur_growth = xageur[0] / xageur[FETCH - 1]; xagusd_growth = xagusd[0] / xagusd[FETCH - 1]; //--- Update system variables SymbolSelect(SYMBOL_ONE,true); bid = SymbolInfoDouble(SYMBOL_ONE,SYMBOL_BID); ask = SymbolInfoDouble(SYMBOL_ONE,SYMBOL_ASK); //--- Check if we need to setup a new position if(PositionsTotal() == 0) find_setup(); //--- Check if we need to manage our positions if(PositionsTotal() > 0) manage_setup(); //--- Give feedback on the market growth Comment("EURUSD Growth: ",eurusd_growth,"\nXAGEUR Growth: ",xageur_growth,"\nXAGUSD Grwoth: ",xagusd_growth); }
Nuestra configuración comercial puede imaginarse como una configuración específica en la que queremos encontrar los mercados, unos en relación con otros. Queremos ver esencialmente los movimientos en el mercado XAGUSD, respaldados por los movimientos en los mercados EURUSD y XAGEUR, respectivamente. Las reglas que hemos especificado a continuación son equivalentes a la ilustración visual que dimos en las figuras 4 y 5.
//+------------------------------------------------------------------+ //| Find setup | //+------------------------------------------------------------------+ void find_setup(void) { //--- Check if the current market setup matches our expectations for selling if((eurusd_growth < 1) && (xageur_growth > 1) && (xagusd_growth < 1)) { Trade.Sell(VOLUME,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),""); } //--- Check if the current market setup matches our expectations for buying if((eurusd_growth > 1) && (xageur_growth < 1) && (xagusd_growth > 1)) { Trade.Buy(VOLUME,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),""); } }
Una vez abiertas, nuestras operaciones serán supervisadas por un stop loss dinámico para ayudarnos a retener nuestras ganancias.
//+------------------------------------------------------------------+ //| Manage setup | //+------------------------------------------------------------------+ void manage_setup(void) { //--- Select our open position if(PositionSelect(SYMBOL_ONE)) { double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); //--- Buy setup if(current_sl < current_tp) { if((bid - sl_width) > current_sl) Trade.PositionModify(SYMBOL_ONE,(bid - sl_width),(bid + sl_width)); } //--- Sell setup if(current_sl > current_tp) { if((ask + sl_width) < current_sl) Trade.PositionModify(SYMBOL_ONE,(ask + sl_width),(ask - sl_width)); } } }Por último, definimos las constantes del sistema que definimos anteriormente.
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef TF_1 #undef SYMBOL_ONE #undef SYMBOL_TWO #undef SYMBOL_THREE #undef VOLUME #undef FETCH
Cuando reunimos todos los componentes de nuestro sistema nuestra aplicación está completa.
//+------------------------------------------------------------------+ //| Baseline Model.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System Constants | //+------------------------------------------------------------------+ #define SYMBOL_ONE "XAGUSD" //--- Our primary symbol, the price of Silver in USD #define SYMBOL_TWO "XAGEUR" //--- Our secondary symbol, the price of Silver in EUR #define SYMBOL_THREE "EURUSD" //--- Our EURUSD exchange rate. #define FETCH 24 //--- How many bars of data should we fetch? #define TF_1 PERIOD_H1 //--- Our intended time frame #define VOLUME SymbolInfoDouble(SYMBOL_ONE,SYMBOL_VOLUME_MIN) * 10 //--- Our trading volume //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ vector eurusd,xagusd,xageur; double eurusd_growth,xagusd_growth,xageur_growth,bid,ask; double sl_width = 3e2 * _Point; //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Setup our technical indicators setup(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- New prices have been quoted new_quotes_received(); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Custom functions | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Updates system variables accordingly | //+------------------------------------------------------------------+ void new_quotes_received(void) { static datetime time_stamp; datetime time = iTime(SYMBOL_ONE,TF_1,0); if(time_stamp != time) { time_stamp = time; update(); } } //+------------------------------------------------------------------+ //| Setup our technical indicators and select the symbols we need | //+------------------------------------------------------------------+ void setup(void) { //--- Select the symbols we need SymbolSelect(SYMBOL_ONE,true); SymbolSelect(SYMBOL_TWO,true); SymbolSelect(SYMBOL_THREE,true); } //+------------------------------------------------------------------+ //| Update our system setup | //+------------------------------------------------------------------+ void update(void) { //--- Fetch updated prices xagusd.CopyRates(SYMBOL_ONE,TF_1,COPY_RATES_CLOSE,1,FETCH); xageur.CopyRates(SYMBOL_TWO,TF_1,COPY_RATES_CLOSE,1,FETCH); eurusd.CopyRates(SYMBOL_THREE,TF_1,COPY_RATES_CLOSE,1,FETCH); //--- Calculate the growth in market prices eurusd_growth = eurusd[0] / eurusd[FETCH - 1]; xageur_growth = xageur[0] / xageur[FETCH - 1]; xagusd_growth = xagusd[0] / xagusd[FETCH - 1]; //--- Update system variables SymbolSelect(SYMBOL_ONE,true); bid = SymbolInfoDouble(SYMBOL_ONE,SYMBOL_BID); ask = SymbolInfoDouble(SYMBOL_ONE,SYMBOL_ASK); //--- Check if we need to setup a new position if(PositionsTotal() == 0) find_setup(); //--- Check if we need to manage our positions if(PositionsTotal() > 0) manage_setup(); //--- Give feedback on the market growth Comment("EURUSD Growth: ",eurusd_growth,"\nXAGEUR Growth: ",xageur_growth,"\nXAGUSD Grwoth: ",xagusd_growth); } //+------------------------------------------------------------------+ //| Find setup | //+------------------------------------------------------------------+ void find_setup(void) { //--- Check if the current market setup matches our expectations for selling if((eurusd_growth < 1) && (xageur_growth > 1) && (xagusd_growth < 1)) { Trade.Sell(VOLUME,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),""); } //--- Check if the current market setup matches our expectations for buying if((eurusd_growth > 1) && (xageur_growth < 1) && (xagusd_growth > 1)) { Trade.Buy(VOLUME,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),""); } } //+------------------------------------------------------------------+ //| Manage setup | //+------------------------------------------------------------------+ void manage_setup(void) { //--- Select our open position if(PositionSelect(SYMBOL_ONE)) { double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); //--- Buy setup if(current_sl < current_tp) { if((bid - sl_width) > current_sl) Trade.PositionModify(SYMBOL_ONE,(bid - sl_width),(bid + sl_width)); } //--- Sell setup if(current_sl > current_tp) { if((ask + sl_width) < current_sl) Trade.PositionModify(SYMBOL_ONE,(ask + sl_width),(ask - sl_width)); } } } //+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef TF_1 #undef SYMBOL_ONE #undef SYMBOL_TWO #undef SYMBOL_THREE #undef VOLUME #undef FETCH
Al comienzo de nuestra discusión, especificamos que nuestra prueba retrospectiva se realizará desde el primero de noviembre de 2023 hasta enero de 2025. Utilizaremos el marco de tiempo H1 para realizar nuestra prueba. Esperamos que el H1 nos proporcione más oportunidades comerciales que en marcos de tiempo más altos, como el Diario, y que no nos confunda con tanto ruido cuando operamos con marcos de tiempo incluso más pequeños, como el M1.

Figura 6: Las fechas que utilizaremos para nuestra prueba retrospectiva de nuestra estrategia XAGUSD.
Nuestras condiciones comerciales se seleccionarán para simular la experiencia del comercio real. Queremos seleccionar un retraso aleatorio, con cada tic basado en tics reales. Esto proporciona una simulación confiable de las condiciones del mercado experimentadas en el pasado.

Figura 7: Nuestras condiciones comerciales, utilizando ticks reales, son la configuración más realista que se nos ofrece.
Nuestra estrategia produjo una curva de capital caracterizada por períodos de rentabilidad seguidos de períodos prolongados de pérdidas. La estrategia es rentable, pero es inestable en esta forma. Dado que nuestra estrategia sigue «oscilando» entre períodos rentables y no rentables, es lógico pensar que aprender a identificar este comportamiento periódico puede ayudarnos a hacer que la estrategia sea más estable.

Figura 8: La curva de capital generada por nuestra estrategia comercial.
Al realizar una prueba retrospectiva utilizando nuestra estrategia de trading, se observa que nuestro sistema podría haber sido potencialmente mucho más rentable. Nuestro objetivo de utilizar tres mercados para negociar con éxito 1 está al alcance de la mano. Es posible que nuestras reglas de negociación deban revisarse y que puedan ser cuestionadas por las reglas que nuestro ordenador puede generar a partir de los datos. Esto puede servir como solución a nuestras preocupaciones sobre la estabilidad del algoritmo.

Figura 9: Análisis detallado del rendimiento de nuestra estrategia comercial.
Mejorando nuestro rendimiento inicial
Hay mucho margen de mejora en lo que respecta a nuestra estrategia inicial. Intentaremos corregir el comportamiento inestable de nuestra estrategia inicial y enderezar las oscilaciones periódicas que observamos desde períodos rentables hasta períodos no rentables sostenidos. Resumiremos brevemente los cambios realizados en el sistema para obtener mejores resultados de nuestra estrategia de trading:
| Mejorar la propuesta | Propósito previsto |
|---|---|
| Integración de indicadores técnicos para una confirmación adicional | Podemos filtrar una gran parte del ruido del mercado utilizando la confirmación adicional de nuestros indicadores técnicos en nuestra terminal MetaTrader 5, para reducir el retraso en nuestra toma de decisiones. |
| Creación de modelos estadísticos individuales para cada mercado que seguimos | Disponer de un modelo del mercado puede ayudarnos a anticipar cambios en la dirección o la volatilidad del mercado, e incluso ayudarnos posteriormente a reducir el tamaño de nuestras posiciones en momentos de incertidumbre. |
Para empezar, primero necesitamos obtener cotizaciones históricas del mercado para disponer de datos con los que entrenar nuestro modelo. Cuando se emplean modelos estadísticos, no sabemos desde el principio qué conjunto de datos va a dar lugar al mejor modelo. Por lo tanto, por lo general es recomendable pensar en tantas características diferentes como sea posible y luego reducir las opciones más adelante.
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //--- File name string file_name = "XAGEUR XAGUSD EURUSD Triangular Exchange Rates.csv"; //--- Amount of data requested input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { //---Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle,"Time","XAGUSD Open","XAGUSD High","XAGUSD Low","XAGUSD Close","XAGEUR Open","XAGEUR High","XAGEUR Low","XAGEUR Close","EURUSD Open","EURUSD High","EURUSD Low","EURUSD Close","Open Squared","High Squared","Low Squared","Close Squared","Open Cubed","High Cubed","Low Cubed","Close Cubed","Open Squre Root","High Square Root","Low Square Root","Close Square Root","Open Growth","High Growth","Low Grwoth","Close Growth","O / H","O / L","O / C","H / L","Log Open Growth","Log High Grwoth","Log Low Growth","Log Close Grwoth","Sin H / L","Cos O / C"); } else { FileWrite(file_handle, iTime("XAGUSD",PERIOD_CURRENT,i), iOpen("XAGUSD",PERIOD_CURRENT,i), iHigh("XAGUSD",PERIOD_CURRENT,i), iLow("XAGUSD",PERIOD_CURRENT,i), iClose("XAGUSD",PERIOD_CURRENT,i), iOpen("XAGEUR",PERIOD_CURRENT,i), iHigh("XAGEUR",PERIOD_CURRENT,i), iLow("XAGEUR",PERIOD_CURRENT,i), iClose("XAGEUR",PERIOD_CURRENT,i), iOpen("EURUSD",PERIOD_CURRENT,i), iHigh("EURUSD",PERIOD_CURRENT,i), iLow("EURUSD",PERIOD_CURRENT,i), iClose("EURUSD",PERIOD_CURRENT,i), MathPow(iOpen("XAGUSD",PERIOD_CURRENT,i),2), MathPow(iHigh("XAGUSD",PERIOD_CURRENT,i),2), MathPow(iLow("XAGUSD",PERIOD_CURRENT,i),2), MathPow(iClose("XAGUSD",PERIOD_CURRENT,i),2), MathPow(iOpen("XAGUSD",PERIOD_CURRENT,i),3), MathPow(iHigh("XAGUSD",PERIOD_CURRENT,i),3), MathPow(iLow("XAGUSD",PERIOD_CURRENT,i),3), MathPow(iClose("XAGUSD",PERIOD_CURRENT,i),3), MathSqrt(iOpen("XAGUSD",PERIOD_CURRENT,i)), MathSqrt(iHigh("XAGUSD",PERIOD_CURRENT,i)), MathSqrt(iLow("XAGUSD",PERIOD_CURRENT,i)), MathSqrt(iClose("XAGUSD",PERIOD_CURRENT,i)), (iOpen("XAGUSD",PERIOD_CURRENT,i) / iOpen("XAGUSD",PERIOD_CURRENT,i+1)), (iHigh("XAGUSD",PERIOD_CURRENT,i) / iHigh("XAGUSD",PERIOD_CURRENT,i+1)), (iLow("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+1)), (iClose("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i+1)), (iOpen("XAGUSD",PERIOD_CURRENT,i) / iHigh("XAGUSD",PERIOD_CURRENT,i+1)), (iOpen("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+1)), (iOpen("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i+1)), (iHigh("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+1)), MathLog10(iOpen("XAGUSD",PERIOD_CURRENT,i) / iOpen("XAGUSD",PERIOD_CURRENT,i+1)), MathLog10(iHigh("XAGUSD",PERIOD_CURRENT,i) / iHigh("XAGUSD",PERIOD_CURRENT,i+1)), MathLog10(iLow("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i+1)), MathLog10(iClose("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i+1)), (MathSin(iHigh("XAGUSD",PERIOD_CURRENT,i) / iLow("XAGUSD",PERIOD_CURRENT,i))), (MathCos(iOpen("XAGUSD",PERIOD_CURRENT,i) / iClose("XAGUSD",PERIOD_CURRENT,i))) ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+
Analizando nuestros datos en Python
Una vez recopilados nuestros datos de entrenamiento, estamos listos para comenzar a construir un modelo estadístico de los datos. Primero, importaremos las bibliotecas de Python que necesitamos.
#Import libraries we need import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt
Ahora etiquetaremos los datos. Recordemos que estamos operando en el marco temporal H1, por lo que estableceremos nuestra etiqueta como el cambio en los niveles de precios durante 24 horas (1 día de negociación).
#Clean up the data LOOK_AHEAD = 24 data = pd.read_csv("../XAGEUR XAGUSD EURUSD Triangular Exchange Rates.csv") data["Target"] = data["XAGUSD Close"].shift(-LOOK_AHEAD) - data["XAGUSD Close"] data.dropna(inplace=True) data.reset_index(inplace=True,drop=True)
Este es el paso más importante de todo el proceso que estamos siguiendo juntos. Nuestros datos contendrán inicialmente cotizaciones de mercado que se superponen con nuestro período de prueba retrospectiva. Esto no es deseable para los profesionales que buscan obtener una estimación honesta del valor de su estrategia comercial.
Por lo tanto, eliminaremos todos los datos de mercado que se superpongan con el período de prueba retrospectiva previsto. Recordemos que en la figura 6, nuestro período de backtest comienza claramente el 1 de noviembre de 2023, y en la Fig. 10, nuestros datos de entrenamiento finalizan el 31 de octubre de 2023.
#Drop the dates corresponding to our backtest _ = data.iloc[-((24 * 365) - 918):,:] #Keep the dates before our backtest data = data.iloc[:-((24 * 365) - 918),:] data

Figura 10: Asegúrese de que los datos de entrenamiento que utiliza para sus modelos estadísticos no revelen información sobre el futuro.
Si visualizamos el crecimiento que se produce en los mercados XAGUSD y XAGEUR, observamos que el diferencial entre ambos mercados crece y se contrae con el paso del tiempo. Esto podría indicar que existen oportunidades de arbitraje entre estos dos mercados. De lo contrario, si no existieran oportunidades de arbitraje, las líneas roja y verde deberían haberse superpuesto perfectamente sin separarse nunca, desde el principio de los tiempos hasta el día de hoy. Huelga decir que no es así. Los dos mercados se desacoplan claramente durante algún tiempo, antes de que se produzca una corrección posterior.
plt.title("Comparing XAGUSD & XAGEUR Growth") plt.plot((data['XAGUSD Close'] / data.loc[0,"XAGUSD Close"]) / (data['XAGUSD Close'].max() - data['XAGUSD Close'].min()),color="red") plt.plot((data['XAGEUR Close'] / data.loc[0,"XAGEUR Close"]) / (data['XAGEUR Close'].max() - data['XAGEUR Close'].min()),color="green") plt.ylabel("Commodity Growth") plt.xlabel("Time") plt.legend(["XAGUSD","XAGEUR"]) plt.grid()

Figura 11: La visualización de nuestros dos mercados financieros superpuestos nos permite detectar posibles oportunidades de arbitraje que podemos aprovechar.
Etiqueta nuestras entradas y el objetivo.
X = data.iloc[:,1:-1].columns y = "Target"
Prepárese para ajustar un árbol de refuerzo de gradiente a nuestros datos y exportarlo al formato ONNX. Los árboles de impulso gradiente son conocidos por su capacidad para detectar interacciones que se producen dentro de un conjunto de datos determinado. Esperamos aprovechar sus potentes capacidades de detección de patrones para mejorar nuestra estrategia comercial.
import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorTypea from sklearn.ensemble import GradientBoostingRegressor
Ahora construyamos modelos estadísticos específicos para cada mercado que nos interese. Esperamos que esto nos ayude a filtrar las falsas rupturas. Ajuste cada modelo a los datos y expórtelo al formato ONNX para que posteriormente podamos importar los tres a nuestra aplicación MetaTrader 5.
Comencemos por representar primero un modelo del mercado XAGUSD.
model = GradientBoostingRegressor() model.fit(data.loc[:,["XAGUSD Open","XAGUSD High","XAGUSD Low","XAGUSD Close"]],data.loc[:,y]) initial_types = [("float_input",FloatTensorType([1,4]))] xagusd_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12) onnx.save(xagusd_model_proto,"../XAGUSD State Model.onnx")
En segundo lugar, continuaremos con el mercado XAGEUR.
model = GradientBoostingRegressor() model.fit(data.loc[:,["XAGEUR Open","XAGEUR High","XAGEUR Low","XAGEUR Close"]],data.loc[:,"XAGEUR Target"]) initial_types = [("float_input",FloatTensorType([1,4]))] xageur_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12) onnx.save(xageur_model_proto,"../XAGEUR State Model.onnx")
Y, por último, exportaremos nuestro modelo estadístico del mercado EURUSD.
model = GradientBoostingRegressor() model.fit(data.loc[:,["EURUSD Open","EURUSD High","EURUSD Low","EURUSD Close"]],data.loc[:,"EURUSD Target"]) initial_types = [("float_input",FloatTensorType([1,4]))] eurusd_model_proto = convert_sklearn(model,initial_types=initial_types,target_opset=12) onnx.save(eurusd_model_proto,"../EURUSD State Model.onnx")
Nuestro sistema otorgará mayor importancia a los movimientos del mercado que estén respaldados por nuestros tres modelos al unísono. En primer lugar, queremos observar cómo se forman en el mercado los patrones que describimos en las figuras 3 y 4, seguidos de nuestros tres modelos que pronostican que nuestros patrones no desaparecerán rápidamente, sino que se mantendrán a lo largo del tiempo. Esto nos dará una medida razonable de confianza en la solidez que respalda los movimientos del mercado que esperamos que se produzcan.
Implementación de nuestras mejoras en MQL5
Ahora podemos empezar a implementar nuestras mejoras en la versión original de nuestra estrategia comercial. En primer lugar, añadiremos indicadores técnicos adicionales a nuestro arsenal, para proporcionar a nuestro ordenador una mejor orientación y detección de tendencias. El cruce de medias móviles es una buena estrategia para esta tarea. Sin embargo, utilizaremos una versión más receptiva de la estrategia, que minimiza el retraso en las señales de negociación. Los lectores que estén interesados en obtener más información sobre esta versión de la estrategia de cruce de medias móviles pueden encontrar más detalles aquí.
#define XAGUSD_MA_PERIOD 8
Cargue los modelos ONNX que acabamos de crear como recursos del sistema.
//+------------------------------------------------------------------+ //| System resources | //+------------------------------------------------------------------+ #resource "\\Files\\XAGUSD State Model.onnx" as uchar xagusd_onnx_buffer[] #resource "\\Files\\XAGEUR State Model.onnx" as uchar xageur_onnx_buffer[] #resource "\\Files\\EURUSD State Model.onnx" as uchar eurusd_onnx_buffer[]
Necesitaremos una nueva variable global que se corresponda con los indicadores de media móvil que estamos introduciendo, así como los componentes individuales necesarios para completar nuestro modelo ONNX.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ vector eurusd,xagusd,xageur; double eurusd_growth,xagusd_growth,xageur_growth,bid,ask; double sl_width = 3e2 * _Point; int xagusd_f_ma_handler,xagusd_s_ma_handler; double xagusd_f[],xagusd_s[]; vectorf model_output = vectorf::Zeros(1); long onnx_model; vectorf xageur_model_output = vectorf::Zeros(1); long xageur_onnx_model; vectorf eurusd_model_output = vectorf::Zeros(1); long eurusd_onnx_model;
Algunas de nuestras funciones deben ser refactorizadas para cumplir con nuestras nuevas expectativas. Para empezar, cuando nuestra aplicación de trading ya no se utiliza, debemos liberar de forma segura los recursos que se asignaron a nuestros modelos ONNX y nuestras dos medias móviles.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- OnnxRelease(onnx_model); OnnxRelease(xageur_onnx_model); OnnxRelease(eurusd_onnx_model); IndicatorRelease(xagusd_f_ma_handler); IndicatorRelease(xagusd_s_ma_handler); Print("System deinitialized"); }
Además, nuestro procedimiento de configuración también debe adaptarse a nuestros modelos ONNX y nuestros indicadores técnicos. Tenemos que validar que nuestros indicadores y modelos deseados se han cargado y especificado correctamente. De lo contrario, interrumpiremos el procedimiento de inicialización e informaremos al usuario de lo que ha fallado.
//+------------------------------------------------------------------+ //| Setup our technical indicators and select the symbols we need | //+------------------------------------------------------------------+ bool setup(void) { //--- Select the symbols we need SymbolSelect(SYMBOL_ONE,true); SymbolSelect(SYMBOL_TWO,true); SymbolSelect(SYMBOL_THREE,true); //--- Setup the moving averages xagusd_f_ma_handler = iMA(SYMBOL_ONE,TF_1,XAGUSD_MA_PERIOD,0,MODE_SMA,PRICE_OPEN); xagusd_s_ma_handler = iMA(SYMBOL_ONE,TF_1,XAGUSD_MA_PERIOD,0,MODE_SMA,PRICE_CLOSE); if((xagusd_f_ma_handler == INVALID_HANDLE) || (xagusd_s_ma_handler == INVALID_HANDLE)) { Comment("Failed to load our technical indicators correctly. ", GetLastError()); return(false); } //--- Setup our statistical models onnx_model = OnnxCreateFromBuffer(xagusd_onnx_buffer,ONNX_DEFAULT); xageur_onnx_model = OnnxCreateFromBuffer(xageur_onnx_buffer,ONNX_DEFAULT); eurusd_onnx_model = OnnxCreateFromBuffer(eurusd_onnx_buffer,ONNX_DEFAULT); if(onnx_model == INVALID_HANDLE) { Comment("Failed to create our XAGUSD ONNX model correctly. ",GetLastError()); return(false); } if(xageur_onnx_model == INVALID_HANDLE) { Comment("Failed to create our XAGEUR ONNX model correctly. ",GetLastError()); return(false); } if(eurusd_onnx_model == INVALID_HANDLE) { Comment("Failed to create our EURUSD ONNX model correctly. ",GetLastError()); return(false); } ulong input_shape[] = {1,4}; ulong output_shape[] = {1,1}; if(!(OnnxSetInputShape(onnx_model,0,input_shape))) { Comment("Failed to specify XAGUSD model input shape. ",GetLastError()); return(false); } if(!(OnnxSetInputShape(xageur_onnx_model,0,input_shape))) { Comment("Failed to specify XAGEUR model input shape. ",GetLastError()); return(false); } if(!(OnnxSetInputShape(eurusd_onnx_model,0,input_shape))) { Comment("Failed to specify EURUSD model input shape. ",GetLastError()); return(false); } if(!(OnnxSetOutputShape(onnx_model,0,output_shape))) { Comment("Failed to specify XAGUSD model output shape. ",GetLastError()); return(false); } if(!(OnnxSetOutputShape(xageur_onnx_model,0,output_shape))) { Comment("Failed to specify XAGEUR model output shape. ",GetLastError()); return(false); } if(!(OnnxSetOutputShape(eurusd_onnx_model,0,output_shape))) { Comment("Failed to specify EURUSD model output shape. ",GetLastError()); return(false); } Print("System initialized succefully"); //--- If we have gotten this far, everything went fine. return(true); }
También necesitaremos una función específica para obtener predicciones de nuestro modelo ONNX. Nuestra función preparará primero las entradas del modelo ONNX y, a continuación, llamará a la función OnnxRun para obtener una predicción de nuestro modelo.
//+------------------------------------------------------------------+ //| Fetch a prediction from our model | //+------------------------------------------------------------------+ void model_predict(void) { vectorf model_inputs = { (float) iOpen(SYMBOL_ONE,TF_1,1), (float) iClose(SYMBOL_ONE,TF_1,1)}; OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_output); Print(StringFormat("Model forecast: %d",model_output)); }
También debemos revisar nuestro procedimiento para encontrar una configuración comercial. El primer paso es obtener una previsión de nuestro modelo y, una vez completada, buscamos una confirmación adicional en los cruces de medias móviles. Recordemos que nuestra versión específica de los cruces de medias móviles coloca una media móvil en el precio de apertura y la otra en el precio de cierre, compartiendo ambas medias móviles el mismo periodo. Cuando la media móvil abierta está en la parte superior, lo interpretamos como una señal corta. Si no, apostamos por el largo plazo.
Por lo tanto, nuestra condición final será que nuestro regresor potenciado por gradiente espere que la acción del precio sea coherente con las dos señales que hemos observado. Si este es el caso, consideraremos que se trata de una configuración de operación con alta probabilidad y duplicaremos el tamaño de nuestra posición. De lo contrario, operaremos de forma conservadora.
//+------------------------------------------------------------------+ //| Find setup | //+------------------------------------------------------------------+ void find_setup(void) { model_predict(); //--- Check if the current market setup matches our expectations for selling if((eurusd_growth < 1) && (xageur_growth > 1) && (xagusd_growth < 1)) { if(xagusd_s[0] < xagusd_f[0]) { if(model_output[0] < 0) { //--- If all our systems align, we have a high probability trade setup Trade.Sell(VOLUME * 2,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),""); } //--- Otherwise, we should trade conservatively Trade.Sell(VOLUME,SYMBOL_ONE,bid,(ask + sl_width),(ask - sl_width),""); } } //--- Check if the current market setup matches our expectations for buying if((eurusd_growth > 1) && (xageur_growth < 1) && (xagusd_growth > 1)) { if(xagusd_s[0] > xagusd_f[0]) { if(model_output[0] > 0) { Trade.Buy(VOLUME * 2,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),""); } Trade.Buy(VOLUME,SYMBOL_ONE,ask,(bid - sl_width),(bid + sl_width),""); } } }
Repitamos ahora la misma prueba que realizamos anteriormente, pero esta vez utilizaremos nuestra versión perfeccionada de la estrategia de negociación. Recordemos que nos aseguramos de que las fechas de nuestras pruebas retrospectivas no se solaparan con las fechas que utilizamos para entrenar nuestros modelos estadísticos.

Figura 12: Estamos utilizando el mismo periodo de prueba en ambas pruebas. Además, ninguno de nuestros modelos estadísticos ha visto estos datos anteriormente.
Como es de esperar, todos los ajustes se mantendrán constantes para garantizar una comparación justa entre ambas estrategias de negociación.

Figura 13: Lo ideal es que estos ajustes se mantengan iguales en las dos pruebas que realice.
Analicemos ahora los resultados que hemos obtenido. En nuestra prueba retrospectiva inicial, obtuvimos un índice de Sharpe de 0,14, mientras que nuestra estrategia revisada muestra un índice de Sharpe de 1,85. Esto supone una mejora significativa en nuestro ratio de Sharpe, lo que significa que hemos logrado ser más rentables de manera eficaz, al tiempo que asumimos riesgos adicionales de forma responsable. Los bajos índices de Sharpe se asocian con una alta varianza en rendimientos bajos.
Además, nuestra pérdida media se redujo de unos 115 dólares por operación a unos 109 dólares por operación, mientras que nuestra ganancia media, por otro lado, creció de una media de 188 dólares a una media de 213 dólares. Esto es un comentario positivo para nosotros. Nuestra ganancia total también creció de $395 en nuestra primera iteración de la estrategia a $1,449 en esta iteración actual. Todo esto, mientras realizamos menos operaciones que nuestra versión de la estrategia configurada manualmente.

Figura 14: Un resumen detallado del desempeño histórico de nuestra estrategia comercial en el mercado XAGUSD.
Los cambios que hemos aplicado a nuestro sistema rectificaron las oscilaciones inestables en el saldo de las cuentas que observamos en la versión inicial de nuestra estrategia. Según nuestro backtest, en promedio, nuestra nueva estrategia tiende a obtener más ganancias de las que tiende a perder en cada operación. Esto le da a nuestra nueva curva de ganancias y pérdidas períodos bajos más superficiales que las profundidades de las pérdidas acumuladas en la versión original y más riesgosa de nuestra estrategia comercial.

Figura 15: Visualización de la curva de ganancias y pérdidas producida por nuestra versión revisada de la estrategia comercial.
Conclusión
Después de leer este artículo, se espera que el lector obtenga una estrategia algorítmica para operar en mercados interconectados. El lector se irá con un entendimiento de cómo combinar su experiencia en el dominio para encontrar y operar de manera rentable en mercados triangulares. Utilizando modelos estadísticos, el lector puede encontrar mejor las configuraciones comerciales exactas que está buscando. Aprovechando la naturaleza interconectada de ciertos mercados, siempre podemos crear indicadores en tiempo real de la verdadera fortaleza del mercado.
| Nombre del archivo adjunto | Descripción |
|---|---|
| Baseline_Model | Nuestra versión inicial de la estrategia de trading triangular. |
| Second Version | La versión revisada y más rentable de nuestra estrategia comercial. |
| EURUSD State Model | Nuestro modelo estadístico del mercado EURUSD. |
| XAGEUR State Model | Nuestro modelo estadístico del mercado XAGEUR. |
| XAGUSD State Model | Nuestro modelo estadístico del mercado XAGUSD. |
| Triangular Exchange Rates | El cuaderno Jupyter que usamos para analizar nuestros datos de mercado y construir nuestros modelos estadísticos del mercado. |
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/17258
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Desarrollamos un asesor experto multidivisas (Parte 24): Añadimos una nueva estrategia (I)
Introducción a las curvas ROC (Receiver Operating Characteristic)
Características del Wizard MQL5 que debe conocer (Parte 56): Fractales de Bill Williams
Aplicación de la teoría de juegos a algoritmos comerciales
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso