Automatización de estrategias de trading en MQL5 (Parte 22): Desarrollo de un sistema de recuperación por zonas para trading de tendencia con Envelopes
Introducción
En nuestro artículo anterior (Parte 21), analizamos una estrategia de trading basada en una red neuronal y mejorada con tasas de aprendizaje adaptativas para aumentar la precisión en la predicción de los movimientos del mercado en MetaQuotes Language 5 (MQL5). En la parte 22, cambiamos el foco hacia la creación de un sistema de recuperación por zonas integrado con una estrategia de trading de tendencias basada en las «envolventes», que combina el indicador RSI y el indicador Envelopes para automatizar las operaciones y gestionar las pérdidas de forma eficaz. Abordaremos los siguientes temas:
- Comprender la arquitectura de la estrategia de tendencia con Envelopes y recuperación por zonas
- Implementación en MQL5
- Backtesting
- Conclusión
Al final, tendrás un sistema de trading MQL5 sólido, diseñado para condiciones de mercado dinámicas y listo para su implementación y pruebas. ¡Empecemos!
Comprender la arquitectura de la estrategia de tendencia con Envelopes y recuperación por zonas
La recuperación de zona es una estrategia de trading inteligente que nos ayuda a convertir las pérdidas potenciales en ganancias mediante la realización de operaciones adicionales cuando el mercado se mueve en nuestra contra, con el objetivo de obtener ganancias o al menos alcanzar el punto de equilibrio. Imagina que compras un par de divisas esperando que suba, pero baja: la recuperación por zonas entra en acción estableciendo un rango de precios, o «zona», en el que realizamos operaciones contrarias para recuperar las pérdidas si el precio repunta. Tenemos previsto desarrollar un sistema automatizado en MetaQuotes Language 5 (MQL5) que aproveche este concepto para operar en los mercados de divisas, manteniendo al mismo tiempo un bajo nivel de riesgo y maximizando los beneficios.
Para que esto funcione, utilizaremos dos indicadores técnicos con el fin de identificar los momentos óptimos para abrir posiciones. Un indicador medirá el impulso del mercado, garantizando que solo operemos cuando haya un impulso fuerte en una dirección, evitando así señales débiles o confusas. El otro, llamado Envelopes, traza un canal en torno al precio medio del mercado, lo que nos permite ver cuándo los precios se alejan demasiado al alza o a la baja, lo que indica un probable punto de rebote en el que entrar en el mercado. Estos indicadores se complementarán entre sí para identificar operaciones con altas probabilidades de éxito en las que el precio esté a punto de girarse dentro de la tendencia.
Así es como pensamos llevarlo a cabo: empezaremos por abrir una operación cuando nuestros indicadores señalen un giro, como cuando el precio alcance el límite del canal de las «Envelopes» con un fuerte impulso. Si el mercado evoluciona en la dirección equivocada, activaremos la estrategia de recuperación de zona abriendo operaciones contrarias dentro de nuestra zona de precios establecida, con un tamaño cuidadosamente calculado para equilibrar el riesgo y la recuperación. Limitaremos el número de operaciones para evitar dejarnos llevar por el entusiasmo y garantizar así que el sistema se mantenga disciplinado. Esta estrategia nos permitirá aprovechar oportunidades dentro de la tendencia y, al mismo tiempo, contar con un colchón de seguridad para cuando las cosas no salgan según lo previsto, y se adapta tanto a mercados volátiles como tranquilos. ¡Acompáñanos mientras hacemos realidad este plan y lo ponemos a prueba! Consulta el plan de ejecución que figura a continuación.

Implementación en MQL5
Para crear el programa en MQL5, abre el MetaEditor, ve al Navegador, busca la carpeta «Indicators», haz clic en «Nuevo» y sigue las instrucciones para crear el archivo. Una vez creado, en el entorno de programación, empezaremos por declarar algunas variables de entrada que nos ayudarán a controlar fácilmente los valores clave del programa.
//+------------------------------------------------------------------+ //| Envelopes Trend Bounce with Zone Recovery EA.mq5 | //| Copyright 2025, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property strict #include <Trade/Trade.mqh> //--- Include trade library enum TradingLotSizeOptions { FIXED_LOTSIZE, UNFIXED_LOTSIZE }; //--- Define lot size options input group "======= EA GENERAL SETTINGS =======" input TradingLotSizeOptions lotOption = UNFIXED_LOTSIZE; // Lot Size Option input double initialLotSize = 0.01; // Initial Lot Size input double riskPercentage = 1.0; // Risk Percentage (%) input int riskPoints = 300; // Risk Points input int magicNumber = 123456789; // Magic Number input int maxOrders = 1; // Maximum Initial Positions input double zoneTargetPoints = 600; // Zone Target Points input double zoneSizePoints = 300; // Zone Size Points input bool restrictMaxOrders = true; // Apply Maximum Orders Restriction
En esta sección, sentamos las bases de nuestro «Sistema de recuperación por zonas para el trading de tendencias con envolventes» en MQL5, configurando los componentes esenciales y los ajustes personalizables por el usuario. Empezamos incluyendo la biblioteca «<Trade/Trade.mqh>», que proporciona la clase «CTrade» para ejecutar operaciones de trading, como abrir y cerrar posiciones. Esta incorporación es fundamental, ya que dota a nuestro asesor experto (EA) de las herramientas necesarias para interactuar con el mercado sin problemas, especialmente a la hora de abrir órdenes. A continuación te explicamos cómo abrir el archivo.

A continuación, definimos la enumeración «TradingLotSizeOptions» con dos valores: «FIXED_LOTSIZE» y «UNFIXED_LOTSIZE». Esto nos permite ofrecer a los usuarios la posibilidad de elegir entre un tamaño de lote constante o uno que se ajuste dinámicamente en función de los parámetros de riesgo, lo que proporciona flexibilidad a la hora de determinar el tamaño de las operaciones para adaptarse a los distintos estilos de negociación. A continuación, configuramos los parámetros de entrada en el grupo «EA GENERAL SETTINGS», que los usuarios pueden ajustar en la plataforma MetaTrader 5.
El parámetro «lotOption», cuyo valor por defecto es «UNFIXED_LOTSIZE», determina si las operaciones utilizan un tamaño de lote fijo o basado en el riesgo. El parámetro «initialLotSize» (0,01) establece el tamaño del lote para las operaciones fijas, mientras que «riskPercentage» (1,0 %) y «riskPoints» (300) definen el porcentaje del saldo de la cuenta y la distancia del stop-loss para el cálculo dinámico del tamaño del lote. Estos ajustes controlan el nivel de riesgo que asumimos en cada operación, lo que garantiza que el EA se adapte a la tolerancia al riesgo del usuario.
Asignamos un «magicNumber» único (123456789) para identificar las operaciones de nuestro EA, lo que nos permite distinguirlas de otras operaciones de la misma cuenta. Los parámetros «maxOrders» (1) y «restrictMaxOrders» (true) limitan el número de posiciones iniciales, lo que evita que el EA abra demasiadas operaciones a la vez. Por último, «zoneTargetPoints» (600) y «zoneSizePoints» (300) establecen el objetivo de beneficios y el tamaño de la zona de recuperación en puntos, definiendo así los límites de nuestra estrategia de recuperación por zonas. Tras la compilación, obtenemos el siguiente resultado.

Una vez cargados los datos de entrada, ya podemos empezar a definir la lógica principal de todo el sistema. Empezaremos declarando algunas estructuras y clases que vamos a utilizar, ya que queremos aplicar un enfoque de programación orientada a objetos (POO).
class MarketZoneTrader { private: //--- Trade State Definition enum TradeState { INACTIVE, RUNNING, TERMINATING }; //--- Define trade lifecycle states //--- Data Structures struct TradeMetrics { bool operationSuccess; //--- Track operation success double totalVolume; //--- Sum closed trade volumes double netProfitLoss; //--- Accumulate profit/loss }; struct ZoneBoundaries { double zoneHigh; //--- Upper recovery zone boundary double zoneLow; //--- Lower recovery zone boundary double zoneTargetHigh; //--- Upper profit target double zoneTargetLow; //--- Lower profit target }; struct TradeConfig { string marketSymbol; //--- Trading symbol double openPrice; //--- Position entry price double initialVolume; //--- Initial trade volume long tradeIdentifier; //--- Magic number string tradeLabel; //--- Trade comment ulong activeTickets[]; //--- Active position tickets ENUM_ORDER_TYPE direction; //--- Trade direction double zoneProfitSpan; //--- Profit target range double zoneRecoverySpan; //--- Recovery zone range double accumulatedBuyVolume; //--- Total buy volume double accumulatedSellVolume; //--- Total sell volume TradeState currentState; //--- Current trade state }; struct LossTracker { double tradeLossTracker; //--- Track cumulative profit/loss }; };
En este apartado definimos la estructura básica de nuestro sistema para el trading de tendencias con envolventes en MQL5 mediante la implementación de la clase «MarketZoneTrader», centrándonos en su sección privada, que contiene las definiciones del estado de las operaciones y las estructuras de datos. Esta lógica ayudará a organizar los componentes fundamentales necesarios para gestionar las operaciones, realizar un seguimiento de las zonas de recuperación y supervisar el rendimiento. Comenzamos definiendo la clase «MarketZoneTrader», que constituye la columna vertebral de nuestro asesor experto (EA) y encapsula la lógica de nuestra estrategia de trading.
En su sección privada, introducimos la enumeración «TradeState», que cuenta con tres estados: «INACTIVE», «RUNNING» y «TERMINATING». Estos estados nos permiten realizar un seguimiento del ciclo de vida de nuestras operaciones financieras, lo que nos garantiza saber si el EA está inactivo, gestionando operaciones de forma activa o cerrando posiciones. Esto es fundamental para mantener el control sobre el proceso de negociación, ya que nos ayuda a coordinar acciones como abrir operaciones de recuperación o cerrar posiciones.
A continuación, creamos la estructura «TradeMetrics» para almacenar los datos clave de rendimiento de nuestras operaciones. Incluye «operationSuccess» para comprobar si las operaciones (como el cierre de posiciones) se han realizado con éxito, «totalVolume» para sumar los volúmenes de las operaciones cerradas y «netProfitLoss» para acumular las ganancias o pérdidas derivadas de dichas operaciones. Esta estructura nos ayuda a evaluar el resultado de nuestras operaciones de trading, ofreciéndonos una visión clara del rendimiento tanto en la fase de recuperación como en la de cierre.
A continuación, definimos la estructura «ZoneBoundaries», que contiene los niveles de precio para nuestra estrategia de recuperación por zonas. Las variables «zoneHigh» y «zoneLow» marcan los límites superior e inferior de la zona de recuperación, donde realizamos operaciones en sentido contrario para mitigar las pérdidas. Los parámetros «zoneTargetHigh» y «zoneTargetLow» establecen los objetivos de beneficio por encima y por debajo de la zona, definiendo cuándo salimos de las operaciones con beneficios. Estos límites son fundamentales para nuestra estrategia, ya que nos indican cuándo debemos activar medidas de recuperación o cerrar posiciones. Así es como se verían en la visualización, para ofrecer una visión clara de por qué necesitamos esta estructura.

A continuación, la estructura «TradeConfig» es donde almacenamos la configuración de la operación. Incluye «marketSymbol» para el par de divisas, «openPrice» para el precio de entrada y «initialVolume» para el volumen de la operación. El campo «tradeIdentifier» contiene nuestro número mágico único, y «tradeLabel» añade un comentario para identificar la operación. La matriz «activeTickets» registra las posiciones abiertas, mientras que «direction» especifica si la operación es de compra o de venta. También incluimos «zoneProfitSpan» y «zoneRecoverySpan» para definir el objetivo de beneficio y el tamaño de la zona de recuperación en unidades de precio, así como «accumulatedBuyVolume» y «accumulatedSellVolume» para supervisar los volúmenes totales de cada tipo de operación. La variable «currentState», que utiliza la enumeración «TradeState», realiza un seguimiento del estado de la operación y sirve de nexo de unión entre todos los elementos.
Por último, añadimos la estructura «LossTracker» con una única variable «tradeLossTracker» para realizar un seguimiento de las ganancias o pérdidas acumuladas en todas las operaciones. Esto nos ayuda a evaluar el impacto financiero de nuestras medidas de recuperación, lo que nos permite ajustar nuestra estrategia si las pérdidas se vuelven demasiado elevadas. A continuación, podemos definir algunas variables de miembro que nos ayuden a almacenar el resto de la información de trading, menos crítica pero necesaria.
//--- Member Variables TradeConfig m_tradeConfig; //--- Store trade configuration ZoneBoundaries m_zoneBounds; //--- Store zone boundaries LossTracker m_lossTracker; //--- Track profit/loss string m_lastError; //--- Store error message int m_errorStatus; //--- Store error code CTrade m_tradeExecutor; //--- Manage trade execution int m_handleRsi; //--- RSI indicator handle int m_handleEnvUpper; //--- Upper Envelopes handle int m_handleEnvLower; //--- Lower Envelopes handle double m_rsiBuffer[]; //--- RSI data buffer double m_envUpperBandBuffer[]; //--- Upper Envelopes buffer double m_envLowerBandBuffer[]; //--- Lower Envelopes buffer TradingLotSizeOptions m_lotOption; //--- Lot size option double m_initialLotSize; //--- Fixed lot size double m_riskPercentage; //--- Risk percentage int m_riskPoints; //--- Risk points int m_maxOrders; //--- Maximum positions bool m_restrictMaxOrders; //--- Position restriction flag double m_zoneTargetPoints; //--- Profit target points double m_zoneSizePoints; //--- Recovery zone points
Definimos las variables clave de los miembros en la sección private de la clase «MarketZoneTrader» para gestionar los parámetros de las operaciones, las zonas de recuperación y los datos de los indicadores. Utilizamos «m_tradeConfig» (estructura «TradeConfig») para almacenar los detalles de la operación, como el símbolo y la dirección; «m_zoneBounds» (estructura «ZoneBoundaries») para los precios de la zona de recuperación y del objetivo de beneficios; y «m_lossTracker» (estructura «LossTracker») para realizar un seguimiento de las ganancias o pérdidas. En cuanto a la gestión de errores, «m_lastError» (cadena de caracteres) y «m_errorStatus» (entero) registran los problemas, mientras que «m_tradeExecutor» (clase «CTrade») se encarga de las operaciones financieras.
Los identificadores de indicadores —«m_handleRsi», «m_handleEnvUpper» y «m_handleEnvLower»— permiten acceder a los datos del RSI y de las envolventes, mientras que las matrices «m_rsiBuffer», «m_envUpperBandBuffer» y «m_envLowerBandBuffer» almacenan sus valores. Almacenamos los parámetros de configuración en «m_lotOption» («TradingLotSizeOptions»), «m_initialLotSize», «m_riskPercentage», «m_riskPoints», «m_maxOrders», «m_restrictMaxOrders», «m_zoneTargetPoints» y «m_zoneSizePoints» para controlar el tamaño de los lotes, los límites de las posiciones y el tamaño de las zonas. Estas variables constituyen la base para gestionar las operaciones y los indicadores, y nos preparan para la lógica de negociación que nos espera. A continuación, tenemos que definir algunas funciones auxiliares que utilizaremos con frecuencia en el programa.
//--- Error Handling void logError(string message, int code) { //--- Error Logging Start m_lastError = message; //--- Store error message m_errorStatus = code; //--- Store error code Print("Error: ", message); //--- Log error to Experts tab //--- Error Logging End } //--- Market Data Access double getMarketVolumeStep() { //--- Volume Step Retrieval Start return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_VOLUME_STEP); //--- Retrieve broker's volume step //--- Volume Step Retrieval End } double getMarketAsk() { //--- Ask Price Retrieval Start return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_ASK); //--- Retrieve ask price //--- Ask Price Retrieval End } double getMarketBid() { //--- Bid Price Retrieval Start return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_BID); //--- Retrieve bid price //--- Bid Price Retrieval End }
Aquí añadimos funciones de utilidad fundamentales para la gestión de errores y el acceso a los datos de mercado. La función «logError» almacena «message» en «m_lastError», «code» en «m_errorStatus» y registra el mensaje mediante Print en la pestaña «Expertos» para la depuración. La función «getMarketVolumeStep» utiliza SymbolInfoDouble con SYMBOL_VOLUME_STEP para obtener el incremento de volumen del bróker para «m_tradeConfig.marketSymbol», lo que garantiza que los tamaños de las operaciones sean válidos. Las funciones «getMarketAsk» y «getMarketBid» recuperan los precios ask y bid utilizando «SymbolInfoDouble» con SYMBOL_ASK y «SYMBOL_BID», respectivamente, para obtener una cotización precisa de la operación.
Ahora podemos definir las funciones principales para llevar a cabo operaciones comerciales. Empecemos por aquellas que nos ayudarán a inicializar el sistema, a almacenar los tickets de operaciones para el seguimiento y la supervisión de las mismas, y a cerrar las operaciones, ya que se trata de la lógica menos compleja.
//--- Trade Initialization bool configureTrade(ulong ticket) { //--- Trade Configuration Start if (!PositionSelectByTicket(ticket)) { //--- Select position by ticket logError("Failed to select ticket " + IntegerToString(ticket), INIT_FAILED); //--- Log selection failure return false; //--- Return failure } m_tradeConfig.marketSymbol = PositionGetString(POSITION_SYMBOL); //--- Set symbol m_tradeConfig.tradeLabel = __FILE__; //--- Set trade comment m_tradeConfig.tradeIdentifier = PositionGetInteger(POSITION_MAGIC); //--- Set magic number m_tradeConfig.direction = (ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE); //--- Set direction m_tradeConfig.openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Set entry price m_tradeConfig.initialVolume = PositionGetDouble(POSITION_VOLUME); //--- Set initial volume m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); //--- Set magic number for executor return true; //--- Return success //--- Trade Configuration End } //--- Trade Ticket Management void storeTradeTicket(ulong ticket) { //--- Ticket Storage Start int ticketCount = ArraySize(m_tradeConfig.activeTickets); //--- Get ticket count ArrayResize(m_tradeConfig.activeTickets, ticketCount + 1); //--- Resize ticket array m_tradeConfig.activeTickets[ticketCount] = ticket; //--- Store ticket //--- Ticket Storage End } //--- Trade Execution ulong openMarketTrade(ENUM_ORDER_TYPE tradeDirection, double tradeVolume, double price) { //--- Trade Opening Start ulong ticket = 0; //--- Initialize ticket if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, tradeDirection, tradeVolume, price, 0, 0, m_tradeConfig.tradeLabel)) { //--- Open position ticket = m_tradeExecutor.ResultOrder(); //--- Get ticket } else { Print("Failed to open trade: Direction=", EnumToString(tradeDirection), ", Volume=", tradeVolume); //--- Log failure } return ticket; //--- Return ticket //--- Trade Opening End } //--- Trade Closure void closeActiveTrades(TradeMetrics &metrics) { //--- Trade Closure Start for (int i = ArraySize(m_tradeConfig.activeTickets) - 1; i >= 0; i--) { //--- Iterate tickets in reverse if (m_tradeConfig.activeTickets[i] > 0) { //--- Check valid ticket if (m_tradeExecutor.PositionClose(m_tradeConfig.activeTickets[i])) { //--- Close position m_tradeConfig.activeTickets[i] = 0; //--- Clear ticket metrics.totalVolume += m_tradeExecutor.ResultVolume(); //--- Accumulate volume if ((ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE) == ORDER_TYPE_BUY) { //--- Check buy position metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (m_tradeExecutor.ResultPrice() - PositionGetDouble(POSITION_PRICE_OPEN)); //--- Calculate buy profit } else { //--- Handle sell position metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (PositionGetDouble(POSITION_PRICE_OPEN) - m_tradeExecutor.ResultPrice()); //--- Calculate sell profit } } else { metrics.operationSuccess = false; //--- Mark failure Print("Failed to close ticket: ", m_tradeConfig.activeTickets[i]); //--- Log failure } } } //--- Trade Closure End } //--- Bar Detection bool isNewBar() { //--- New Bar Detection Start static datetime previousTime = 0; //--- Store previous bar time datetime currentTime = iTime(m_tradeConfig.marketSymbol, Period(), 0); //--- Get current bar time bool result = (currentTime != previousTime); //--- Check for new bar previousTime = currentTime; //--- Update previous time return result; //--- Return new bar status //--- New Bar Detection End }
Aquí nos adentramos en la lógica central de nuestro programa, creando funciones para configurar operaciones, realizar un seguimiento de las posiciones, ejecutar órdenes, cerrar operaciones y sincronizar nuestras acciones. Empezamos creando la función «configureTrade» para preparar una operación para un «ticket» determinado. En primer lugar, intentamos seleccionar la posición con la función PositionSelectByTicket. Si no funciona, registramos el problema mediante «logError» y salimos con el valor «false». Cuando se realiza con éxito, rellenamos «m_tradeConfig» con los datos: obtenemos «marketSymbol» mediante la función PositionGetString, establecemos «tradeLabel» en __FILE__ y extraemos «tradeIdentifier» y «direction» de PositionGetInteger, convirtiendo este último a ENUM_ORDER_TYPE. A continuación, configuramos «openPrice» e «initialVolume» con PositionGetDouble y marcamos «m_tradeExecutor» con «SetExpertMagicNumber», asegurándonos de que nuestra operación esté lista para ejecutarse.
A continuación, creamos la función «storeTradeTicket» para mantener organizadas nuestras posiciones abiertas. Comprobamos el tamaño de «m_tradeConfig.activeTickets» con la función ArraySize, ampliamos la matriz en una posición mediante la función ArrayResize e insertamos el nuevo «ticket» en su lugar, de modo que siempre sepamos qué operaciones están activas. A continuación, creamos la función «openMarketTrade» para realizar operaciones en el mercado. Llamamos a «m_tradeExecutor.PositionOpen» con los datos de «tradeDirection», «tradeVolume», «price» y «m_tradeConfig». Si se lleva a cabo, asignamos el «ticket» con «ResultOrder»; si no, registramos el error con «Print», lo que nos permite mantener un control estricto sobre la ejecución de la operación.
A continuación, nos ocupamos de cerrar las posiciones con la función «closeActiveTrades». Recorremos hacia atrás la lista «m_tradeConfig.activeTickets», cerrando cada ticket válido con «m_tradeExecutor.PositionClose». Cuando el cierre se realiza correctamente, borramos el ticket, sumamos «ResultVolume» a «metrics.totalVolume» y calculamos «metrics.netProfitLoss» utilizando las funciones «PositionGetInteger» y «PositionGetDouble» para comprobar la dirección de la operación. Si algo falla, marcamos «metrics.operationSuccess» como falso y lo registramos con Print, asegurándonos así de hacer un seguimiento de cada resultado.
Por último, añadimos la función «isNewBar» para opera solo una vez por barra, lo que reduce el consumo de recursos. Obtenemos la hora de la barra actual para «m_tradeConfig.marketSymbol» mediante la función iTime, la comparamos con «previousTime» y actualizamos «previousTime» si es diferente, lo que nos permite saber cuándo llega una nueva barra para comprobar si hay señales de operación. Por último, necesitaremos una función para calcular el volumen de operaciones y otra para abrir las operaciones.
//--- Lot Size Calculation double calculateLotSize(double riskPercent, int riskPips) { //--- Lot Size Calculation Start double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * riskPercent / 100; //--- Calculate risk amount double tickSize = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_SIZE); //--- Get tick size double tickValue = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_VALUE); //--- Get tick value if (tickSize == 0 || tickValue == 0) { //--- Validate tick data Print("Invalid tick size or value"); //--- Log invalid data return -1; //--- Return invalid lot } double lotValue = (riskPips * _Point) / tickSize * tickValue; //--- Calculate lot value if (lotValue == 0) { //--- Validate lot value Print("Invalid lot value"); //--- Log invalid lot return -1; //--- Return invalid lot } return NormalizeDouble(riskMoney / lotValue, 2); //--- Return normalized lot size //--- Lot Size Calculation End } //--- Order Execution int openOrder(ENUM_ORDER_TYPE orderType, double stopLoss, double takeProfit) { //--- Order Opening Start int ticket; //--- Initialize ticket double openPrice; //--- Initialize open price if (orderType == ORDER_TYPE_BUY) { //--- Check buy order openPrice = NormalizeDouble(getMarketAsk(), Digits()); //--- Set buy price } else if (orderType == ORDER_TYPE_SELL) { //--- Check sell order openPrice = NormalizeDouble(getMarketBid(), Digits()); //--- Set sell price } else { Print("Invalid order type"); //--- Log invalid type return -1; //--- Return invalid ticket } double lotSize = 0; //--- Initialize lot size if (m_lotOption == FIXED_LOTSIZE) { //--- Check fixed lot lotSize = m_initialLotSize; //--- Use fixed lot size } else if (m_lotOption == UNFIXED_LOTSIZE) { //--- Check dynamic lot lotSize = calculateLotSize(m_riskPercentage, m_riskPoints); //--- Calculate risk-based lot } if (lotSize <= 0) { //--- Validate lot size Print("Invalid lot size: ", lotSize); //--- Log invalid lot return -1; //--- Return invalid ticket } if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, orderType, lotSize, openPrice, 0, 0, __FILE__)) { //--- Open position ticket = (int)m_tradeExecutor.ResultOrder(); //--- Get ticket Print("New trade opened: Ticket=", ticket, ", Type=", EnumToString(orderType), ", Volume=", lotSize); //--- Log success } else { ticket = -1; //--- Set invalid ticket Print("Failed to open order: Type=", EnumToString(orderType), ", Volume=", lotSize); //--- Log failure } return ticket; //--- Return ticket //--- Order Opening End }
Empezamos con la función «calculateLotSize» para determinar el tamaño de la operación en función de los parámetros de riesgo. En primer lugar, calculamos el «riskMoney» aplicando un porcentaje al saldo de la cuenta mediante AccountInfoDouble, utilizando ACCOUNT_BALANCE y «riskPercent». A continuación, obtenemos «tickSize» y «tickValue» para «m_tradeConfig.marketSymbol» utilizando SymbolInfoDouble con «SYMBOL_TRADE_TICK_SIZE» y «SYMBOL_TRADE_TICK_VALUE». Si alguno de los dos es cero, registramos un error con «Print» y devolvemos -1 para evitar cálculos erróneos. Calculamos el «lotValue» utilizando «riskPips», _Point, «tickSize» y «tickValue», y si el resultado es cero, registramos otro error y devolvemos -1. Por último, redondeamos el tamaño del lote con NormalizeDouble a dos decimales, asegurándonos de que cumpla con los requisitos del bróker.
A continuación, creamos la función «openOrder» para realizar operaciones. Inicializamos «ticket» y «openPrice» y, a continuación, comprobamos «orderType». Para ORDER_TYPE_BUY, establecemos «openPrice» utilizando «getMarketAsk» y «NormalizeDouble» con Digits; para «ORDER_TYPE_SELL», utilizamos «getMarketBid». Si «orderType» no es válido, lo registramos con «Print» y devolvemos -1. Determinamos «lotSize» en función de «m_lotOption»: para «FIXED_LOTSIZE», utilizamos «m_initialLotSize»; para «UNFIXED_LOTSIZE», llamamos a «calculateLotSize» con «m_riskPercentage» y «m_riskPoints». Si «lotSize» no es válido, registramos el error con «Print» y devolvemos -1. A continuación, abrimos la posición con «m_tradeExecutor.PositionOpen» utilizando «m_tradeConfig.marketSymbol», «orderType», «lotSize», «openPrice» y «FILE» como comentario. Si se realiza correctamente, asignamos a «ticket» el valor de «ResultOrder» y lo registramos con «Print»; si falla, asignamos a «ticket» el valor -1 y registramos el error. Por último, devolvemos el valor del ticket.
Una vez hecho esto, tenemos que inicializar los valores del sistema. Podemos conseguirlo mediante una función específica, pero para que todo sea más sencillo, utilizaremos el constructor. Es recomendable definir el constructor con un modificador de acceso «public» para que esté disponible en cualquier parte del programa. Definamos también aquí el destructor.
public: //--- Constructor MarketZoneTrader(TradingLotSizeOptions lotOpt, double initLot, double riskPct, int riskPts, int maxOrds, bool restrictOrds, double targetPts, double sizePts) { //--- Constructor Start m_tradeConfig.currentState = INACTIVE; //--- Set initial state ArrayResize(m_tradeConfig.activeTickets, 0); //--- Initialize ticket array m_tradeConfig.zoneProfitSpan = targetPts * _Point; //--- Set profit target m_tradeConfig.zoneRecoverySpan = sizePts * _Point; //--- Set recovery zone m_lossTracker.tradeLossTracker = 0.0; //--- Initialize loss tracker m_lotOption = lotOpt; //--- Set lot size option m_initialLotSize = initLot; //--- Set initial lot m_riskPercentage = riskPct; //--- Set risk percentage m_riskPoints = riskPts; //--- Set risk points m_maxOrders = maxOrds; //--- Set max positions m_restrictMaxOrders = restrictOrds; //--- Set restriction flag m_zoneTargetPoints = targetPts; //--- Set target points m_zoneSizePoints = sizePts; //--- Set zone points m_tradeConfig.marketSymbol = _Symbol; //--- Set symbol m_tradeConfig.tradeIdentifier = magicNumber; //--- Set magic number //--- Constructor End } //--- Destructor ~MarketZoneTrader() { //--- Destructor Start cleanup(); //--- Release resources //--- Destructor End }
Continuamos definiendo el constructor y el destructor de la clase «MarketZoneTrader» en su sección pública. Empezamos con el constructor «MarketZoneTrader», que toma como parámetros «lotOpt», «initLot», «riskPct», «riskPts», «maxOrds», «restrictOrds», «targetPts» y «sizePts». Inicializamos el entorno de negociación estableciendo «m_tradeConfig.currentState» en «INACTIVE» para indicar que no hay operaciones activas. A continuación, vaciamos el array «m_tradeConfig.activeTickets» utilizando ArrayResize para ponerlo a cero, preparándolo así para nuevos tickets. Calculamos «m_tradeConfig.zoneProfitSpan» y «m_tradeConfig.zoneRecoverySpan» multiplicando «targetPts» y «sizePts» por «_Point», estableciendo así los tamaños del objetivo de beneficio y de la zona de recuperación en unidades de precio. Restablecemos «m_lossTracker.tradeLossTracker» a 0,0 para empezar a llevar un seguimiento de las ganancias o pérdidas desde cero.
A continuación, asignamos los parámetros de entrada a las variables de miembro: «m_lotOption» a «lotOpt», «m_initialLotSize» a «initLot», «m_riskPercentage» a «riskPct», «m_riskPoints» a «riskPts», «m_maxOrders» a «maxOrds», «m_restrictMaxOrders» a «restrictOrds», «m_zoneTargetPoints» a «targetPts» y «m_zoneSizePoints» a «sizePts». Establecemos «m_tradeConfig.marketSymbol» en _Symbol para operar con el símbolo del gráfico actual y asignamos «m_tradeConfig.tradeIdentifier» a «magicNumber» para identificar la operación de forma única. Esta configuración garantiza que nuestro EA refleje los ajustes del usuario y esté listo para operar.
A continuación, definimos el destructor «~MarketZoneTrader» para liberar recursos. Llamamos a la función «cleanup» para liberar cualquier recurso asignado, como los identificadores de indicadores, lo que garantiza que el EA se cierre correctamente sin fugas de memoria. Cabe señalar que el constructor y el destructor tienen el mismo nombre de clase, con la única diferencia de que el destructor lleva una tilde (~) delante. Solo eso. Aquí tienes la función para eliminar la clase cuando ya no sea necesaria.
//--- Cleanup void cleanup() { //--- Cleanup Start IndicatorRelease(m_handleRsi); //--- Release RSI handle ArrayFree(m_rsiBuffer); //--- Free RSI buffer IndicatorRelease(m_handleEnvUpper); //--- Release upper Envelopes handle ArrayFree(m_envUpperBandBuffer); //--- Free upper Envelopes buffer IndicatorRelease(m_handleEnvLower); //--- Release lower Envelopes handle ArrayFree(m_envLowerBandBuffer); //--- Free lower Envelopes buffer //--- Cleanup End }
Simplemente utilizamos la función IndicatorRelease para liberar los identificadores de los indicadores y la función ArrayFree para liberar las matrices de almacenamiento. Ya que hemos hablado de los indicadores, definamos una función de inicialización que llamaremos al iniciar el programa.
//--- Getters TradeState getCurrentState() { //--- Get Current State Start return m_tradeConfig.currentState; //--- Return trade state //--- Get Current State End } double getZoneTargetHigh() { //--- Get Target High Start return m_zoneBounds.zoneTargetHigh; //--- Return profit target high //--- Get Target High End } double getZoneTargetLow() { //--- Get Target Low Start return m_zoneBounds.zoneTargetLow; //--- Return profit target low //--- Get Target Low End } double getZoneHigh() { //--- Get Zone High Start return m_zoneBounds.zoneHigh; //--- Return recovery zone high //--- Get Zone High End } double getZoneLow() { //--- Get Zone Low Start return m_zoneBounds.zoneLow; //--- Return recovery zone low //--- Get Zone Low End } //--- Initialization int initialize() { //--- Initialization Start m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); //--- Set magic number int totalPositions = PositionsTotal(); //--- Get total positions for (int i = 0; i < totalPositions; i++) { //--- Iterate positions ulong ticket = PositionGetTicket(i); //--- Get ticket if (PositionSelectByTicket(ticket)) { //--- Select position if (PositionGetString(POSITION_SYMBOL) == m_tradeConfig.marketSymbol && PositionGetInteger(POSITION_MAGIC) == m_tradeConfig.tradeIdentifier) { //--- Check symbol and magic if (activateTrade(ticket)) { //--- Activate position Print("Existing position activated: Ticket=", ticket); //--- Log activation } else { Print("Failed to activate existing position: Ticket=", ticket); //--- Log failure } } } } m_handleRsi = iRSI(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 8, PRICE_CLOSE); //--- Initialize RSI if (m_handleRsi == INVALID_HANDLE) { //--- Check RSI Print("Failed to initialize RSI indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } m_handleEnvUpper = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 150, 0, MODE_SMA, PRICE_CLOSE, 0.1); //--- Initialize upper Envelopes if (m_handleEnvUpper == INVALID_HANDLE) { //--- Check upper Envelopes Print("Failed to initialize upper Envelopes indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } m_handleEnvLower = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 95, 0, MODE_SMA, PRICE_CLOSE, 1.4); //--- Initialize lower Envelopes if (m_handleEnvLower == INVALID_HANDLE) { //--- Check lower Envelopes Print("Failed to initialize lower Envelopes indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } ArraySetAsSeries(m_rsiBuffer, true); //--- Set RSI buffer ArraySetAsSeries(m_envUpperBandBuffer, true); //--- Set upper Envelopes buffer ArraySetAsSeries(m_envLowerBandBuffer, true); //--- Set lower Envelopes buffer Print("EA initialized successfully"); //--- Log success return INIT_SUCCEEDED; //--- Return success //--- Initialization End }
En este punto, empezamos creando unas funciones getter sencillas para acceder a datos clave sobre operaciones de trading. La función «getCurrentState» devuelve «m_tradeConfig.currentState», lo que nos permite comprobar si el sistema se encuentra en estado «INACTIVE», «RUNNING» o «TERMINATING». A continuación, creamos las funciones «getZoneTargetHigh» y «getZoneTargetLow» para recuperar los valores «m_zoneBounds.zoneTargetHigh» y «m_zoneBounds.zoneTargetLow», que nos proporcionan los precios objetivo de beneficio para nuestras operaciones. A continuación, añadimos «getZoneHigh» y «getZoneLow» para obtener «m_zoneBounds.zoneHigh» y «m_zoneBounds.zoneLow», lo que nos proporciona los límites de la zona de recuperación.
A continuación, creamos la función «initialize» para configurar nuestro Asesor Experto (EA). Empezamos asignando «m_tradeConfig.tradeIdentifier» a «m_tradeExecutor» mediante «SetExpertMagicNumber» para etiquetar nuestras operaciones. A continuación, comprobamos si hay posiciones existentes con «PositionsTotal» y las recorremos una a una, obteniendo cada «ticket» con «PositionGetTicket». Si PositionSelectByTicket se ejecuta correctamente y la posición coincide con «m_tradeConfig.marketSymbol» y «m_tradeConfig.tradeIdentifier» (a través de PositionGetString y «PositionGetInteger»), llamamos a «activateTrade» para gestionarla, registrando el éxito o el fallo con «Print».
A continuación, configuramos nuestros indicadores. Creamos el identificador del RSI con la función iRSI para «m_tradeConfig.marketSymbol», utilizando un parámetro de 8 períodos en el marco temporal actual y «PRICE_CLOSE». Si «m_handleRsi» es INVALID_HANDLE, registramos el error con «Print» y devolvemos «INIT_FAILED». A continuación, inicializamos los indicadores de envolventes: «m_handleEnvUpper» con la función «iEnvelopes», utilizando una media móvil simple de 150 períodos, una desviación de 0,1 y «PRICE_CLOSE», y «m_handleEnvLower» con una media móvil simple de 95 períodos y una desviación de 1,4. Si alguno de los identificadores es «INVALID_HANDLE», registramos el error y devolvemos «INIT_FAILED». Por último, configuramos «m_rsiBuffer», «m_envUpperBandBuffer» y «m_envLowerBandBuffer» como matrices de series temporales mediante ArraySetAsSeries, registramos el éxito con «Print» y devolvemos INIT_SUCCEEDED. Ahora podemos llamar a esta función en el controlador de eventos OnInit, pero primero necesitaremos una instancia de la clase.
//--- Global Instance MarketZoneTrader *trader = NULL; //--- Declare trader instance
Aquí configuramos la instancia global de nuestro sistema declarando un puntero a la clase «MarketZoneTrader». Creamos la variable «trader» como un puntero a «MarketZoneTrader» y la inicializamos con el valor «NULL». Este paso garantiza que dispongamos de una única instancia de nuestro sistema de negociación, accesible desde cualquier lugar, que podamos utilizar en todo el Asesor Experto (EA) para gestionar todas las operaciones de negociación, como la inicialización de operaciones, la ejecución de órdenes y la gestión de las zonas de recuperación. Al comenzar con «NULL», preparamos el «trader» para que se instancie correctamente más adelante, evitando así cualquier acceso prematuro antes de que el EA esté totalmente configurado. Ahora podemos pasar a llamar a la función.
int OnInit() { //--- EA Initialization Start trader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, maxOrders, restrictMaxOrders, zoneTargetPoints, zoneSizePoints); //--- Create trader instance return trader.initialize(); //--- Initialize EA //--- EA Initialization End }
En el controlador de eventos OnInit, empezamos creando una nueva instancia de la clase «MarketZoneTrader» y asignándola al puntero global «trader». Pasamos los parámetros de entrada definidos por el usuario —«lotOption», «initialLotSize», «riskPercentage», «riskPoints», «maxOrders», «restrictMaxOrders», «zoneTargetPoints» y «zoneSizePoints»— al constructor para configurar el sistema de negociación con los ajustes deseados. A continuación, llamamos a la función «initialize» de «trader» para configurar el EA, lo que incluye el etiquetado de operaciones, la comprobación de posiciones existentes y la inicialización de indicadores, y devolvemos su resultado para indicar si la configuración se ha realizado correctamente. Esta función garantiza que nuestro EA esté totalmente preparado para empezar a operar con las configuraciones especificadas. Tras la compilación, obtenemos el siguiente resultado.

En la imagen se puede ver que el programa se ha iniciado correctamente. Sin embargo, surge un problema cuando intentamos retirar el EA del gráfico. Véase más abajo.

En la imagen se puede observar que hay objetos no liberados que provocan una fuga de memoria. Para solucionar esto, tenemos que realizar la limpieza de objetos. Para lograrlo, utilizamos la siguiente lógica.
void OnDeinit(const int reason) { //--- EA Deinitialization Start if (trader != NULL) { //--- Check trader existence delete trader; //--- Delete trader trader = NULL; //--- Clear pointer Print("EA deinitialized"); //--- Log deinitialization } //--- EA Deinitialization End }
Para gestionar la limpieza, en el controlador de eventos OnDeinit, empezamos por comprobar que el puntero «trader» no sea «NULL», con el fin de asegurarnos de que la instancia «MarketZoneTrader» existe. Si es así, utilizamos el operador delete para liberar la memoria asignada a «trader», evitando así las fugas de memoria. A continuación, establecemos «trader» en «NULL» para evitar el acceso accidental a la memoria desasignada. Por último, registramos un mensaje con la función «Print» para confirmar que el EA se ha desinicializado. Esta función garantiza que nuestro EA se cierre correctamente, liberando los recursos de forma adecuada. Ahora podemos seguir definiendo la lógica principal para gestionar las evaluaciones de señales y la gestión de las operaciones abiertas. Para eso necesitaremos funciones de utilidad.
//--- Position Management bool activateTrade(ulong ticket) { //--- Position Activation Start m_tradeConfig.currentState = INACTIVE; //--- Set state to inactive ArrayResize(m_tradeConfig.activeTickets, 0); //--- Clear tickets m_lossTracker.tradeLossTracker = 0.0; //--- Reset loss tracker if (!configureTrade(ticket)) { //--- Configure trade return false; //--- Return failure } storeTradeTicket(ticket); //--- Store ticket if (m_tradeConfig.direction == ORDER_TYPE_BUY) { //--- Handle buy position m_zoneBounds.zoneHigh = m_tradeConfig.openPrice; //--- Set zone high m_zoneBounds.zoneLow = m_zoneBounds.zoneHigh - m_tradeConfig.zoneRecoverySpan; //--- Set zone low m_tradeConfig.accumulatedBuyVolume = m_tradeConfig.initialVolume; //--- Set buy volume m_tradeConfig.accumulatedSellVolume = 0.0; //--- Reset sell volume } else { //--- Handle sell position m_zoneBounds.zoneLow = m_tradeConfig.openPrice; //--- Set zone low m_zoneBounds.zoneHigh = m_zoneBounds.zoneLow + m_tradeConfig.zoneRecoverySpan; //--- Set zone high m_tradeConfig.accumulatedSellVolume = m_tradeConfig.initialVolume; //--- Set sell volume m_tradeConfig.accumulatedBuyVolume = 0.0; //--- Reset buy volume } m_zoneBounds.zoneTargetHigh = m_zoneBounds.zoneHigh + m_tradeConfig.zoneProfitSpan; //--- Set target high m_zoneBounds.zoneTargetLow = m_zoneBounds.zoneLow - m_tradeConfig.zoneProfitSpan; //--- Set target low m_tradeConfig.currentState = RUNNING; //--- Set state to running return true; //--- Return success //--- Position Activation End } //--- Tick Processing void processTick() { //--- Tick Processing Start double askPrice = NormalizeDouble(getMarketAsk(), Digits()); //--- Get ask price double bidPrice = NormalizeDouble(getMarketBid(), Digits()); //--- Get bid price if (!isNewBar()) return; //--- Exit if not new bar if (!CopyBuffer(m_handleRsi, 0, 0, 3, m_rsiBuffer)) { //--- Load RSI data Print("Error loading RSI data. Reverting."); //--- Log RSI failure return; //--- Exit } if (!CopyBuffer(m_handleEnvUpper, 0, 0, 3, m_envUpperBandBuffer)) { //--- Load upper Envelopes Print("Error loading upper envelopes data. Reverting."); //--- Log failure return; //--- Exit } if (!CopyBuffer(m_handleEnvLower, 1, 0, 3, m_envLowerBandBuffer)) { //--- Load lower Envelopes Print("Error loading lower envelopes data. Reverting."); //--- Log failure return; //--- Exit } int ticket = 0; //--- Initialize ticket const int rsiOverbought = 70; //--- Set RSI overbought level const int rsiOversold = 30; //--- Set RSI oversold level if (m_rsiBuffer[1] < rsiOversold && m_rsiBuffer[2] > rsiOversold && m_rsiBuffer[0] < rsiOversold) { //--- Check buy signal if (askPrice > m_envUpperBandBuffer[0]) { //--- Confirm price above upper Envelopes if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) { //--- Check position limit ticket = openOrder(ORDER_TYPE_BUY, 0, 0); //--- Open buy order } } } else if (m_rsiBuffer[1] > rsiOverbought && m_rsiBuffer[2] < rsiOverbought && m_rsiBuffer[0] > rsiOverbought) { //--- Check sell signal if (bidPrice < m_envLowerBandBuffer[0]) { //--- Confirm price below lower Envelopes if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) { //--- Check position limit ticket = openOrder(ORDER_TYPE_SELL, 0, 0); //--- Open sell order } } } if (ticket > 0) { //--- Check if trade opened if (activateTrade(ticket)) { //--- Activate position Print("New position activated: Ticket=", ticket); //--- Log activation } else { Print("Failed to activate new position: Ticket=", ticket); //--- Log failure } } //--- Tick Processing End }
En este punto, continuamos desarrollando nuestro programa mediante la implementación de las funciones «activateTrade» y «processTick» dentro de la clase «MarketZoneTrader» para gestionar las posiciones y procesar los ticks del mercado. Empezamos con la función «activateTrade» para activar una operación para un «ticket» determinado. En primer lugar, establecemos «m_tradeConfig.currentState» en «INACTIVE» y vaciamos «m_tradeConfig.activeTickets» utilizando la función ArrayResize para restablecer la lista de tickets. Restablecemos «m_lossTracker.tradeLossTracker» a 0,0 y, a continuación, llamamos a «configureTrade» con «ticket». Si falla, devolvemos «false». A continuación, guardamos el «ticket» con «storeTradeTicket». Para una operación de compra («m_tradeConfig.direction» como ORDER_TYPE_BUY), establecemos «m_zoneBounds.zoneHigh» en «m_tradeConfig.openPrice», calculamos «m_zoneBounds.zoneLow» restando «m_tradeConfig.zoneRecoverySpan» y actualizamos «m_tradeConfig.accumulatedBuyVolume» a «m_tradeConfig.initialVolume», al tiempo que restablecemos «m_tradeConfig.accumulatedSellVolume» .
Para una operación de venta, establecemos «m_zoneBounds.zoneLow» en «m_tradeConfig.openPrice», sumamos «m_tradeConfig.zoneRecoverySpan» a «m_zoneBounds.zoneHigh» y ajustamos los volúmenes en consecuencia. A continuación, establecemos «m_zoneBounds.zoneTargetHigh» y «m_zoneBounds.zoneTargetLow» utilizando «m_tradeConfig.zoneProfitSpan», cambiamos «m_tradeConfig.currentState» a «RUNNING» y devolvemos «true».
A continuación, creamos la función «processTick» para gestionar los ticks del mercado. Obtenemos «askPrice» y «bidPrice» mediante «getMarketAsk» y «getMarketBid», normalizados con NormalizeDouble y «Digits». Si «isNewBar» devuelve «false», salimos para ahorrar recursos. Cargamos los datos del indicador con CopyBuffer: «m_handleRsi» en «m_rsiBuffer», «m_handleEnvUpper» en «m_envUpperBandBuffer» y «m_handleEnvLower» en «m_envLowerBandBuffer», registrando los errores con «Print» y saliendo del programa si se produce algún fallo. Para las señales de trading, hemos fijado «rsiOverbought» en 70 y «rsiOversold» en 30.
Si «m_rsiBuffer» indica una situación de sobreventa y «askPrice» supera «m_envUpperBandBuffer», abrimos una orden de compra con «openOrder» siempre que «m_restrictMaxOrders» sea falso o PositionsTotal sea inferior a «m_maxOrders». En caso de que se produzca una situación de sobrecompra con el «bidPrice» por debajo de «m_envLowerBandBuffer», abrimos una orden de venta. Si se devuelve un «ticket» válido, llamamos a «activateTrade» y registramos el resultado en el diario. Ahora podemos ejecutar la función en el controlador de eventos OnTick para procesar la evaluación de la señal y el inicio de la posición.
void OnTick() { //--- Tick Handling Start if (trader != NULL) { //--- Check trader existence trader.processTick(); //--- Process tick } //--- Tick Handling End }
En el controlador de eventos «OnTick», empezamos por comprobar que el puntero «trader», nuestra instancia de la clase «MarketZoneTrader», no sea «NULL», para asegurarnos de que el sistema de negociación está inicializado. Si existe, llamamos a la función «processTick» de «trader» para gestionar cada tick del mercado, evaluar las posiciones, comprobar las señales de los indicadores y ejecutar operaciones según sea necesario. Tras la compilación, obtenemos el siguiente resultado.

En la imagen podemos ver que hemos identificado una señal, la hemos evaluado y hemos abierto una posición de compra. Lo que tenemos que hacer ahora es gestionar las posiciones abiertas. Lo gestionaremos mediante funciones para garantizar la modularidad.
//--- Market Tick Evaluation void evaluateMarketTick() { //--- Tick Evaluation Start if (m_tradeConfig.currentState == INACTIVE) return; //--- Exit if inactive if (m_tradeConfig.currentState == TERMINATING) { //--- Check terminating state finalizePosition(); //--- Finalize position return; //--- Exit } }
Aquí implementamos la función «evaluateMarketTick» dentro de la clase «MarketZoneTrader» para evaluar las condiciones del mercado en las operaciones activas. Empezamos comprobando el valor de «m_tradeConfig.currentState» para ver si es «INACTIVE». Si es así, salimos inmediatamente para evitar un procesamiento innecesario cuando no hay operaciones activas. A continuación, comprobamos si «m_tradeConfig.currentState» es «TERMINATING». En ese caso, llamamos a la función «finalizePosition» para cerrar todas las posiciones abiertas y completar el ciclo de negociación, y a continuación salimos. Esta es la función para cerrar las operaciones.
//--- Position Finalization bool finalizePosition() { //--- Position Finalization Start m_tradeConfig.currentState = TERMINATING; //--- Set terminating state TradeMetrics metrics = {true, 0.0, 0.0}; //--- Initialize metrics closeActiveTrades(metrics); //--- Close all trades if (metrics.operationSuccess) { //--- Check success ArrayResize(m_tradeConfig.activeTickets, 0); //--- Clear tickets m_tradeConfig.currentState = INACTIVE; //--- Set inactive state Print("Position closed successfully"); //--- Log success } else { Print("Failed to close position"); //--- Log failure } return metrics.operationSuccess; //--- Return status //--- Position Finalization End }
Empezamos estableciendo «m_tradeConfig.currentState» en «TERMINATING» para indicar que el ciclo comercial está llegando a su fin. Esto ayuda a evitar el ciclo de gestión cuando estamos en proceso de cerrar las operaciones. A continuación, inicializamos una estructura «TradeMetrics» denominada «metrics» con «operationSuccess» establecido en «true», «totalVolume» en 0,0 y «netProfitLoss» en 0,0 para realizar un seguimiento de los resultados de las operaciones al cierre. Llamamos a «closeActiveTrades» con «metrics» para cerrar todas las posiciones abiertas que figuran en «m_tradeConfig.activeTickets». Si «metrics.operationSuccess» sigue siendo «true», vaciamos «m_tradeConfig.activeTickets» utilizando ArrayResize para restablecer la lista de tickets, establecemos «m_tradeConfig.currentState» en «INACTIVE» para marcar el sistema como inactivo y registramos el éxito con «Print».
Si el cierre falla, registramos el error mediante «Print». Por último, devolvemos «metrics.operationSuccess» para indicar si el proceso se ha completado correctamente. Si no hemos cerrado las operaciones en este momento, significa que aún no estamos en el proceso de cierre de posiciones, por lo que podemos continuar con la evaluación para comprobar si el precio ha alcanzado las zonas de recuperación o los niveles objetivo. Empezaremos con la instancia «buy».
double currentPrice; //--- Initialize price if (m_tradeConfig.direction == ORDER_TYPE_BUY) { //--- Handle buy position currentPrice = getMarketBid(); //--- Get bid price if (currentPrice > m_zoneBounds.zoneTargetHigh) { //--- Check profit target Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); //--- Log closure finalizePosition(); //--- Close position return; //--- Exit } else if (currentPrice < m_zoneBounds.zoneLow) { //--- Check recovery trigger Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); //--- Log recovery triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice); //--- Open sell recovery } }
Continuamos implementando la lógica dentro de la función «evaluateMarketTick» de la clase «MarketZoneTrader» para gestionar las posiciones de compra. Empezamos declarando la variable «currentPrice» para almacenar el precio de mercado. Si «m_tradeConfig.direction» es ORDER_TYPE_BUY, establecemos «currentPrice» utilizando la función «getMarketBid» para obtener el precio bid, ya que este es el precio al que podemos cerrar una posición de compra. A continuación, comprobamos si «currentPrice» supera «m_zoneBounds.zoneTargetHigh». Si es así, registramos el cierre con «Print», mostrando el precio de compra y el objetivo, y luego llamamos a «finalizePosition» para cerrar la operación y salimos con «return».
Si «currentPrice» cae por debajo de «m_zoneBounds.zoneLow», registramos un disparador de recuperación con «Print» y llamamos a «triggerRecoveryTrade» con ORDER_TYPE_SELL y «currentPrice» para abrir una operación de venta con el fin de mitigar las pérdidas. Esta lógica garantiza que cerremos las operaciones de compra rentables o iniciemos la recuperación de las que están en pérdidas, lo que permite que nuestra estrategia se adapte a las circunstancias. A continuación se muestra la lógica de la función encargada de abrir operaciones de recuperación.
//--- Recovery Trade Handling void triggerRecoveryTrade(ENUM_ORDER_TYPE tradeDirection, double price) { //--- Recovery Trade Start TradeMetrics metrics = {true, 0.0, 0.0}; //--- Initialize metrics closeActiveTrades(metrics); //--- Close existing trades for (int i = 0; i < 10 && !metrics.operationSuccess; i++) { //--- Retry closure Sleep(1000); //--- Wait 1 second metrics.operationSuccess = true; //--- Reset success flag closeActiveTrades(metrics); //--- Retry closure } m_lossTracker.tradeLossTracker += metrics.netProfitLoss; //--- Update loss tracker if (m_lossTracker.tradeLossTracker > 0 && metrics.operationSuccess) { //--- Check positive profit Print("Closing position due to positive profit: ", m_lossTracker.tradeLossTracker); //--- Log closure finalizePosition(); //--- Close position m_lossTracker.tradeLossTracker = 0.0; //--- Reset loss tracker return; //--- Exit } double tradeSize = determineRecoverySize(tradeDirection); //--- Calculate trade size ulong ticket = openMarketTrade(tradeDirection, tradeSize, price); //--- Open recovery trade if (ticket > 0) { //--- Check if trade opened storeTradeTicket(ticket); //--- Store ticket m_tradeConfig.direction = tradeDirection; //--- Update direction if (tradeDirection == ORDER_TYPE_BUY) m_tradeConfig.accumulatedBuyVolume += tradeSize; //--- Update buy volume else m_tradeConfig.accumulatedSellVolume += tradeSize; //--- Update sell volume Print("Recovery trade opened: Ticket=", ticket, ", Direction=", EnumToString(tradeDirection), ", Volume=", tradeSize); //--- Log recovery trade } //--- Recovery Trade End } //--- Recovery Size Calculation double determineRecoverySize(ENUM_ORDER_TYPE tradeDirection) { //--- Recovery Size Calculation Start double tradeSize = -m_lossTracker.tradeLossTracker / m_tradeConfig.zoneProfitSpan; //--- Calculate lot size tradeSize = MathCeil(tradeSize / getMarketVolumeStep()) * getMarketVolumeStep(); //--- Round to volume step return tradeSize; //--- Return trade size //--- Recovery Size Calculation End }
Para gestionar los casos en los que el mercado requiere activar operaciones de recuperación, comenzamos con la función «triggerRecoveryTrade», que se encarga de gestionar las operaciones de recuperación cuando una posición evoluciona en nuestra contra. En primer lugar, inicializamos una estructura «TradeMetrics» denominada «metrics» con «operationSuccess» establecido en «true», «totalVolume» en 0,0 y «netProfitLoss» en 0,0. Llamamos a «closeActiveTrades» con «metrics» para cerrar las posiciones existentes. Si «metrics.operationSuccess» es falso, volvemos a intentarlo hasta 10 veces, esperando un segundo con Sleep y restableciendo «operationSuccess» antes de cada intento.
Actualizamos «m_lossTracker.tradeLossTracker» añadiendo «metrics.netProfitLoss». Si «m_lossTracker.tradeLossTracker» es positivo y «metrics.operationSuccess» es verdadero, registramos el cierre con «Print», llamamos a «finalizePosition», restablecemos «m_lossTracker.tradeLossTracker» a 0,0 y salimos con «return». De lo contrario, calculamos el «tradeSize» de recuperación mediante «determineRecoverySize» con «tradeDirection» y, a continuación, abrimos una nueva operación con «openMarketTrade» utilizando «tradeDirection», «tradeSize» y «price».
Si el «ticket» devuelto es válido, lo guardamos con «storeTradeTicket», actualizamos «m_tradeConfig.direction», ajustamos «m_tradeConfig.accumulatedBuyVolume» o «m_tradeConfig.accumulatedSellVolume» en función de «tradeDirection» y registramos la operación con «Print» utilizando EnumToString. A continuación, creamos la función «determineRecoverySize» para calcular el tamaño de la posición en las operaciones de recuperación. Calculamos «tradeSize» dividiendo el valor negativo de «m_lossTracker.tradeLossTracker» entre «m_tradeConfig.zoneProfitSpan» para determinar el tamaño de la operación con el fin de cubrir las pérdidas. A continuación, redondeamos «tradeSize» al paso de volumen del bróker utilizando MathCeil y «getMarketVolumeStep» para garantizar el cumplimiento de las normas, y devolvemos el resultado. Ahora, esto gestiona las instancias de recuperación y podemos continuar con la lógica para gestionar las zonas de venta. Con esto ya tenemos resueltas las instancias de recuperación, y podemos continuar con la lógica para gestionar las zonas de venta. La función completa final será la siguiente.
//--- Market Tick Evaluation void evaluateMarketTick() { //--- Tick Evaluation Start if (m_tradeConfig.currentState == INACTIVE) return; //--- Exit if inactive if (m_tradeConfig.currentState == TERMINATING) { //--- Check terminating state finalizePosition(); //--- Finalize position return; //--- Exit } double currentPrice; //--- Initialize price if (m_tradeConfig.direction == ORDER_TYPE_BUY) { //--- Handle buy position currentPrice = getMarketBid(); //--- Get bid price if (currentPrice > m_zoneBounds.zoneTargetHigh) { //--- Check profit target Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); //--- Log closure finalizePosition(); //--- Close position return; //--- Exit } else if (currentPrice < m_zoneBounds.zoneLow) { //--- Check recovery trigger Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); //--- Log recovery triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice); //--- Open sell recovery } } else if (m_tradeConfig.direction == ORDER_TYPE_SELL) { //--- Handle sell position currentPrice = getMarketAsk(); //--- Get ask price if (currentPrice < m_zoneBounds.zoneTargetLow) { //--- Check profit target Print("Closing position: Ask=", currentPrice, " < TargetLow=", m_zoneBounds.zoneTargetLow); //--- Log closure finalizePosition(); //--- Close position return; //--- Exit } else if (currentPrice > m_zoneBounds.zoneHigh) { //--- Check recovery trigger Print("Triggering recovery trade: Ask=", currentPrice, " > ZoneHigh=", m_zoneBounds.zoneHigh); //--- Log recovery triggerRecoveryTrade(ORDER_TYPE_BUY, currentPrice); //--- Open buy recovery } } //--- Tick Evaluation End }
La función ya contempla toda la lógica de recuperación. Tras la compilación, obtenemos el siguiente resultado.

En la imagen podemos ver que gestionamos con éxito las posiciones que se activan a raíz de señales de rebote de tendencia. Lo que queda por hacer es realizar un backtesting del programa, y eso se trata en la siguiente sección.
Backtesting
Tras realizar exhaustivas pruebas retrospectivas, hemos obtenido los siguientes resultados.
Gráfico de backtest:

Informe de backtest:

Conclusión
En conclusión, hemos desarrollado un programa MQL5 robusto que implementa un sistema de recuperación por zonas para el trading de tendencias con envolventes, combinando el índice de fuerza relativa (RSI) y los indicadores de envolventes para identificar oportunidades de trading y gestionar las pérdidas a través de zonas de recuperación estructuradas, utilizando un enfoque de programación orientada a objetos (POO). Mediante el uso de componentes como la clase «MarketZoneTrader», estructuras como «TradeConfig» y «ZoneBoundaries», y funciones como «processTick» y «triggerRecoveryTrade», hemos creado un sistema flexible que puedes adaptar ajustando parámetros como «zoneTargetPoints» o «riskPercentage» para que se ajuste a diversas condiciones del mercado.
Aviso legal: Este artículo tiene únicamente fines educativos. Las operaciones de trading conllevan riesgos financieros importantes, y la volatilidad del mercado puede provocar pérdidas. Es fundamental realizar pruebas retrospectivas exhaustivas y aplicar una gestión del riesgo cuidadosa antes de utilizar este programa en mercados reales.
Con las bases sentadas en este artículo, podrás perfeccionar este sistema de recuperación por zonas o adaptar su lógica para desarrollar nuevas estrategias de trading, lo que impulsará tu progreso en el trading algorítmico. ¡Feliz trading!
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/18720
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.
Redes neuronales en el trading: Desentrañando los componentes estructurados (SCNN)
Red neuronal cuántica en MQL5 (Parte II): Entrenamos una red neuronal con retropropagación del error utilizando matrices de Márkov de ALGLIB
Optimización por comunidad de científicos — Community of Scientist Optimization (CoSO): Práctica
Arbitraje estadístico mediante acciones cointegradas (Parte 1): Pruebas de cointegración de Engle-Granger y Johansen
- 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
Muchísimas gracias 🙏
De nada. Gracias