English Русский 中文 Deutsch 日本語
preview
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

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

MetaTrader 5Trading |
35 2
Allan Munene Mutiiria
Allan Munene Mutiiria

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:

  1. Comprender la arquitectura de la estrategia de tendencia con Envelopes y recuperación por zonas
  2. Implementación en MQL5
  3. Backtesting
  4. 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.

PLAN ESTRATÉGICO


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.

ARCHIVO DE OPERACIONES DE NEGOCIACIÓN DE MQL5

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.

ENTRADAS CARGADAS

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.

EJEMPLO DE ZONAS

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.

IMAGEN DE INICIALIZACIÓN

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.

FUGAS DE MEMORIA EN OBJETOS

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.

POSICIÓN INICIAL

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.

RESULTADO FINAL

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:

GRÁFICO

Informe de backtest:

INFORME


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

Sabrina Hellal
Sabrina Hellal | 9 jul 2025 en 13:44
Muchísimas gracias 🙏
Allan Munene Mutiiria
Allan Munene Mutiiria | 9 jul 2025 en 16:46
Sabrina Hellal #:
Muchísimas gracias 🙏

De nada. Gracias

Redes neuronales en el trading: Desentrañando los componentes estructurados (SCNN) Redes neuronales en el trading: Desentrañando los componentes estructurados (SCNN)
Le invitamos a conocer el innovador framework SCNN, que lleva el análisis de series temporales a un nuevo nivel gracias a una separación clara de los datos en componentes de largo plazo, estacionales, de corto plazo y residuales. Este enfoque mejora considerablemente la precisión de la predicción y permite que el modelo se adapte a la compleja y cambiante dinámica del mercado.
Red neuronal cuántica en MQL5 (Parte II): Entrenamos una red neuronal con retropropagación del error utilizando matrices de Márkov de ALGLIB Red neuronal cuántica en MQL5 (Parte II): Entrenamos una red neuronal con retropropagación del error utilizando matrices de Márkov de ALGLIB
En este artículo se presenta una arquitectura innovadora de red neuronal cuántica para el trading algorítmico, que combina los principios de la mecánica cuántica con los métodos actuales de aprendizaje automático. El sistema incorpora efectos cuánticos (resonancia, interferencia, decoherencia), memoria multinivel de distintas escalas temporales, cadenas de Márkov con la biblioteca ALGLIB y control adaptativo de parámetros. La implementación completa está realizada en MQL5 utilizando los tipos integrados matrix/vector, lo que elimina las barreras de adopción en MetaTrader 5.
Optimización por comunidad de científicos — Community of Scientist Optimization (CoSO): Práctica Optimización por comunidad de científicos — Community of Scientist Optimization (CoSO): Práctica
Continuación del tema de la optimización mediante una comunidad de científicos. CoSO no debe considerarse una solución lista para usar, sino una plataforma de investigación prometedora. Con los ajustes adecuados, CoSO podría encontrar su nicho en problemas en los que la adaptabilidad y la robustez ante los cambios son importantes, y en los que el tiempo de cálculo no es un factor crítico.
Arbitraje estadístico mediante acciones cointegradas (Parte 1): Pruebas de cointegración de Engle-Granger y Johansen Arbitraje estadístico mediante acciones cointegradas (Parte 1): Pruebas de cointegración de Engle-Granger y Johansen
El objetivo de este artículo es ofrecer una introducción sencilla y accesible para los operadores a las pruebas de cointegración más habituales, junto con una guía básica para comprender sus resultados. Las pruebas de cointegración de Engle-Granger y Johansen pueden revelar pares o grupos de activos estadísticamente significativos que comparten una dinámica a largo plazo. La prueba de Johansen resulta especialmente útil para carteras con tres o más activos, ya que calcula la fuerza de los vectores cointegrados de una sola vez.