Desarrollo de asesores expertos autooptimizables en MQL5 (Parte 9): Cruce de dos medias móviles
En nuestra serie de artículos, hemos analizado diversas perspectivas sobre cómo reducir el retraso en la estrategia clásica de trading basada en el cruce de medias móviles.
Nuestros primeros intentos consistieron en utilizar herramientas de modelización estadística para predecir con antelación los cruces de medias móviles. Hemos avanzado en esta dirección y hemos constatado que, en las condiciones de mercado adecuadas, predecir los cruces de medias móviles puede resultar más preciso que predecir directamente el precio. A partir de ahí, descubrimos otro método más para reducir aún más el retraso. Este método consiste en fijar los períodos de las dos medias móviles de modo que compartan un valor común y, en su lugar, generar cruces aplicando una media móvil al precio de apertura y la otra al precio de cierre. Este sistema alternativo resultó eficaz, ya que nos permitió reducir aún más el retraso sin necesidad de recurrir a herramientas de modelización avanzadas, simplemente utilizando el mismo período y variando el precio aplicado a los dos indicadores.
En este debate, analizamos otro enfoque singular que aún no habíamos tenido en cuenta. Al igual que ocurre con la mayoría de los problemas de la vida y de las matemáticas, hay más de una forma de abordar una cuestión, y cada solución tiene sus propias ventajas e inconvenientes. Al sopesar estas alternativas, nuestro objetivo es comprender hasta qué punto podemos controlar el retraso en el sistema.
Aquí vamos a probar lo que yo denomino «estrategia de cruce de dos medias móviles». Como se muestra en la figura 1, la estrategia clásica de cruce de medias móviles se suele aplicar en un único marco temporal con dos medias móviles de diferentes períodos. A diferencia de lo que vimos anteriormente —donde ambas medias móviles tenían un período fijo—, en esta ocasión volvemos ligeramente al enfoque clásico, permitiendo que los dos indicadores utilicen períodos diferentes.
El problema de este método original es que la confirmación de las señales de entrada suele llegar tarde —cuando el movimiento ya ha comenzado—, lo que provoca entradas tardías o la pérdida de oportunidades.

Figura 1: Visualización de nuestra estrategia de cruce de medias móviles en el marco temporal diario.
Lo que proponemos aquí no es del todo nuevo. De hecho, los operadores discrecionales o humanos llevan mucho tiempo aplicando una lógica similar. La idea fundamental consiste en observar primero los patrones de cruce en un marco temporal superior (por ejemplo, el gráfico diario que se muestra en la figura 1). Sin embargo, no reaccionamos de inmediato ante estas señales. En cambio, en cuanto observamos un cruce en el marco temporal superior, descendemos a un marco temporal inferior —como el M30 que se muestra en la figura 2— y buscamos patrones de cruce que se correspondan con lo que hemos observado en el marco temporal superior.
Los operadores humanos suelen decir: «Opera en consonancia con el marco temporal superior». En la mayoría de nuestros debates sobre el trading algorítmico, aplicamos y operamos una estrategia en el mismo marco temporal. Hoy, sin embargo, aplicamos la estrategia dos veces: una en un marco temporal superior y otra en uno inferior. El marco temporal superior nos proporciona un sesgo direccional para la jornada, mientras que en el marco temporal inferior buscamos señales de entrada que se ajusten a dicha tendencia. Esta es la esencia de nuestra estrategia de doble cruce. Al establecer primero una tendencia en un marco temporal superior y, a continuación, buscar oportunidades para operar en consonancia con ella en un marco temporal inferior, esperamos filtrar en parte el desfase que introducen las señales generadas por el marco temporal superior.
Ahora que todos estamos de acuerdo en lo que entendemos, podemos empezar a poner en práctica la estrategia para comprobar si tiene sentido. Antes de adentrarnos en el código, está claro que este enfoque presenta varios aspectos que debemos considerar detenidamente. Una cuestión clave es: ¿cómo definimos las condiciones de entrada? Supongamos que el marco temporal superior muestra un cruce alcista. A continuación, tenemos dos opciones en el marco temporal inferior:
- Entrada contraria: Espere a que se produzca un cruce bajista en el marco temporal inferior y, a continuación, apueste en contra, creyendo que el marco temporal inferior acabará realineándose con la tendencia alcista al final del día.
- Entrada siguiendo la tendencia: simplemente espere a que el marco temporal inferior forme un cruce alcista en la misma dirección que la tendencia del marco temporal superior.
Estas dos opciones representan enfoques distintos para abrir operaciones con esta estrategia. Cerrar posiciones introduce aún más complejidad y variación, cada una con sus propias ventajas e inconvenientes. Por ejemplo, podríamos decidir cerrar una posición cuando el marco temporal inferior ya no se alinee con la tendencia del marco temporal superior. Alternativamente, podríamos salir solo cuando el sesgo cambie en el marco temporal superior. Así pues, si comenzamos el día con una señal alcista en el gráfico diario, mantenemos nuestra posición hasta que ese marco temporal superior cambie a bajista.
Como puede ver, existen muchas maneras de entrar y salir de operaciones utilizando este método. En lugar de basarnos únicamente en el razonamiento para determinar la combinación óptima, debemos utilizar un optimizador genético. Esto nos ayudará a identificar, a partir de los datos de mercado, qué combinación de estas alternativas podría ser la más rentable.

Figura 2: Visualización de nuestra estrategia de cruce de medias móviles en un marco temporal inferior, el M30.
El primer paso que daremos será definir algunas constantes del sistema que mantendremos fijas durante la fase de desarrollo de la aplicación. Para simplificar, mantendremos constantes los marcos temporales: el gráfico diario servirá como aproximación a nuestro marco temporal superior, el M15 como nuestro marco temporal inferior y el H4 como el marco temporal utilizado para calcular nuestro stop loss.
Podríamos considerar la posibilidad de convertir estas constantes del sistema en parámetros ajustables que un optimizador genético pueda utilizar para garantizar que logremos las mejores entradas posibles. Sin embargo, para empezar, mantendremos estos valores fijos.//+------------------------------------------------------------------+ //| Double Crossover.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ //--- System time frames #define TF_1 PERIOD_D1 #define TF_2 PERIOD_M15 #define TF_3 PERIOD_H4 #define LOT_MULTILPLE 1
También necesitamos definir un conjunto de enumeraciones personalizadas para representar los diferentes modos en los que puede operar nuestra estrategia. Por ejemplo, la aplicación puede ejecutarse en modo de seguimiento de tendencias o en modo de reversión a la media, y una enumeración específica permite al usuario alternar entre ellos. De forma similar, definimos otra enumeración para especificar si nuestras condiciones de cierre deben evaluarse en el marco temporal inferior o en el marco temporal superior.
//+------------------------------------------------------------------+ //| Custom enumerations | //+------------------------------------------------------------------+ //--- What trading style should we follow when opening our positions, trend following or mean reverting? enum STRATEGY_MODES { TREND = 0, //Trend Following Mode MEAN_REVERTING = 1 //Mean Reverting Mode }; //--- Which time frame should we consult, when determining if we should close our position? enum CLOSING_TIME_FRAME { HIGHER_TIME_CLOSE = 0, //Close on higher time frames LOWER_TIME_CLOSE = 1 //Close on lower time frames };
Nuestros parámetros de entrada son bastante sencillos. Especificaremos un valor de período en el marco temporal superior y luego definiremos un intervalo entre el primer y el segundo período de la media móvil. Este diseño garantiza que el optimizador genético seleccione un intervalo de uno o más, lo que impone una diferencia distinta de cero entre los períodos por construcción.
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "Technical Indicators" input int ma_1_period = 10; //Higher Time Frame Period input int ma_1_gap = 20; //Higher Time Frame Period Gap input int ma_2_period = 10; //Lower Time Frame Period input int ma_2_gap = 20; //Lower Time Frame Period Gap input group "Strategy Settings" input STRATEGY_MODES strategy_mode = 0; //Strategy Operation Mode input CLOSING_TIME_FRAME closing_tf = 0; //Strategy Closing Timeframe
La aplicación solo requerirá un número moderado de variables globales, como los indicadores técnicos y las instancias utilizadas para realizar un seguimiento de los precios actuales del mercado.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_c_1_handle,ma_c_2_handle,ma_c_3_handle,ma_c_4_handle; double ma_c_1[],ma_c_2[],ma_c_3[],ma_c_4[]; double volume_min; double bid,ask; int state;
La única dependencia externa necesaria para este ejercicio es la biblioteca Trade, que utilizamos para abrir y cerrar posiciones según sea necesario.
//+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
Al inicializarse, configuraremos los indicadores técnicos utilizando los ajustes proporcionados por el usuario. También restableceremos el estado del sistema a -1, lo que indica que no hay posiciones abiertas, y registraremos el volumen mínimo de negociación permitido en el mercado.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- volume_min = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN); ma_c_2_handle = iMA(Symbol(),TF_1,ma_1_period,0,MODE_SMA,PRICE_CLOSE); ma_c_1_handle = iMA(Symbol(),TF_1,(ma_1_period + ma_1_gap),0,MODE_SMA,PRICE_CLOSE); ma_c_4_handle = iMA(Symbol(),TF_2,ma_2_period,0,MODE_SMA,PRICE_CLOSE); ma_c_3_handle = iMA(Symbol(),TF_2,(ma_2_period + ma_2_gap),0,MODE_SMA,PRICE_CLOSE); state = -1; //--- return(INIT_SUCCEEDED); }
Si la aplicación deja de utilizarse, liberaremos los indicadores técnicos para liberar memoria.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(ma_c_1_handle); IndicatorRelease(ma_c_2_handle); IndicatorRelease(ma_c_3_handle); IndicatorRelease(ma_c_4_handle); }
Cuando recibimos nuevos datos de precios, comprobamos si hay una nueva vela en el marco temporal inferior (M15, en este caso). Si se detecta una nueva vela, procedemos a actualizar el estado interno de nuestro sistema.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- datetime current_time = iTime(Symbol(),TF_2,0); static datetime time_stamp; if(time_stamp != current_time) { time_stamp = current_time; update(); } } //+------------------------------------------------------------------+
Uno de los componentes más importantes es la función utilizada para identificar configuraciones de trading. Esta función acepta un parámetro conocido como padding, que representa el tamaño del stop-loss para nuestras posiciones. El padding lo calcula la función principal a partir de los rangos históricos del mercado. Como medida de seguridad, la función de configuración primero comprueba si el total de posiciones abiertas es cero para evitar el exceso de operaciones.
Si se produce un cruce alcista en el marco temporal superior y nuestra estrategia está en modo de seguimiento de tendencias, la función de configuración busca un cruce alcista coincidente en el marco temporal inferior. Si operamos en modo de reversión a la media, buscamos el cruce opuesto (es decir, bajista) y apostamos en contra de él. La lógica se refleja en las entradas bajistas: si se produce un cruce bajista en el marco temporal superior, buscamos una señal correspondiente en el marco temporal inferior, dependiendo del modo seleccionado.
//+------------------------------------------------------------------+ //| Find a trading signal | //+------------------------------------------------------------------+ void find_setup(double padding) { if(PositionsTotal() == 0) { //--- Reset the system state state = -1; //--- Bullish on the higher time frame if(ma_c_1[0] > ma_c_2[0]) { //--- Trend following mode if((ma_c_3[0] > ma_c_4[0]) && (strategy_mode == 0)) { Trade.Buy(volume_min,Symbol(),ask,(bid - padding),0,""); state = 1; } //--- Mean reverting mode if((ma_c_3[0] < ma_c_4[0]) && (strategy_mode == 1)) { Trade.Buy(volume_min,Symbol(),ask,(bid - padding),0,""); state = 1; } } //--- Bearish on the higher time frame if(ma_c_1[0] < ma_c_2[0]) { //--- Trend following mode if((ma_c_3[0] < ma_c_4[0]) && (strategy_mode == 0)) { Trade.Sell(volume_min,Symbol(),bid,(ask + padding),0,""); state = 0; } //--- Mean reverting mode if((ma_c_3[0] > ma_c_4[0]) && (strategy_mode == 1)) { Trade.Sell(volume_min,Symbol(),bid,(ask + padding),0,""); state = 0; } } } }
Una vez abierta una posición, activamos una función de gestión de posiciones independiente, que también opera en dos modos distintos dependiendo de si queremos cerrar las operaciones en función del marco temporal inferior o superior. El estado del sistema se actualiza cuando se abren posiciones, y este estado determina la dirección de las operaciones. Por ejemplo, si el estado es cero, abrimos una operación de venta. Si la primera media móvil se sitúa ahora por encima de la segunda (lo que indica una tendencia alcista en el gráfico diario) y si gestionamos las salidas en función del marco temporal superior, cerraremos la posición. De lo contrario, si la gestión se basa en un plazo menor, esperamos a que se produzca allí un cruce. No resulta obvio de inmediato qué condición de salida es más efectiva, por lo que deben probarse ambas opciones.
//+------------------------------------------------------------------+ //| Manage our open positions | //+------------------------------------------------------------------+ void manage_setup(void) { if(closing_tf == 0) { if((state ==0) && (ma_c_1[0] > ma_c_2[0])) Trade.PositionClose(Symbol()); if((state ==1) && (ma_c_1[0] < ma_c_2[0])) Trade.PositionClose(Symbol()); } else if(closing_tf == 1) { if((state ==0) && (ma_c_3[0] > ma_c_4[0])) Trade.PositionClose(Symbol()); if((state ==1) && (ma_c_3[0] < ma_c_4[0])) Trade.PositionClose(Symbol()); } }
Por último, necesitamos una función para actualizar las variables clave del sistema, como los precios de compra y venta actuales. También haremos un seguimiento de los 10 valores máximos y mínimos anteriores que se produzcan en el tercer marco temporal, nuestro marco temporal de riesgo. En este ejemplo, el marco temporal de riesgo es H4, que se sitúa perfectamente entre el M15 y el diario. Esto lo convierte en una opción útil para medir el riesgo de mercado y establecer límites de pérdidas que no sean ni demasiado ajustados ni demasiado alejados del punto de entrada.
//+------------------------------------------------------------------+ //| Update our technical indicators and positions | //+------------------------------------------------------------------+ void update(void) { //Update technical indicators and market readings CopyBuffer(ma_c_2_handle,0,0,1,ma_c_2); CopyBuffer(ma_c_1_handle,0,0,1,ma_c_1); CopyBuffer(ma_c_4_handle,0,0,1,ma_c_4); CopyBuffer(ma_c_3_handle,0,0,1,ma_c_3); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); vector high = vector::Zeros(10); vector low = vector::Zeros(10); low.CopyRates(Symbol(),TF_3,COPY_RATES_LOW,0,10); high.CopyRates(Symbol(),TF_3,COPY_RATES_HIGH,0,10); vector var = high - low; double padding = var.Mean(); //Find an open position if(PositionsTotal() == 0) find_setup(padding); //Manage our open positions else if(PositionsTotal() > 0) manage_setup(); } //+------------------------------------------------------------------+
Para comenzar con nuestro proceso de backtesting, primero debemos seleccionar el asesor experto que acabamos de crear juntos: el EX5 de doble crossover. A partir de ahí, seleccionamos el símbolo EURUSD, el marco temporal de un minuto y un período de backtesting que va desde enero de 2020 hasta este año, un backtesting de cinco años. Realizaremos pruebas prospectivas utilizando la mitad de los datos disponibles para que nuestros resultados sean lo más realistas posible.

Figura 3: Selección de nuestra aplicación de trading y nuestras fechas de formación.
Para simular las condiciones reales del mercado, estableceremos retrasos de forma aleatoria y utilizaremos ticks reales como modelo. Recordemos que nuestro procedimiento de optimización utilizará un algoritmo rápido de base genética.

Figura 4: Selección de las condiciones de simulación de mercado.
A continuación, debemos seleccionar los parámetros de entrada de nuestra estrategia que necesitan ser ajustados, como se mencionó anteriormente. Esto incluye el período de cruce de la media móvil y la brecha tanto en los marcos temporales superiores como inferiores. Además, habilitaremos dos modos operativos para la estrategia: seguimiento de tendencias y reversión a la media. Por último, la estrategia puede configurarse para cerrar posiciones en función del marco temporal superior o inferior. Nuestro optimizador genético analizará todas estas configuraciones para ayudarnos a seleccionar los parámetros más adecuados.

Figura 5: Selección de los parámetros de la estrategia que ajustará nuestro optimizador genético.
Los resultados de las pruebas retrospectivas parecen alentadores. Como se puede observar en nuestros resultados de optimización, la mayoría de nuestras estrategias fueron rentables, especialmente en el modo de reversión a la media. Sin embargo, al considerar los resultados de la prueba prospectiva, encontramos que la mayoría de las estrategias rentables operaron en modo de seguimiento de tendencias en lugar de reversión a la media, como se observó en la prueba retrospectiva.

Figura 6: Resultados de la prueba retrospectiva de nuestra prueba inicial.
Lo más preocupante es que, al examinar más de cerca los resultados de las pruebas prospectivas, ninguna de las estrategias de backtesting coincide con los resultados de las pruebas prospectivas. Es decir, casi ninguna de las estrategias que obtuvieron buenos resultados en la prueba prospectiva fue rentable también en la prueba retrospectiva.

Figura 7: Los resultados prospectivos de nuestra prueba inicial indican que la mayoría de nuestras estrategias no fueron estables en ambas pruebas.
Tuve que filtrar manualmente los resultados para encontrar configuraciones de estrategia que fueran rentables en ambas pruebas. Esto es lo que buscamos como señal de estabilidad: que una estrategia sea rentable tanto en la prueba retrospectiva como en la prueba prospectiva. Al ver que solo un puñado de configuraciones cumplían con este estándar, me animé a intentar mejoras adicionales.

Figura 8: Solo un puñado de las estrategias producidas por nuestro optimizador genético resultaron rentables en ambas pruebas. Esto no es un buen indicador.
Mejoras adicionales
Una buena razón para mejorar la estrategia es permitir que el optimizador genético controle el marco temporal utilizado para calcular los parámetros de stop loss y de riesgo. Además, dejaremos que el optimizador decida cuántas barras históricas deben utilizarse en el cálculo del stop loss. En nuestro primer intento, habíamos asumido que 10 barras H4 serían suficientes. Pero ahora, dejaremos que el optimizador ajuste esta configuración y comprobaremos si este cambio mejora el rendimiento. //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "Money Management Settings" input ENUM_TIMEFRAMES TF_3 = PERIOD_H4; //Risk Time Frame input int HISTORICAL_BARS = 10; //Historical bars for risk calculation
También debemos introducir cambios en el código. En la versión anterior, el método de actualización se encargaba del cálculo del relleno para cada posición. En esta nueva versión, una función independiente se encargará del relleno, ya que ahora queremos que el stop loss siga a las posiciones ganadoras.
//+------------------------------------------------------------------+ //| Get the stop loss size to use | //+------------------------------------------------------------------+ double get_padding(void) { vector high = vector::Zeros(10); vector low = vector::Zeros(10); low.CopyRates(Symbol(),TF_3,COPY_RATES_LOW,0,HISTORICAL_BARS); high.CopyRates(Symbol(),TF_3,COPY_RATES_HIGH,0,HISTORICAL_BARS); vector var = high - low; double padding = var.Mean(); return(padding); }
El método de actualización cambiará en consecuencia. El padding se calculará ahora utilizando el método get_padding. La parte del método de actualización que inicialmente identificaba las posiciones ahora llamará a una función responsable de encontrar nuestra configuración y a otro método para gestionar las posiciones abiertas.
//+------------------------------------------------------------------+ //| Update our technical indicators and positions | //+------------------------------------------------------------------+ void update(void) { //Update technical indicators and market readings CopyBuffer(ma_c_2_handle,0,0,1,ma_c_2); CopyBuffer(ma_c_1_handle,0,0,1,ma_c_1); CopyBuffer(ma_c_4_handle,0,0,1,ma_c_4); CopyBuffer(ma_c_3_handle,0,0,1,ma_c_3); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); double padding = get_padding(); //Find an open position if(PositionsTotal() == 0) find_setup(padding); //Manage our open positions else if(PositionsTotal() > 0) manage_setup(); } //+------------------------------------------------------------------+
El método que gestiona las posiciones siempre comprobará si el nuevo valor de stop loss sugerido es más rentable que el actual. En ese caso, actualiza el stop loss; de lo contrario, mantiene el valor actual.
//+------------------------------------------------------------------+ //| Manage our open positions | //+------------------------------------------------------------------+ void manage_setup(void) { //Does the position exist? if(PositionSelect(Symbol())) { //Get the current stop loss double current_sl = PositionGetDouble(POSITION_SL); double padding = get_padding(); double new_sl; //Sell position if((state == 0)) { new_sl = (ask + padding); if(new_sl < current_sl) Trade.PositionModify(Symbol(),new_sl,0); } //Buy position if((state == 1)) { new_sl = (bid - padding); if(new_sl > current_sl) Trade.PositionModify(Symbol(),new_sl,0); } if(closing_tf == 0) { if((state ==0) && (ma_c_1[0] > ma_c_2[0])) Trade.PositionClose(Symbol()); if((state ==1) && (ma_c_1[0] < ma_c_2[0])) Trade.PositionClose(Symbol()); } else if(closing_tf == 1) { if((state ==0) && (ma_c_3[0] > ma_c_4[0])) Trade.PositionClose(Symbol()); if((state ==1) && (ma_c_3[0] < ma_c_4[0])) Trade.PositionClose(Symbol()); } } }
Para llevar a cabo nuestras pruebas, seleccioné una configuración que había resultado rentable tanto en la prueba retrospectiva como en la prueba prospectiva, y luego mantuve fijos los demás ajustes mientras permitía que el optimizador genético buscara mejores configuraciones de parámetros de riesgo.

Figura 9: Intentando mejorar nuestros resultados iniciales. Aunque permitimos que el optimizador genético controlara la configuración de riesgo, nuestros nuevos resultados aún no fueron rentables en ambas pruebas.
Lamentablemente, volvimos a encontrarnos con el mismo problema. Y cuando estábamos optimizando la configuración de nuestros parámetros de riesgo, nuestra aplicación solo fue rentable en la prueba prospectiva y no logró serlo en la prueba retrospectiva.

Figura 10: Nuestros nuevos resultados aún no fueron rentables en ambas pruebas.
Conclusión
Hemos aprendido mucho de este ejercicio. Con nuestra estrategia de cruce de medias móviles dobles, hemos observado que podemos controlar la cantidad de retardo en la estrategia, aunque las consecuencias de esos cambios no siempre son inmediatamente claras. Quizás convenga reconsiderar la posibilidad de volver a ejecutar la optimización con todos los parámetros disponibles simultáneamente. Es posible que ajustar un parámetro manteniendo fijos los demás no sea el enfoque óptimo. Analizar todos los parámetros a la vez puede generar resultados más estables.
En nuestra conversación de seguimiento, después de realizar nuevamente un análisis de optimización completo, construiremos modelos estadísticos basados en las configuraciones más rentables. Esto podría ayudarnos a reducir aún más el retardo. Por ahora, sin embargo, hemos arrancado con fuerza. Recuerda que la optimización no nos ofrece garantías y que la IA no sustituye el trabajo arduo de los desarrolladores. Debemos repetir nuestro procedimiento de optimización hasta que nuestro optimizador genético pueda ofrecernos lotes de estrategias que sean rentables en ambas pruebas; de lo contrario, es probable que la optimización sea prematura.
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/18793
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.
Herramientas de trading de MQL5 (Parte 5): Creación de una cinta de cotizaciones deslizante para el seguimiento en tiempo real de símbolos
Características del Wizard MQL5 que debe conocer (Parte 75): Uso del Awesome Oscillator y Envelopes
Particularidades del trabajo con números del tipo double en MQL4
Algoritmo de optimización basado en los sueños — Dream Optimization Algorithm (DOA)
- 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