English Русский 中文 Deutsch 日本語
preview
Automatización de estrategias de trading en MQL5 (Parte 23): Recuperación por zonas con trailing stop y lógica de cestas

Automatización de estrategias de trading en MQL5 (Parte 23): Recuperación por zonas con trailing stop y lógica de cestas

MetaTrader 5Trading |
24 1
Allan Munene Mutiiria
Allan Munene Mutiiria

Introducción

En nuestro artículo anterior (Parte 22), desarrollamos un sistema de recuperación por zonas para el trading de tendencias con envolventes en MetaQuotes Language 5 (MQL5), utilizando el RSI y los indicadores de envolventes (Envelopes) para automatizar las operaciones y gestionar las pérdidas mediante zonas de recuperación estructuradas. En la parte 23, perfeccionamos esta estrategia incorporando stops dinámicos para asegurar las ganancias de forma dinámica y un sistema de cestas múltiples para gestionar de manera eficiente múltiples señales de negociación, mejorando así la adaptabilidad en mercados volátiles. Trataremos los siguientes temas:

  1. Comprender el «trailing stop» mejorado y la arquitectura de múltiples cestas
  2. Implementación en MQL5
  3. Backtesting
  4. Conclusión

Al final, tendrás un sistema de trading MQL5 perfeccionado con funciones avanzadas, listo para ser probado y personalizado aún más. ¡Manos a la obra!


Comprender el «trailing stop» mejorado y la arquitectura de múltiples cestas

La estrategia de recuperación de zona que estamos perfeccionando está diseñada para convertir las posibles pérdidas en ganancias, abriendo operaciones opuestas dentro de un rango de precios definido cuando el mercado se mueve en nuestra contra. Ahora lo estamos mejorando con dos novedades clave: los stops dinámicos y la negociación con varias cestas. Los «trailing stops» son necesarios porque nos permiten asegurar las ganancias a medida que el mercado evoluciona a nuestro favor, protegiendo así las ganancias sin cerrar las operaciones demasiado pronto, lo cual es fundamental en mercados con tendencia, donde los precios pueden recorrer tramos significativos. La negociación con múltiples cestas es igualmente importante, ya que nos permite gestionar varias señales de negociación independientes al mismo tiempo, lo que aumenta nuestra capacidad para aprovechar más oportunidades al tiempo que mantenemos el riesgo organizado en grupos de operaciones separados. Véase más abajo.

ARQUITECTURA DEL TRAILING STOP

Lograremos estas mejoras mediante la integración de un mecanismo de «trailing stop» que ajusta dinámicamente el nivel de stop-loss en función de la evolución del mercado, lo que garantiza la protección de los beneficios al tiempo que deja margen para que las operaciones sigan creciendo. Para la negociación con múltiples cestas, introduciremos un sistema para gestionar múltiples instancias de trading, cada una con su identificador único, lo que nos permitirá seguir y gestionar varios ciclos de recuperación de zonas a la vez sin solapamientos. Tenemos previsto combinar estas características con los indicadores ya existentes Relative Strength Indicator (RSI) y Envelopes para mantener entradas precisas en las operaciones, mientras que los stops dinámicos y el sistema de cesta se complementan para optimizar la protección de los beneficios y la capacidad operativa, lo que hace que la estrategia sea más sólida y adaptable a diversas condiciones de mercado. Acompáñanos mientras llevamos estas mejoras a la práctica.


Implementación en MQL5

Para implementar las mejoras en MQL5, añadiremos algunos campos de entrada adicionales para la función de «trailing stop» y cambiaremos el nombre del límite máximo de órdenes, ya que ahora estamos trabajando con varias instancias de recuperación.

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    baseMagicNumber = 123456789;                              // Base Magic Number
input int    maxInitialPositions = 1;                                  // Maximum Initial Positions (Baskets/Signals)
input double zoneTargetPoints = 600;                                   // Zone Target Points
input double zoneSizePoints = 300;                                     // Zone Size Points
input bool   enableInitialTrailing = true;                             // Enable Trailing Stop for Initial Positions
input int    trailingStopPoints = 50;                                  // Trailing Stop Points
input int    minProfitPoints = 50;                                     // Minimum Profit Points to Start Trailing

Comenzamos la mejora de nuestro sistema «Zone Recovery» para el trading de tendencias con envolventes en MQL5 actualizando los parámetros de entrada del grupo «EA GENERAL SETTINGS» para que admitan los trailing stops y el trading con varias cestas. Realizamos cuatro cambios fundamentales en los datos de entrada. En primer lugar, cambiamos el nombre de «magicNumber» por «baseMagicNumber» y lo establecemos en 123456789, para que sirva de punto de partida a la hora de generar números mágicos únicos para varias cestas de operaciones, garantizando que cada cesta se gestione y se identifique por separado en nuestro sistema de cestas múltiples. En segundo lugar, sustituimos «maxOrders» por «maxInitialPositions», con un valor de 1, para limitar el número de cestas de operaciones iniciales, lo que nos permite gestionar varias señales de operaciones de forma eficiente.

En tercer lugar, añadimos «enableInitialTrailing», un valor booleano establecido en «true», que nos permite activar o desactivar los stops dinámicos para las posiciones iniciales, lo que nos proporciona control sobre nuestra nueva función de fijación de beneficios. En cuarto lugar, introducimos los parámetros «trailingStopPoints», fijado en 50, y «minProfitPoints», fijado en 50, que definen, respectivamente, la distancia del trailing stop y el beneficio mínimo necesario para activarlo, con el fin de implementar una protección dinámica de las ganancias. Estos cambios permitirán que nuestro sistema gestione múltiples cestas de operaciones y proteja los beneficios de forma eficaz, allanando el camino para futuras mejoras. Destacaremos los cambios para facilitar su seguimiento y evitar confusiones. Tras la compilación, disponemos del siguiente conjunto de datos de entrada.

NUEVO CONJUNTO DE DATOS DE ENTRADA

Tras añadir los parámetros de entrada, ya podemos declarar por adelantado la clase «MarketZoneTrader» para que la clase que la utiliza pueda acceder a ella, ya que ahora queremos gestionar varias instancias de operaciones.

//--- Forward Declaration of MarketZoneTrader
class MarketZoneTrader;

Aquí introducimos una declaración anticipada de la clase «MarketZoneTrader». Lo añadimos antes de la definición de la clase «BasketManager», que definiremos justo después de esta clase, para que pueda hacer referencia a «MarketZoneTrader» sin necesidad de que esta última esté aún definida en su totalidad. Este cambio es necesario porque nuestro nuevo sistema de cestas múltiples, gestionado por «BasketManager», tendrá que crear y gestionar varias instancias de «MarketZoneTrader» para las distintas cestas de operaciones. Al declarar primero «MarketZoneTrader», nos aseguramos de que el compilador lo reconozca cuando se utilice en la nueva clase, lo que permite que nuestro sistema admita de forma eficiente varios ciclos de negociación simultáneos. A continuación, podemos definir la clase «BasketManager».

//--- Basket Manager Class to Handle Multiple Traders
class BasketManager {
private:
   MarketZoneTrader* m_traders[];                                        //--- Array of trader instances
   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
   string            m_symbol;                                          //--- Trading symbol
   int               m_baseMagicNumber;                                 //--- Base magic number
   int               m_maxInitialPositions;                             //--- Maximum baskets (signals)

   //--- Initialize Indicators
   bool initializeIndicators() {
      m_handleRsi = iRSI(m_symbol, PERIOD_CURRENT, 8, PRICE_CLOSE);
      if (m_handleRsi == INVALID_HANDLE) {
         Print("Failed to initialize RSI indicator");
         return false;
      }
      m_handleEnvUpper = iEnvelopes(m_symbol, PERIOD_CURRENT, 150, 0, MODE_SMA, PRICE_CLOSE, 0.1);
      if (m_handleEnvUpper == INVALID_HANDLE) {
         Print("Failed to initialize upper Envelopes indicator");
         return false;
      }
      m_handleEnvLower = iEnvelopes(m_symbol, PERIOD_CURRENT, 95, 0, MODE_SMA, PRICE_CLOSE, 1.4);
      if (m_handleEnvLower == INVALID_HANDLE) {
         Print("Failed to initialize lower Envelopes indicator");
         return false;
      }
      ArraySetAsSeries(m_rsiBuffer, true);
      ArraySetAsSeries(m_envUpperBandBuffer, true);
      ArraySetAsSeries(m_envLowerBandBuffer, true);
      return true;
   }

}

Para facilitar la gestión de las operaciones de cestas, definimos la clase «BasketManager» con miembros privados para gestionar varias instancias de la clase «MarketZoneTrader» y los datos de los indicadores. Creamos «m_traders», una matriz de «MarketZoneTrader» punteros, para almacenar cestas de operaciones individuales, cada una de las cuales representa un ciclo de recuperación de zona independiente. Este cambio es fundamental, ya que nos permite gestionar varias señales de negociación al mismo tiempo, a diferencia del enfoque de instancia única de la versión anterior. También declaramos «m_handleRsi», «m_handleEnvUpper» y «m_handleEnvLower» para almacenar los identificadores de los indicadores, y las matrices «m_rsiBuffer», «m_envUpperBandBuffer» y «m_envLowerBandBuffer» para almacenar los datos del RSI y de las envolventes, trasladando la gestión de los indicadores de «MarketZoneTrader» a «BasketManager» para un control centralizado de todas las cestas.

Además, añadimos «m_symbol» para almacenar el símbolo de trading, «m_baseMagicNumber» para generar números mágicos únicos por cada cesta y «m_maxInitialPositions» para limitar el número de cestas activas, en consonancia con el nuevo parámetro «maxInitialPositions». En la función «initializeIndicators», configuramos el indicador RSI con iRSI utilizando un parámetro de 8 períodos y los indicadores Envelopes con iEnvelopes (150 períodos con una desviación de 0,1 y 95 períodos con una desviación de 1,4), comprobando si hay un «INVALID_HANDLE» y registrando los errores con Print. Configuramos «m_rsiBuffer», «m_envUpperBandBuffer» y «m_envLowerBandBuffer» como matrices de series temporales mediante ArraySetAsSeries. Esta nueva estructura de clases nos permitirá coordinar de forma eficiente múltiples cestas de operaciones, centralizando los datos de los indicadores para generar señales coherentes en todas las cestas. A continuación, necesitamos una lógica para contar todas las posiciones individuales de las cestas, con el fin de facilitar su seguimiento, y limpiar las cestas.

//--- Count Active Baskets
int countActiveBaskets() {
   int count = 0;
   for (int i = 0; i < ArraySize(m_traders); i++) {
      if (m_traders[i] != NULL && m_traders[i].getCurrentState() != MarketZoneTrader::INACTIVE) {
         count++;
      }
   }
   return count;
}

//--- Cleanup Terminated Baskets
void cleanupTerminatedBaskets() {
   int newSize = 0;
   for (int i = 0; i < ArraySize(m_traders); i++) {
      if (m_traders[i] != NULL && m_traders[i].getCurrentState() == MarketZoneTrader::INACTIVE) {
         delete m_traders[i];
         m_traders[i] = NULL;
      }
      if (m_traders[i] != NULL) newSize++;
   }
   MarketZoneTrader* temp[];
   ArrayResize(temp, newSize);
   int index = 0;
   for (int i = 0; i < ArraySize(m_traders); i++) {
      if (m_traders[i] != NULL) {
         temp[index] = m_traders[i];
         index++;
      }
   }
   ArrayFree(m_traders);
   ArrayResize(m_traders, newSize);
   for (int i = 0; i < newSize; i++) {
      m_traders[i] = temp[i];
   }
   ArrayFree(temp);
}

Aquí añadimos dos nuevas funciones a la clase «BasketManager»: «countActiveBaskets» y «cleanupTerminatedBaskets». Empezamos con la función «countActiveBaskets» para llevar la cuenta del número de cestas activas. Inicializamos una variable «count» con el valor 0 y recorremos la matriz «m_traders» utilizando la función ArraySize. Para cada entrada de «m_traders» que no sea nula, comprobamos si su estado, obtenido mediante «getCurrentState», no es «MarketZoneTrader::INACTIVE». Si está activo, incrementamos «count». Devolvemos «count» para controlar cuántas cestas se están ejecutando en ese momento, lo cual es fundamental para garantizar que no superemos el límite «m_maxInitialPositions» al abrir nuevas cestas.

A continuación, creamos la función «cleanupTerminatedBaskets» para eliminar las cestas inactivas y optimizar la memoria. En primer lugar, contamos las entradas no nulas de «m_traders» recorriendo el array en un bucle. Si un trader no es nulo y su «getCurrentState» devuelve «MarketZoneTrader::INACTIVE», utilizamos «delete» para liberar su memoria y establecemos el valor en «NULL». Llevamos la cuenta del número de operadores no nulos restantes en «newSize». A continuación, creamos un array temporal llamado «temp», le cambiamos el tamaño a «newSize» con ArrayResize y copiamos las operaciones que no sean nulas de «m_traders» a «temp» utilizando un contador «index». Borramos «m_traders» con «ArrayFree», le cambiamos el tamaño a «newSize» y recuperamos los traders de «temp». Por último, liberamos «temp» con ArrayFree. Esta limpieza garantiza que eliminemos las cestas cerradas, lo que permite que nuestro sistema siga funcionando de forma eficiente y esté listo para nuevas operaciones. A continuación, pasamos al modificador de acceso «public», donde cambiaremos la forma en que gestionamos el constructor y el destructor a la hora de inicializar y destruir los miembros y elementos de la clase.

public:
   BasketManager(string symbol, int baseMagic, int maxInitPos) {
      m_symbol = symbol;
      m_baseMagicNumber = baseMagic;
      m_maxInitialPositions = maxInitPos;
      ArrayResize(m_traders, 0);
      m_handleRsi = INVALID_HANDLE;
      m_handleEnvUpper = INVALID_HANDLE;
      m_handleEnvLower = INVALID_HANDLE;
   }

   ~BasketManager() {
      for (int i = 0; i < ArraySize(m_traders); i++) {
         if (m_traders[i] != NULL) delete m_traders[i];
      }
      ArrayFree(m_traders);
      cleanupIndicators();
   }

Empezamos con el constructor de «BasketManager», que toma como parámetros «symbol», «baseMagic» y «maxInitPos». Los asignamos a «m_symbol», «m_baseMagicNumber» y «m_maxInitialPositions», respectivamente, para establecer el símbolo de negociación, el número mágico base para la identificación única de la cesta y el número máximo de cestas activas. Inicializamos la matriz «m_traders» con tamaño cero mediante la función ArrayResize y establecemos los identificadores de los indicadores —«m_handleRsi», «m_handleEnvUpper» y «m_handleEnvLower»— en «INVALID_HANDLE» para preparar su configuración posterior. Este constructor es fundamental para configurar el sistema de cestas múltiples.

A continuación, creamos el destructor de «~BasketManager» para liberar recursos. Por lo general, los destructores llevan el signo de la tilde como prefijo, a modo de recordatorio. Recorremos el array «m_traders» utilizando ArraySize y eliminamos todas las instancias de «MarketZoneTrader» que no sean nulas con delete para liberar su memoria. A continuación, liberamos la matriz «m_traders» con ArrayFree y llamamos a «cleanupIndicators» para liberar los identificadores y los búferes de los indicadores. Esto garantiza que nuestro sistema se apague correctamente, evitando fugas de memoria cuando se detiene el EA. En la versión anterior, tuvimos que añadir la lógica de eliminación en el controlador de eventos OnDeinit directamente después de darnos cuenta de que había una fuga de memoria, pero aquí podemos añadirla antes, ya que sabemos de antemano que tenemos que ocuparnos de las fugas de memoria. A continuación, debemos modificar la lógica de inicialización para que pueda cargar las posiciones existentes en las respectivas cestas. Esta es la lógica que implementamos para lograrlo.

bool initialize() {
   if (!initializeIndicators()) return false;
   //--- Load existing positions into baskets
   int totalPositions = PositionsTotal();
   for (int i = 0; i < totalPositions; i++) {
      ulong ticket = PositionGetTicket(i);
      if (PositionSelectByTicket(ticket)) {
         if (PositionGetString(POSITION_SYMBOL) == m_symbol) {
            long magic = PositionGetInteger(POSITION_MAGIC);
            if (magic >= m_baseMagicNumber && magic < m_baseMagicNumber + m_maxInitialPositions) {
               //--- Check if basket already exists for this magic
               bool exists = false;
               for (int j = 0; j < ArraySize(m_traders); j++) {
                  if (m_traders[j] != NULL && m_traders[j].getMagicNumber() == magic) {
                     exists = true;
                     break;
                  }
               }
               if (!exists && countActiveBaskets() < m_maxInitialPositions) {
                  createNewBasket(magic, ticket);
               }
            }
         }
      }
   }
   Print("BasketManager initialized with ", ArraySize(m_traders), " existing baskets");
   return true;
}

/*
//--- PREVIOUS 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
}
*/

Aquí implementamos la función «initialize» actualizada en la clase «BasketManager» para dar soporte a nuestra mejora en la negociación con cestas múltiples, inicializando los indicadores y cargando las posiciones existentes en cestas separadas. Empezamos llamando a «initializeIndicators» para configurar los indicadores RSI y Envelopes; si falla, devuelve «false», lo que garantiza que nuestro sistema disponga de los datos de mercado necesarios. A diferencia de la versión anterior, en la que gestionábamos la configuración de los indicadores directamente en la función «initialize» de «MarketZoneTrader», ahora centralizamos esta tarea en «BasketManager» para compartir los datos de los indicadores entre varias cestas. A continuación, comprobamos si hay posiciones existentes mediante la función PositionsTotal y recorremos cada una de ellas, obteniendo su «ticket» con la función PositionGetTicket.

Si PositionSelectByTicket se ejecuta correctamente y el símbolo de la posición coincide con «m_symbol» (mediante PositionGetString), comprobamos que su número mágico, obtenido con «PositionGetInteger», se encuentre dentro del intervalo comprendido entre «m_baseMagicNumber» y «m_baseMagicNumber + m_maxInitialPositions». A continuación, comprobamos si ya existe una cesta para ese número mágico recorriendo «m_traders» y llamando a «getMagicNumber» para las entradas que no sean nulas. Si no existe ninguna cesta y «countActiveBaskets» es inferior a «m_maxInitialPositions», llamamos a «createNewBasket» con el número mágico y el «ticket» para cargar la posición en una nueva cesta. Por último, registramos el número de cestas inicializadas con «Print» utilizando ArraySize de «m_traders» y devolvemos «true». Al ejecutar el programa, obtenemos el siguiente resultado.

INICIALIZACIÓN DE LAS CESTAS

Ahora podemos pasar al procesamiento de los ticks, donde debemos gestionar las cestas existentes en cada tick y crear nuevas cestas cuando se confirmen nuevas señales en la función «processTick», a diferencia de la versión anterior, en la que solo teníamos que iniciar operaciones basándonos en señales confirmadas.

void processTick() {
   //--- Process existing baskets
   for (int i = 0; i < ArraySize(m_traders); i++) {
      if (m_traders[i] != NULL) {
         m_traders[i].processTick(m_rsiBuffer, m_envUpperBandBuffer, m_envLowerBandBuffer);
      }
   }
   cleanupTerminatedBaskets();

   //--- Check for new signals on new bar
   if (!isNewBar()) return;

   if (!CopyBuffer(m_handleRsi, 0, 0, 3, m_rsiBuffer)) {
      Print("Error loading RSI data. Reverting.");
      return;
   }
   if (!CopyBuffer(m_handleEnvUpper, 0, 0, 3, m_envUpperBandBuffer)) {
      Print("Error loading upper envelopes data. Reverting.");
      return;
   }
   if (!CopyBuffer(m_handleEnvLower, 1, 0, 3, m_envLowerBandBuffer)) {
      Print("Error loading lower envelopes data. Reverting.");
      return;
   }

   const int rsiOverbought = 70;
   const int rsiOversold = 30;
   int ticket = -1;
   ENUM_ORDER_TYPE signalType = (ENUM_ORDER_TYPE)-1;

   double askPrice = NormalizeDouble(SymbolInfoDouble(m_symbol, SYMBOL_ASK), Digits());
   double bidPrice = NormalizeDouble(SymbolInfoDouble(m_symbol, SYMBOL_BID), Digits());

   if (m_rsiBuffer[1] < rsiOversold && m_rsiBuffer[2] > rsiOversold && m_rsiBuffer[0] < rsiOversold) {
      if (askPrice > m_envUpperBandBuffer[0]) {
         if (countActiveBaskets() < m_maxInitialPositions) {
            signalType = ORDER_TYPE_BUY;
         }
      }
   } else if (m_rsiBuffer[1] > rsiOverbought && m_rsiBuffer[2] < rsiOverbought && m_rsiBuffer[0] > rsiOverbought) {
      if (bidPrice < m_envLowerBandBuffer[0]) {
         if (countActiveBaskets() < m_maxInitialPositions) {
            signalType = ORDER_TYPE_SELL;
         }
      }
   }

   if (signalType != (ENUM_ORDER_TYPE)-1) {
      //--- Create new basket with unique magic number
      int newMagic = m_baseMagicNumber + ArraySize(m_traders);
      if (newMagic < m_baseMagicNumber + m_maxInitialPositions) {
         MarketZoneTrader* newTrader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints, newMagic);
         ticket = newTrader.openInitialOrder(signalType); //--- Open INITIAL position
         if (ticket > 0 && newTrader.activateTrade(ticket)) {
            int size = ArraySize(m_traders);
            ArrayResize(m_traders, size + 1);
            m_traders[size] = newTrader;
            Print("New basket created: Magic=", newMagic, ", Ticket=", ticket, ", Type=", EnumToString(signalType));
         } else {
            delete newTrader;
            Print("Failed to create new basket: Ticket=", ticket);
         }
      } else {
         Print("Maximum initial positions (baskets) reached: ", m_maxInitialPositions);
      }
   }
}

En la función, comenzamos recorriendo el array «m_traders» mediante la función ArraySize y, para cada instancia de «MarketZoneTrader» que no sea nula, llamamos a su función «processTick», pasando «m_rsiBuffer», «m_envUpperBandBuffer» y «m_envLowerBandBuffer» para gestionar la lógica de cada cesta individual. Esto difiere de la versión anterior, en la que «processTick» gestionaba directamente un único ciclo de negociación. A continuación, llamamos a la función «cleanupTerminatedBaskets» para eliminar las cestas inactivas, lo que garantiza un uso eficiente de los recursos. A continuación, comprobamos si hay nuevas señales de negociación únicamente en una nueva barra mediante «isNewBar», y salimos si el valor es falso para ahorrar recursos.

Cargamos los datos de los indicadores con CopyBuffer para «m_handleRsi», «m_handleEnvUpper» y «m_handleEnvLower» en sus respectivos búferes, registrando los errores con «Print» y saliendo del programa si se produce algún fallo, a diferencia de la versión anterior, en la que esto se realizaba en «MarketZoneTrader». Establecemos «rsiOverbought» en 70 y «rsiOversold» en 30, e inicializamos «ticket» y «signalType». Obtenemos «askPrice» y «bidPrice» utilizando SymbolInfoDouble con «SYMBOL_ASK» y SYMBOL_BID, normalizados con la función NormalizeDouble.

Para una señal de compra, si «m_rsiBuffer» indica una situación de sobreventa y «askPrice» supera «m_envUpperBandBuffer», establecemos «signalType» en ORDER_TYPE_BUY si «countActiveBaskets» es inferior a «m_maxInitialPositions». Para una señal de venta, si «m_rsiBuffer» muestra condiciones de sobrecompra y «bidPrice» está por debajo de «m_envLowerBandBuffer», establecemos «signalType» en ORDER_TYPE_SELL. Si existe un «signalType» válido, creamos un número mágico único sumando «m_baseMagicNumber» a «ArraySize(m_traders)», y si el resultado está dentro de «m_maxInitialPositions», instanciamos un nuevo «MarketZoneTrader» con los parámetros de entrada y el nuevo número mágico.

Llamamos a «openInitialOrder» con «signalType» y, si el «ticket» devuelto es válido y «activateTrade» se completa correctamente, añadimos al nuevo trader a «m_traders» utilizando ArrayResize y registramos el éxito con «Print» y la función EnumToString. De lo contrario, eliminamos al operador y registramos el fallo, o bien indicamos si se ha alcanzado el límite de la cesta. Una vez abiertas las nuevas operaciones, tendremos que crear nuevas cestas para ellas. Esta es la lógica que seguimos para lograrlo.

private:
   void createNewBasket(long magic, ulong ticket) {
      MarketZoneTrader* newTrader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints, magic);
      if (newTrader.activateTrade(ticket)) {
         int size = ArraySize(m_traders);
         ArrayResize(m_traders, size + 1);
         m_traders[size] = newTrader;
         Print("Existing position loaded into basket: Magic=", magic, ", Ticket=", ticket);
      } else {
         delete newTrader;
         Print("Failed to load existing position into basket: Ticket=", ticket);
      }
   }

Implementamos la función «createNewBasket» en la sección privada de la clase «BasketManager», una nueva incorporación destinada a respaldar nuestra mejora en la negociación con cestas múltiples mediante la creación y gestión de nuevas cestas de negociación para las posiciones existentes. Empezamos creando una nueva instancia de «MarketZoneTrader», denominada «newTrader», utilizando los parámetros de entrada «lotOption», «initialLotSize», «riskPercentage», «riskPoints», «zoneTargetPoints», «zoneSizePoints» y el número «mágico» facilitado para configurar una cesta de operaciones única. Recordemos que en la versión anterior incluíamos esta entrada del usuario en la fase de inicialización porque solo necesitábamos una instancia de la zona, por lo que se aplicaba a todas las nuevas posiciones; sin embargo, en este caso, la organizamos en nuevas instancias de clase. Aquí tienes el código correspondiente para que puedas compararlo más rápidamente.

//--- PREVIOUS VERSION OF NEW CLASS INSTANCE
//--- Global Instance
MarketZoneTrader *trader = NULL;                                        //--- Declare trader instance

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
}

A continuación, llamamos a «activateTrade» en «newTrader» con el «ticket» indicado para cargar la posición existente en la cesta. Si la operación se realiza correctamente, obtenemos el tamaño actual del array «m_traders» mediante ArraySize, lo ampliamos en una posición con ArrayResize y añadimos «newTrader» a la nueva posición. Registramos el éxito con «Print», incluyendo los valores «magic» y «ticket». Si «activateTrade» falla, eliminamos «newTrader» para liberar memoria y registramos el error con «Print». Esta función nos permitirá ahora organizar las posiciones existentes en cestas independientes, una característica clave de nuestro sistema de cestas múltiples, a diferencia del enfoque de instancia única de la versión anterior. Esa clase nos permitirá ahora gestionar eficazmente las cestas de operaciones. Pasemos ahora a modificar la clase base para que pueda incluir las nuevas funciones de cestas múltiples y de stop dinámico. Empecemos por sus miembros.

//--- Modified MarketZoneTrader Class
class MarketZoneTrader {
private:
   enum TradeState { INACTIVE, RUNNING, TERMINATING };

   struct TradeMetrics {
      bool   operationSuccess;
      double totalVolume;
      double netProfitLoss;
   };

   struct ZoneBoundaries {
      double zoneHigh;
      double zoneLow;
      double zoneTargetHigh;
      double zoneTargetLow;
   };

   struct TradeConfig {
      string         marketSymbol;
      double         openPrice;
      double         initialVolume;
      long           tradeIdentifier;
      string         initialTradeLabel;  //--- Label for initial positions
      string         recoveryTradeLabel; //--- Label for recovery positions
      ulong          activeTickets[];
      ENUM_ORDER_TYPE direction;
      double         zoneProfitSpan;
      double         zoneRecoverySpan;
      double         accumulatedBuyVolume;
      double         accumulatedSellVolume;
      TradeState     currentState;
      bool           hasRecoveryTrades;  //--- Flag to track recovery trades
      double         trailingStopLevel;  //--- Virtual trailing stop level
   };

   struct LossTracker {
      double tradeLossTracker;
   };

   TradeConfig           m_tradeConfig;
   ZoneBoundaries        m_zoneBounds;
   LossTracker           m_lossTracker;
   string                m_lastError;
   int                   m_errorStatus;
   CTrade                m_tradeExecutor;
   TradingLotSizeOptions m_lotOption;
   double                m_initialLotSize;
   double                m_riskPercentage;
   int                   m_riskPoints;
   double                m_zoneTargetPoints;
   double                m_zoneSizePoints;
}

En este punto, mejoramos nuestro programa modificando la clase «MarketZoneTrader», concretamente su sección privada, para incluir nuevas funciones que permiten utilizar stops dinámicos y mejorar el etiquetado de las operaciones. Mantenemos la estructura básica, pero introducimos cambios fundamentales en la estructura de «TradeConfig» para adaptarla a nuestra estrategia mejorada. Mantenemos la enumeración «TradeState» con los estados «INACTIVE», «RUNNING» y «TERMINATING», así como las estructuras «TradeMetrics», «ZoneBoundaries» y «LossTracker» sin cambios respecto a la versión anterior, ya que siguen gestionando los estados de las operaciones, las métricas de rendimiento, los límites de las zonas y el seguimiento de pérdidas.

En la estructura «TradeConfig», añadimos dos nuevas variables de tipo cadena: «initialTradeLabel» y «recoveryTradeLabel». Estas etiquetas nos permiten clasificar por separado las operaciones iniciales y las de recuperación, lo que mejora la identificación y el seguimiento de las operaciones dentro de cada cesta, algo especialmente útil para gestionar varias cestas en nuestro nuevo sistema. También introducimos «hasRecoveryTrades», un valor booleano que permite determinar si una cesta incluye operaciones de recuperación, lo cual es fundamental para activar o desactivar adecuadamente los stops dinámicos. Además, añadimos «trailingStopLevel», un valor de tipo double para almacenar el nivel de stop dinámico para cada cesta, lo que permite proteger de forma dinámica los beneficios de las operaciones iniciales.

Entre las variables miembro, conservamos «m_tradeConfig», «m_zoneBounds», «m_lossTracker», «m_lastError», «m_errorStatus», «m_tradeExecutor», «m_lotOption», «m_initialLotSize», «m_riskPercentage», «m_riskPoints», «m_zoneTargetPoints» y «m_zoneSizePoints» tal y como estaban, pero sus funciones ahora admiten las nuevas funcionalidades de trailing stop y cestas múltiples dentro de cada instancia de «MarketZoneTrader». Cabe destacar que eliminamos de la clase las variables relacionadas con los indicadores, como «m_handleRsi» y «m_rsiBuffer», ya que ahora son gestionadas de forma centralizada por la clase «BasketManager», lo que permite que cada operador se centre en las operaciones individuales de su cesta. En el constructor y el destructor, tendremos que modificar ligeramente algunas variables para que se adapten a las nuevas funcionalidades.

public:
   MarketZoneTrader(TradingLotSizeOptions lotOpt, double initLot, double riskPct, int riskPts, double targetPts, double sizePts, long magic) {
      m_tradeConfig.currentState = INACTIVE;
      ArrayResize(m_tradeConfig.activeTickets, 0);
      m_tradeConfig.zoneProfitSpan = targetPts * _Point;
      m_tradeConfig.zoneRecoverySpan = sizePts * _Point;
      m_lossTracker.tradeLossTracker = 0.0;
      m_lotOption = lotOpt;
      m_initialLotSize = initLot;
      m_riskPercentage = riskPct;
      m_riskPoints = riskPts;
      m_zoneTargetPoints = targetPts;
      m_zoneSizePoints = sizePts;
      m_tradeConfig.marketSymbol = _Symbol;
      m_tradeConfig.tradeIdentifier = magic;
      m_tradeConfig.initialTradeLabel = "EA_INITIAL_" + IntegerToString(magic); //--- Label for initial positions
      m_tradeConfig.recoveryTradeLabel = "EA_RECOVERY_" + IntegerToString(magic); //--- Label for recovery positions
      m_tradeConfig.hasRecoveryTrades = false; //--- Initialize recovery flag
      m_tradeConfig.trailingStopLevel = 0.0; //--- Initialize trailing stop
      m_tradeExecutor.SetExpertMagicNumber(magic);
   }

   ~MarketZoneTrader() {
      ArrayFree(m_tradeConfig.activeTickets);
   }

Empezamos con el constructor «MarketZoneTrader», que ahora admite un parámetro «magic» adicional para asignar un número mágico único a cada cesta de operaciones, a diferencia de la versión anterior, que utilizaba un número mágico fijo. Para facilitar una mejor identificación de las operaciones, añadimos «m_tradeConfig.initialTradeLabel» como «EA_INITIAL» más «magic» (mediante IntegerToString) y «m_tradeConfig.recoveryTradeLabel» como «EA_RECOVERY» más «magic», lo que permite identificar claramente las operaciones iniciales y las de recuperación dentro de una cesta. Inicializamos «m_tradeConfig.hasRecoveryTrades» en «false» para realizar un seguimiento del estado de las operaciones de recuperación y establecemos «m_tradeConfig.trailingStopLevel» en 0,0 para el trailing stop virtual; ambas son nuevas funciones. Por último, configuramos «m_tradeExecutor» con «SetExpertMagicNumber» utilizando «magic». Hemos resaltado los cambios más importantes para que se puedan identificar rápidamente.

A continuación, simplificamos el destructor «~MarketZoneTrader» en comparación con la versión anterior, que se denominaba «cleanup». Ahora solo liberamos «m_tradeConfig.activeTickets» con ArrayFree, ya que la limpieza de los indicadores la gestiona «BasketManager», lo que reduce el ámbito del destructor para centrarse en los recursos específicos de la cesta. A continuación, podemos actualizar la función encargada de activar las operaciones para que pueda inicializar el nivel de trailing stop y el estado de recuperación de las operaciones iniciales.

bool activateTrade(ulong ticket) {

   m_tradeConfig.hasRecoveryTrades = false;
   m_tradeConfig.trailingStopLevel = 0.0;
   
   //--- THE REST OF THE LOGIC REMAINS
   
   return true;
}

Aquí solo añadimos la lógica para inicializar el nivel del trailing stop de la primera operación en 0 y el estado de recuperación en «false», para indicar que se trata de la primera posición de la cesta. Por último, podemos añadir una función para abrir la posición inicial.

int openInitialOrder(ENUM_ORDER_TYPE orderType) {
   //--- Open INITIAL position based on signal
   int ticket;
   double openPrice;
   if (orderType == ORDER_TYPE_BUY) {
      openPrice = NormalizeDouble(getMarketAsk(), Digits());
   } else if (orderType == ORDER_TYPE_SELL) {
      openPrice = NormalizeDouble(getMarketBid(), Digits());
   } else {
      Print("Invalid order type [Magic=", m_tradeConfig.tradeIdentifier, "]");
      return -1;
   }
   double lotSize = 0;
   if (m_lotOption == FIXED_LOTSIZE) {
      lotSize = m_initialLotSize;
   } else if (m_lotOption == UNFIXED_LOTSIZE) {
      lotSize = calculateLotSize(m_riskPercentage, m_riskPoints);
   }
   if (lotSize <= 0) {
      Print("Invalid lot size [Magic=", m_tradeConfig.tradeIdentifier, "]: ", lotSize);
      return -1;
   }
   if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, orderType, lotSize, openPrice, 0, 0, m_tradeConfig.initialTradeLabel)) {
      ticket = (int)m_tradeExecutor.ResultOrder();
      Print("INITIAL trade opened [Magic=", m_tradeConfig.tradeIdentifier, "]: Ticket=", ticket, ", Type=", EnumToString(orderType), ", Volume=", lotSize);
   } else {
      ticket = -1;
      Print("Failed to open INITIAL order [Magic=", m_tradeConfig.tradeIdentifier, "]: Type=", EnumToString(orderType), ", Volume=", lotSize);
   }
   return ticket;
}

Implementamos una nueva función «openInitialOrder» en la sección pública de la clase «MarketZoneTrader» para dar soporte a nuestras mejoras en materia de cestas múltiples y etiquetado de operaciones, mediante la apertura de posiciones iniciales para una cesta de operaciones específica con una identificación distintiva. Empezamos inicializando «ticket» y «openPrice». Para «orderType» establecido en ORDER_TYPE_BUY, fijamos «openPrice» mediante «getMarketAsk» y lo normalizamos con NormalizeDouble y «Digits». Para «ORDER_TYPE_SELL», utilizamos «getMarketBid». Si «orderType» no es válido, registramos un error con «Print», incluyendo «m_tradeConfig.tradeIdentifier», 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 mediante «m_tradeExecutor.PositionOpen» con «m_tradeConfig.marketSymbol», «orderType», «lotSize», «openPrice» y «m_tradeConfig.initialTradeLabel» para identificar claramente las operaciones iniciales. Si la operación se realiza correctamente, establecemos «ticket» con «ResultOrder» y registramos la operación con «Print», incluyendo «m_tradeConfig.tradeIdentifier» y la función EnumToString. Si se produce un error, asignamos el valor -1 a «ticket» y registramos el error. Por último, devolvemos el «ticket». A diferencia de la función «openOrder» de la versión anterior, esta función utiliza el nuevo parámetro «initialTradeLabel» y se centra exclusivamente en las posiciones iniciales, en consonancia con nuestro sistema de cestas múltiples. Tras la compilación, obtenemos el siguiente resultado.

CESTA INICIAL

En la imagen se puede observar que podemos abrir la operación inicial y crear una nueva instancia de cesta para ella. Ahora necesitamos una lógica de trailing para poder gestionar la función de trailing stop de las posiciones.

void evaluateMarketTick() {
   if (m_tradeConfig.currentState == INACTIVE) return;
   if (m_tradeConfig.currentState == TERMINATING) {
      finalizePosition();
      return;
   }
   double currentPrice;
   double profitPoints = 0.0;

   //--- Handle BUY initial position
   if (m_tradeConfig.direction == ORDER_TYPE_BUY) {
      currentPrice = getMarketBid();
      profitPoints = (currentPrice - m_tradeConfig.openPrice) / _Point;

      //--- Trailing Stop Logic for Initial Position
      if (enableInitialTrailing && !m_tradeConfig.hasRecoveryTrades && profitPoints >= minProfitPoints) {
         //--- Calculate desired trailing stop level
         double newTrailingStop = currentPrice - trailingStopPoints * _Point;
         //--- Start or update trailing stop if profit exceeds minProfitPoints + trailingStopPoints
         if (profitPoints >= minProfitPoints + trailingStopPoints) {
            if (m_tradeConfig.trailingStopLevel == 0.0 || newTrailingStop > m_tradeConfig.trailingStopLevel) {
               m_tradeConfig.trailingStopLevel = newTrailingStop;
               Print("Trailing stop updated [Magic=", m_tradeConfig.tradeIdentifier, "]: Level=", m_tradeConfig.trailingStopLevel, ", Profit=", profitPoints, " points");
            }
         }
         //--- Check if price has hit trailing stop
         if (m_tradeConfig.trailingStopLevel > 0.0 && currentPrice <= m_tradeConfig.trailingStopLevel) {
            Print("Trailing stop triggered [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " <= TrailingStop=", m_tradeConfig.trailingStopLevel);
            finalizePosition();
            return;
         }
      }

      //--- Zone Recovery Logic
      if (currentPrice > m_zoneBounds.zoneTargetHigh) {
         Print("Closing position [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh);
         finalizePosition();
         return;
      } else if (currentPrice < m_zoneBounds.zoneLow) {
         Print("Triggering RECOVERY trade [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow);
         triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice);
      }
   }
   //--- Handle SELL initial position
   else if (m_tradeConfig.direction == ORDER_TYPE_SELL) {
      currentPrice = getMarketAsk();
      profitPoints = (m_tradeConfig.openPrice - currentPrice) / _Point;

      //--- Trailing Stop Logic for Initial Position
      if (enableInitialTrailing && !m_tradeConfig.hasRecoveryTrades && profitPoints >= minProfitPoints) {
         //--- Calculate desired trailing stop level
         double newTrailingStop = currentPrice + trailingStopPoints * _Point;
         //--- Start or update trailing stop if profit exceeds minProfitPoints + trailingStopPoints
         if (profitPoints >= minProfitPoints + trailingStopPoints) {
            if (m_tradeConfig.trailingStopLevel == 0.0 || newTrailingStop < m_tradeConfig.trailingStopLevel) {
               m_tradeConfig.trailingStopLevel = newTrailingStop;
               Print("Trailing stop updated [Magic=", m_tradeConfig.tradeIdentifier, "]: Level=", m_tradeConfig.trailingStopLevel, ", Profit=", profitPoints, " points");
            }
         }
         //--- Check if price has hit trailing stop
         if (m_tradeConfig.trailingStopLevel > 0.0 && currentPrice >= m_tradeConfig.trailingStopLevel) {
            Print("Trailing stop triggered [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " >= TrailingStop=", m_tradeConfig.trailingStopLevel);
            finalizePosition();
            return;
         }
      }

      //--- Zone Recovery Logic
      if (currentPrice < m_zoneBounds.zoneTargetLow) {
         Print("Closing position [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " < TargetLow=", m_zoneBounds.zoneTargetLow);
         finalizePosition();
         return;
      } else if (currentPrice > m_zoneBounds.zoneHigh) {
         Print("Triggering RECOVERY trade [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " > ZoneHigh=", m_zoneBounds.zoneHigh);
         triggerRecoveryTrade(ORDER_TYPE_BUY, currentPrice);
      }
   }
}

En este caso, mejoramos el programa actualizando la función «evaluateMarketTick» para incorporar la lógica del stop dinámico, al tiempo que mantenemos la lógica de recuperación de zona ya existente. Empezamos comprobando si «m_tradeConfig.currentState» es «INACTIVE» o «TERMINATING», y, según el caso, salimos o llamamos a «finalizePosition», como antes. Para una posición de compra («m_tradeConfig.direction» como ORDER_TYPE_BUY), obtenemos «currentPrice» con «getMarketBid» y calculamos «profitPoints» como la diferencia entre «currentPrice» y «m_tradeConfig.openPrice» dividida por «_Point». La nueva lógica del trailing stop comprueba si «enableInitialTrailing» es verdadero, si «m_tradeConfig.hasRecoveryTrades» es falso y si «profitPoints» es igual o superior a «minProfitPoints». En ese caso, calculamos «newTrailingStop» restando «trailingStopPoints» multiplicado por «_Point» de «currentPrice». Si además «profitPoints» supera «minProfitPoints» más «trailingStopPoints», y «m_tradeConfig.trailingStopLevel» es 0.0 o inferior a «newTrailingStop», actualizamos «m_tradeConfig.trailingStopLevel» y lo registramos con «Print».

Si se ha establecido «m_tradeConfig.trailingStopLevel» y «currentPrice» cae por debajo de ese nivel, registramos el evento y llamamos a «finalizePosition» para cerrar la operación. La lógica de recuperación de la zona no sufre cambios: se cierra la posición si «currentPrice» supera «m_zoneBounds.zoneTargetHigh» o se activa una operación de recuperación de venta con «triggerRecoveryTrade» si cae por debajo de «m_zoneBounds.zoneLow».

Para una posición de venta («m_tradeConfig.direction» como ORDER_TYPE_SELL), obtenemos «currentPrice» mediante «getMarketAsk» y calculamos «profitPoints» de forma inversa. La lógica del trailing stop sigue el mismo patrón que en el caso de la compra: establece «newTrailingStop» sumando «trailingStopPoints» veces _Point a «currentPrice», actualiza «m_tradeConfig.trailingStopLevel» si se cumplen las condiciones y cierra la posición si «currentPrice» lo supera. La lógica de recuperación de zona cierra la posición si «currentPrice» es inferior a «m_zoneBounds.zoneTargetLow» o activa una operación de compra de recuperación si es superior a «m_zoneBounds.zoneHigh». No incluimos un trailing stop físico porque queremos tener un control total sobre el sistema. De esta forma, podemos supervisar y gestionar todas las instancias. A continuación se muestra el resultado tras ejecutar el programa para la función de stop dinámico.

EJEMPLO DE STOP DINÁMICO

En la imagen se puede observar que podemos ajustar la posición y cerrarla cuando el precio retrocede hasta el nivel del trailing stop. Por último, solo tenemos que crear una instancia del gestor de la cesta y utilizarla para su gestión a nivel global.

//--- Global Instance
BasketManager *manager = NULL;

int OnInit() {
   manager = new BasketManager(_Symbol, baseMagicNumber, maxInitialPositions);
   if (!manager.initialize()) {
      delete manager;
      manager = NULL;
      return INIT_FAILED;
   }
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason) {
   if (manager != NULL) {
      delete manager;
      manager = NULL;
      Print("EA deinitialized");
   }
}

void OnTick() {
   if (manager != NULL) {
      manager.processTick();
   }
}

Actualizamos la instancia global y los controladores de eventos para que utilicen la nueva clase «BasketManager», sustituyendo el uso de la clase «MarketZoneTrader» de la versión anterior, con el fin de dar soporte a nuestra mejora en la negociación de cestas múltiples mediante la centralización de la gestión de varias cestas de negociación. Empezamos declarando un puntero global «manager» a la clase «BasketManager», inicializado a «NULL», en lugar del anterior puntero «trader» a «MarketZoneTrader». Este cambio es fundamental, ya que nos permite gestionar varias cestas de operaciones a través de un único gestor, a diferencia del enfoque de instancia única de la versión anterior.

En el controlador de eventos OnInit, creamos una nueva instancia de «BasketManager» para «manager», pasando «_Symbol», «baseMagicNumber» y «maxInitialPositions» para configurarla según el gráfico actual, la identificación única de la cesta y el número máximo de cestas. Llamamos a «manager.initialize» para configurar los indicadores y cargar las posiciones existentes, y si falla, eliminamos «manager», lo establecemos en «NULL» y devolvemos INIT_FAILED. Si la operación se realiza correctamente, devolvemos «INIT_SUCCEEDED».

En el controlador de eventos «OnDeinit», comprobamos si «manager» no es «NULL»; a continuación, lo eliminamos con «delete», lo establecemos en «NULL» y registramos la desinicialización con «Print». En el OnTick, comprobamos si «manager» no es «NULL» y llamamos a «manager.processTick» para gestionar los ticks del mercado en todas las cestas, sustituyendo la llamada anterior a «trader.processTick». Esto centraliza el procesamiento de ticks para múltiples cestas, lo que mejora la capacidad del sistema para gestionar señales de negociación simultáneas. Tras la compilación, obtenemos el siguiente resultado.

OPERACIONES FINALES

En la imagen se puede observar que podemos crear grupos de señales independientes y gestionarlos, con diferentes etiquetas generadas a partir del número mágico facilitado. Lo que queda por hacer es realizar pruebas retrospectivas del programa, y eso se trata en la siguiente sección.


Backtesting

Tras realizar un exhaustivo análisis retrospectivo, hemos obtenido los siguientes resultados.

Gráfico de backtest:

GRÁFICO

Informe de backtest:

INFORME


Conclusión

En conclusión, hemos mejorado nuestro sistema de recuperación de zona para el trading de tendencias con envolventes en MQL5 mediante la introducción de trailing stops y un sistema de trading con múltiples cestas, partiendo de la base establecida en la parte 22 con nuevos componentes como la clase «BasketManager» y las funciones actualizadas de «MarketZoneTrader». Estas mejoras ofrecen un marco de negociación más flexible y sólido que puedes personalizar aún más ajustando parámetros como «trailingStopPoints» o «maxInitialPositions».

Descargo de responsabilidad: Este artículo tiene fines exclusivamente educativos. Operar en los mercados financieros conlleva riesgos financieros significativos, y la volatilidad del mercado puede provocar pérdidas. Es fundamental realizar pruebas retrospectivas exhaustivas y una gestión de riesgos cuidadosa antes de implementar este programa en mercados reales.

Con estas mejoras, puedes perfeccionar este sistema o adaptar su arquitectura para crear nuevas estrategias, impulsando así su trayectoria en el trading algorítmico. ¡Felices operaciones!

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/18778

Tyrone Chan
Tyrone Chan | 31 ago 2025 en 07:20

Señor, no hay operaciones de venta iniciales abiertas.

¿Tiene esto que ver con la lógica de negociación?

Procesos gaussianos en el aprendizaje automático (Parte 2): Implementación y prueba de un modelo de clasificación en MQL5 Procesos gaussianos en el aprendizaje automático (Parte 2): Implementación y prueba de un modelo de clasificación en MQL5
En esta parte analizaremos la implementación de las interfaces clave de la biblioteca de procesos gaussianos en MQL5: IKernel, ILikelihood e IInference. Además, mostraremos su funcionamiento con datos sintéticos y escribiremos indicadores de clasificación y regresión que demuestren su funcionamiento en modo online, con reentrenamiento del modelo en cada nueva barra.
Algoritmo del átomo artificial — Artificial Atom Algorithm (A3) Algoritmo del átomo artificial — Artificial Atom Algorithm (A3)
Implementación del algoritmo A3 en MQL5: un método metaheurístico de optimización inspirado en los procesos químicos. Con solo dos parámetros configurables, la compacidad y una población reducida proporcionan una alta velocidad de funcionamiento con una calidad suficiente de las soluciones.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Características del Wizard MQL5 que debe conocer (Parte 74): Uso de patrones de Ichimoku y del ADX-Wilder con aprendizaje supervisado Características del Wizard MQL5 que debe conocer (Parte 74): Uso de patrones de Ichimoku y del ADX-Wilder con aprendizaje supervisado
Retomamos el tema de nuestro último artículo, donde presentamos el par de indicadores Ichimoku y ADX, analizando cómo se podría mejorar este dúo con el aprendizaje supervisado. Ichimoku y ADX forman una combinación complementaria de soporte/resistencia y tendencia. Nuestro enfoque de aprendizaje supervisado utiliza una red neuronal que emplea el núcleo de mezcla espectral profunda para ajustar con precisión las predicciones de este par de indicadores. Como de costumbre, esto se realiza en un archivo de clase de señal personalizado que funciona con el asistente MQL5 para ensamblar un Asesor Experto.