English Русский 中文 Deutsch 日本語
preview
Automatización de estrategias de trading en MQL5 (Parte 11): Desarrollo de un sistema de negociación de cuadrícula multinivel

Automatización de estrategias de trading en MQL5 (Parte 11): Desarrollo de un sistema de negociación de cuadrícula multinivel

MetaTrader 5Trading |
109 4
Allan Munene Mutiiria
Allan Munene Mutiiria

Introducción

En nuestro artículo anterior (Parte 10), desarrollamos un Asesor Experto para automatizar la estrategia Trend Flat Momentum utilizando una combinación de medias móviles y filtros de momentum en MetaQuotes Language 5 (MQL5). Ahora, en la Parte 11, nos centramos en crear un sistema de trading con cuadrículas multinivel que aprovecha un enfoque de cuadrículas por capas para sacar partido de las fluctuaciones del mercado. Estructuraremos el artículo en torno a los siguientes temas:

  1. Introducción
  2. Comprender la arquitectura de un sistema de cuadrícula multinivel
  3. Implementación en MQL5
  4. Prueba retrospectiva
  5. Conclusión

Al finalizar este artículo, tendrá una comprensión integral y un programa completamente funcional listo para operar en vivo. ¡Vamos a ello!


Comprender la arquitectura de un sistema de cuadrícula multinivel

Un sistema de negociación por cuadrículas multinivel es un enfoque estructurado que aprovecha la volatilidad del mercado colocando una serie de órdenes de compra y venta a intervalos predeterminados en un rango de niveles de precios. La estrategia que estamos a punto de implementar no consiste en predecir la dirección del mercado, sino en sacar provecho del flujo natural de los precios, obteniendo ganancias tanto si el mercado sube como si baja o se mantiene estable.

Basándonos en este concepto, nuestro programa implementará la estrategia de red multinivel mediante un diseño modular que separa la detección de señales, la ejecución de órdenes y la gestión de riesgos. En el desarrollo de nuestro sistema, primero inicializaremos los parámetros clave, como las medias móviles para identificar señales de trading, y configuraremos una estructura de cesta que englobe los detalles de las operaciones, como los tamaños iniciales de los lotes, el espaciado de la cuadrícula y los niveles de take profit.

A medida que el mercado evoluciona, el programa supervisará los movimientos de los precios para activar nuevas operaciones y gestionar las posiciones existentes, añadiendo órdenes en cada nivel de la cuadrícula en función de condiciones predefinidas y ajustando dinámicamente los parámetros de riesgo. La arquitectura también incluirá funciones para recalcular los puntos de equilibrio, modificar los objetivos de toma de ganancias y cerrar posiciones cuando se alcancen los objetivos de ganancias o los umbrales de riesgo. Este plan estructurado no solo organizará el programa en componentes distintos y manejables, sino que también garantizará que cada capa de la red contribuya a una estrategia comercial cohesionada y con gestión de riesgos, lista para una sólida comprobación retrospectiva y una implementación comercial. En pocas palabras, así es como se verá la arquitectura.

ARQUITECTURA DE LA CUADRÍCULA



Implementación en MQL5

Para crear el programa en MQL5, abra MetaEditor, vaya al Navegador, localice la carpeta Indicadores, haga clic en la pestaña «Nuevo» y siga las instrucciones para crear el archivo. Una vez creado, en el entorno de programación, tendremos que declarar algunos metadatos y variables globales que utilizaremos a lo largo del programa.

//+------------------------------------------------------------------+
//|                        Copyright 2025, Forex Algo-Trader, Allan. |
//|                                 "https://t.me/Forex_Algo_Trader" |
//+------------------------------------------------------------------+
#property copyright "Forex Algo-Trader, Allan"
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property description "This EA trades multiple signals with grid strategy using baskets"
#property strict

#include <Trade/Trade.mqh> //--- Includes the standard trading library for executing trades
CTrade obj_Trade; //--- Instantiates the CTrade object used for managing trade operations

//--- Closure Mode Enumeration and Inputs
enum ClosureMode {
   CLOSE_BY_PROFIT,      //--- Use total profit (in currency) to close positions
   CLOSE_BY_POINTS       //--- Use points threshold from breakeven to close positions
};

input group "General EA Settings"
input ClosureMode closureMode = CLOSE_BY_POINTS;
input double inpLotSize = 0.01;
input long inpMagicNo = 1234567;
input int inpTp_Points = 100;
input int inpGridSize = 100;
input double inpMultiplier = 2.0;
input int inpBreakevenPts = 50;
input int maxBaskets = 5;

input group "MA Indicator Settings" //--- Begins the input group for Moving Average indicator settings
input int inpMAPeriod = 21;                         //--- Period used for the Moving Average calculation

Aquí establecemos los componentes fundamentales de nuestro programa, garantizando una ejecución comercial fluida y una gestión estratégica de las posiciones. Comenzamos incluyendo la biblioteca «Trade/Trade.mqh», que permite acceder a funciones esenciales para la ejecución de operaciones. Para facilitar las operaciones comerciales, instanciamos el objeto «CTrade» como «obj_Trade», lo que nos permite realizar, modificar y cerrar órdenes de manera eficiente dentro de nuestra estrategia automatizada.

Definimos la enumeración «ClosureMode» para proporcionar flexibilidad en la gestión de las salidas de operaciones. El programa puede funcionar en dos modos: «CLOSE_BY_PROFIT», que activa el cierre cuando las ganancias acumuladas totales alcanzan un umbral especificado en la divisa de la cuenta, y «CLOSE_BY_POINTS», que cierra las posiciones en función de una distancia predefinida desde el nivel de equilibrio. Esto garantiza que el usuario pueda ajustar dinámicamente su estrategia de salida en función del comportamiento del mercado y su tolerancia al riesgo.

A continuación, introducimos una sección estructurada input en «Configuración general de EA» para permitir la personalización definida por el usuario de la estrategia de trading. Especificamos «inpLotSize» para controlar el volumen inicial de la operación y utilizamos «inpMagicNo» para identificar de forma única las operaciones del EA, evitando conflictos con otras estrategias activas. Para la ejecución basada en cuadrículas, establecemos «inpTp_Points» para determinar el nivel de take profit por operación, mientras que «inpGridSize» define el espaciado entre órdenes sucesivas de la cuadrícula. Para la ejecución basada en cuadrículas, establecemos «inpTp_Points» para determinar el nivel de take profit por operación, mientras que «inpGridSize» define el espaciado entre órdenes sucesivas de la cuadrícula. Para refinar aún más el control de riesgos, configuramos «inpBreakevenPts», que mueve las operaciones al punto de equilibrio después de un umbral determinado, y «maxBaskets», que limita el número de estructuras de cuadrícula independientes que el EA puede gestionar simultáneamente.

Para mejorar el filtrado de operaciones, incorporamos un mecanismo de media móvil en «MA Indicator Settings». Aquí definimos «inpMAPeriod», que determina el número de períodos utilizados para calcular la media móvil. Esto ayuda a alinear el trading de grid con las tendencias predominantes del mercado, filtrando las condiciones desfavorables y garantizando que las entradas en las operaciones se ajusten al impulso general del mercado. A continuación, dado que tendremos que gestionar muchas instancias de señales, podemos definir una estructura de cesta.

//--- Basket Structure
struct BasketInfo {
   int basketId;            //--- Unique basket identifier (e.g., 1, 2, 3...)
   long magic;              //--- Unique magic number for this basket to differentiate its trades
   int direction;           //--- Direction of the basket: POSITION_TYPE_BUY or POSITION_TYPE_SELL
   double initialLotSize;   //--- The initial lot size assigned to the basket
   double currentLotSize;   //--- The current lot size for subsequent grid trades
   double gridSize;         //--- The next grid level price for the basket
   double takeProfit;       //--- The current take-profit price for the basket
   datetime signalTime;     //--- Timestamp of the signal to avoid duplicate trade entries
};

Aquí definimos la estructura «BasketInfo» para organizar y gestionar cada cesta de la cuadrícula de forma independiente. Asignamos un «basketId» único para realizar un seguimiento de cada cesta y utilizamos «magic» para garantizar que nuestras operaciones se mantengan diferenciadas de las demás. Determinamos la dirección de la operación con «direction», decidiendo si vamos a ejecutar una estrategia de compra o de venta.

Establecemos «initialLotSize» para la primera operación de la cesta, mientras que «currentLotSize» se ajusta dinámicamente para las operaciones posteriores. Utilizamos «gridSize» para establecer el espaciado entre operaciones y «takeProfit» para definir nuestro objetivo de beneficio. Para evitar entradas duplicadas, hacemos un seguimiento de la sincronización de la señal utilizando «signalTime». A continuación, podemos declarar una matriz de almacenamiento utilizando la estructura definida y algunas variables globales iniciales.

BasketInfo baskets[];       //--- Dynamic array to store active basket information
int nextBasketId = 1;       //--- Counter for assigning unique IDs to new baskets
long baseMagic = inpMagicNo;//--- Base magic number obtained from user input
double takeProfitPts = inpTp_Points * _Point; //--- Convert take profit points into price units
double gridSize_Spacing = inpGridSize * _Point; //--- Convert grid size spacing from points into price units
double profitTotal_inCurrency = 100; //--- Target profit in account currency for closing positions

//--- Global Variables
int totalBars = 0;          //--- Stores the total number of bars processed so far
int handle;                 //--- Handle for the Moving Average indicator
double maData[];            //--- Array to store Moving Average indicator data

Utilizamos la matriz dinámica «baskets[]» para almacenar la información de las cestas activas, lo que nos permite realizar un seguimiento eficiente de múltiples posiciones. La variable «nextBasketId» asigna identificadores únicos a cada nueva cesta, mientras que «baseMagic» garantiza que todas las operaciones dentro del sistema sean distinguibles utilizando el número mágico definido por el usuario. Convertimos las entradas del usuario en unidades de precio multiplicando «inpTp_Points» e «inpGridSize» por «_Point», lo que permite un control preciso sobre «takeProfitPts» y «gridSize_Spacing». La variable «profitTotal_inCurrency» define el umbral de beneficio necesario para cerrar todas las posiciones cuando se utiliza un modo de cierre basado en divisas.

Para el análisis técnico, inicializamos «totalBars» para realizar un seguimiento del número de barras de precios procesadas, «handle» para almacenar el indicador de media móvil y «maData[]» como una matriz para almacenar los valores de media móvil calculados. Con eso, podemos definir algunos prototipos de funciones arbitrarias que utilizaremos a lo largo del programa cuando sea necesario.

//--- Function Prototypes
void InitializeBaskets(); //--- Prototype for basket initialization function (if used)
void CheckAndCloseProfitTargets(); //--- Prototype to check and close positions if profit target is reached
void CheckForNewSignal(double ask, double bid); //--- Prototype to check for new trading signals based on price
bool ExecuteInitialTrade(int basketIdx, double ask, double bid, int direction); //--- Prototype to execute the initial trade for a basket
void ManageGridPositions(int basketIdx, double ask, double bid); //--- Prototype to manage and add grid positions for an active basket
void UpdateMovingAverage(); //--- Prototype to update the Moving Average indicator data
bool IsNewBar(); //--- Prototype to check whether a new bar has formed
double CalculateBreakevenPrice(int basketId); //--- Prototype to calculate the weighted breakeven price for a basket
void CheckBreakevenClose(int basketIdx, double ask, double bid); //--- Prototype to check and close positions based on breakeven criteria
void CloseBasketPositions(int basketId); //--- Prototype to close all positions within a basket
string GetPositionComment(int basketId, bool isInitial); //--- Prototype to generate a comment for a position based on basket and trade type
int CountBasketPositions(int basketId); //--- Prototype to count the number of open positions in a basket

Aquí definimos prototipos de funciones que describen las operaciones básicas de nuestro sistema de comercio en cuadrícula multinivel. Estas funciones garantizarán la modularidad, lo que nos permitirá estructurar de manera eficiente la ejecución de operaciones, la gestión de posiciones y el manejo de riesgos. Comenzamos con «InitializeBaskets()», que prepara el sistema para realizar un seguimiento de las cestas activas. La función «CheckAndCloseProfitTargets()» garantiza que las posiciones se cierren una vez que se cumplan las condiciones de beneficio predefinidas. Para detectar oportunidades comerciales, «CheckForNewSignal()» evalúa los niveles de precios para determinar si se debe ejecutar una nueva señal comercial.

La función «ExecuteInitialTrade()» gestionará la primera operación dentro de una cesta, mientras que «ManageGridPositions()» garantizará que los niveles de la cuadrícula se amplíen sistemáticamente a medida que se mueve el mercado. «UpdateMovingAverage()» recupera y procesa los datos del indicador de media móvil para facilitar la generación de señales. Para la gestión comercial, «IsNewBar()» ayuda a optimizar la ejecución al garantizar que las acciones solo se realicen sobre datos de precios recientes. «CalculateBreakevenPrice()» calcula el precio de equilibrio ponderado para una cesta, mientras que «CheckBreakevenClose()» determina si se cumplen las condiciones para salir de las posiciones basándose en criterios de equilibrio.

Para gestionar las posiciones de la cesta, «CloseBasketPositions()» facilita las salidas controladas, garantizando que todas las posiciones dentro de una cesta se cierren cuando sea necesario. «GetPositionComment()» proporciona anotaciones estructuradas sobre las operaciones, lo que mejora el seguimiento de las mismas, y «CountBasketPositions()» ayuda a supervisar el número de posiciones activas dentro de una cesta, lo que garantiza que el sistema funcione dentro de los límites de riesgo definidos.

Ahora podemos comenzar inicializando la media móvil, ya que la utilizaremos únicamente para la generación de señales.

//+------------------------------------------------------------------+
//--- Expert initialization function
//+------------------------------------------------------------------+
int OnInit() {
   handle = iMA(_Symbol, _Period, inpMAPeriod, 0, MODE_SMA, PRICE_CLOSE); //--- Initialize the Moving Average indicator with specified period and parameters
   if(handle == INVALID_HANDLE) {
      Print("ERROR: Unable to initialize Moving Average indicator!"); //--- Log error if indicator initialization fails
      return(INIT_FAILED); //--- Terminate initialization with a failure code
   }
   ArraySetAsSeries(maData, true); //--- Set the moving average data array as a time series (newest data at index 0)
   ArrayResize(baskets, 0); //--- Initialize the baskets array as empty at startup
   obj_Trade.SetExpertMagicNumber(baseMagic); //--- Set the default magic number for trade operations
   return(INIT_SUCCEEDED); //--- Signal that initialization completed successfully
}

En el controlador de eventos OnInit, comenzamos inicializando el indicador de media móvil utilizando la función iMA(), donde aplicamos el período y los parámetros especificados para recuperar datos basados en tendencias. Si el identificador no es válido (INVALID_HANDLE), registramos un mensaje de error y terminamos el proceso de inicialización con INIT_FAILED para evitar que el EA se ejecute con datos faltantes.

A continuación, configuramos la matriz de datos de la media móvil utilizando la función ArraySetAsSeries, asegurándonos de que los valores más recientes se almacenen en el índice 0 para un acceso eficiente. A continuación, redimensionamos la matriz «baskets» a cero, preparándola para la asignación dinámica a medida que se abren nuevas operaciones. Por último, asignamos el número mágico base al objeto de negociación utilizando el método «SetExpertMagicNumber()», lo que permite al EA realizar un seguimiento y gestionar las operaciones con un identificador único. Si todos los componentes se inicializan correctamente, devolvemos INIT_SUCCEEDED para confirmar que el EA está listo para comenzar la ejecución.

Dado que hemos almacenado datos, podemos liberar los recursos cuando ya no necesitemos el programa en el controlador de eventos OnDeinit, llamando a la función IndicatorRelease.

//+------------------------------------------------------------------+
//--- Expert deinitialization function
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   IndicatorRelease(handle); //--- Release the indicator handle to free up resources when the EA is removed
}

A continuación, podemos proceder a procesar los datos en cada tick en el controlador de eventos OnTick. Sin embargo, queremos ejecutar el programa una vez por barra, por lo que tendremos que definir una función para ello.

//+------------------------------------------------------------------+
//--- Expert tick function
//+------------------------------------------------------------------+
void OnTick() {
   if(IsNewBar()) { //--- Execute logic only when a new bar is detected

   }
}

El prototipo de la función es el siguiente.

//+------------------------------------------------------------------+
//--- Check for New Bar
//+------------------------------------------------------------------+
bool IsNewBar() {
   int bars = iBars(_Symbol, _Period); //--- Get the current number of bars on the chart for the symbol and period
   if(bars > totalBars) { //--- Compare the current number of bars with the previously stored total
      totalBars = bars; //--- Update the stored bar count to the new value
      return true; //--- Return true to indicate a new bar has formed
   }
   return false; //--- Return false if no new bar has been detected
}

Aquí definimos la función «IsNewBar()», que comprueba si se ha formado una nueva barra en el gráfico, lo cual es esencial para garantizar que nuestro EA procese los nuevos datos de precios solo cuando aparezca una barra nueva, evitando así recálculos innecesarios. Comenzamos recuperando el número actual de barras en el gráfico utilizando la función iBars, que proporciona el recuento total de barras históricas para el símbolo y el marco temporal activos. A continuación, comparamos este valor con la variable «totalBars», que almacena el recuento de barras registrado anteriormente.

Si el recuento actual de barras es mayor que el valor almacenado en la variable «totalBars», significa que ha aparecido una nueva barra. En este caso, actualizamos la variable «totalBars» utilizando el nuevo recuento y devolvemos «true», lo que indica que el EA debe continuar con los cálculos basados en barras o la lógica de negociación. Si no se detecta ninguna barra nueva, la función devuelve «false», lo que garantiza que el EA no realice operaciones redundantes en la misma barra.

Ahora, una vez que detectamos una nueva barra, necesitamos recuperar los datos de la media móvil para su posterior procesamiento. Para ello, utilizamos una función.

//+------------------------------------------------------------------+
//--- Update Moving Average
//+------------------------------------------------------------------+
void UpdateMovingAverage() {
   if(CopyBuffer(handle, 0, 1, 3, maData) < 0) { //--- Copy the latest 3 values from the Moving Average indicator buffer into the maData array
      Print("Error: Unable to update Moving Average data."); //--- Log an error if copying the indicator data fails
   }
}

Para la función «UpdateMovingAverage()», que garantiza que nuestro EA recupere los últimos valores del indicador de media móvil, utilizamos la función CopyBuffer() para extraer los tres valores más recientes del búfer del indicador de media móvil y almacenarlos en la matriz «maData». Los parámetros especifican el identificador del indicador («handle»), el índice del búfer (0 para la línea principal), la posición inicial (1 para omitir la barra de formación actual), el número de valores (3) y la matriz de destino («maData»).

Si no conseguimos recuperar los datos, registramos un mensaje de error utilizando la función Print() para alertarnos de posibles problemas con la recuperación de datos del indicador, protegiendo al EA contra valores de media móvil incompletos o faltantes y garantizando la fiabilidad en la toma de decisiones. A continuación, podemos llamar a la función y utilizar los datos recuperados para generar la señal.

UpdateMovingAverage(); //--- Update the Moving Average data for the current bar
double ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Get and normalize the current ask price
double bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Get and normalize the current bid price

//--- Check for new signals and create baskets accordingly
CheckForNewSignal(ask, bid);

Después de recuperar los datos del indicador, recuperamos los precios de compra y venta actuales utilizando la función SymbolInfoDouble() con las constantes SYMBOL_ASK y SYMBOL_BID, respectivamente. Dado que los valores de los precios suelen tener varios decimales, utilizamos la función NormalizeDouble con el parámetro _Digits para garantizar que se formateen correctamente según la precisión del precio del símbolo.

Por último, llamamos a la función «CheckForNewSignal()», pasando los precios normalizados de compra y venta. Aquí está el fragmento de código de la función.

//+------------------------------------------------------------------+
//--- Check for New Crossover Signal
//+------------------------------------------------------------------+
void CheckForNewSignal(double ask, double bid) {
   double close1 = iClose(_Symbol, _Period, 1); //--- Retrieve the close price of the previous bar
   double close2 = iClose(_Symbol, _Period, 2); //--- Retrieve the close price of the bar before the previous one
   datetime currentBarTime = iTime(_Symbol, _Period, 1); //--- Get the time of the current bar

   if(ArraySize(baskets) >= maxBaskets) return; //--- Exit if the maximum allowed baskets are already active

   //--- Buy signal: current bar closes above the MA while the previous closed below it
   if(close1 > maData[1] && close2 < maData[1]) {
      //--- Check if this signal was already processed by comparing signal times in existing baskets
      for(int i = 0; i < ArraySize(baskets); i++) {
         if(baskets[i].signalTime == currentBarTime) return; //--- Signal already acted upon; exit the function
      }
      int basketIdx = ArraySize(baskets); //--- Index for the new basket equals the current array size
      ArrayResize(baskets, basketIdx + 1); //--- Increase the size of the baskets array to add a new basket
      if (ExecuteInitialTrade(basketIdx, ask, bid, POSITION_TYPE_BUY)){
         baskets[basketIdx].signalTime = currentBarTime; //--- Record the time of the signal after a successful trade
      }
   }
   //--- Sell signal: current bar closes below the MA while the previous closed above it
   else if(close1 < maData[1] && close2 > maData[1]) {
      //--- Check for duplicate signals by verifying the signal time in active baskets
      for(int i = 0; i < ArraySize(baskets); i++) {
         if(baskets[i].signalTime == currentBarTime) return; //--- Signal already acted upon; exit the function
      }
      int basketIdx = ArraySize(baskets); //--- Determine the index for the new basket
      ArrayResize(baskets, basketIdx + 1); //--- Resize the baskets array to accommodate the new basket
      if (ExecuteInitialTrade(basketIdx, ask, bid, POSITION_TYPE_SELL)){
         baskets[basketIdx].signalTime = currentBarTime; //--- Record the signal time for the new sell basket
      }
   }
}

Para la función «CheckForNewSignal()», primero recuperamos los precios de cierre de las dos barras anteriores utilizando la función iClose(). Esto nos ayuda a determinar si se ha producido un cruce. También utilizamos la función iTime() para obtener la marca de tiempo de la barra más reciente, lo que garantiza que no procesemos la misma señal varias veces.

Antes de continuar, comprobamos si el número de cestas activas ha alcanzado el límite «maxBaskets». Si es así, la función vuelve a funcionar para evitar una acumulación excesiva de operaciones. Para las señales de compra, comprobamos si el precio de cierre más reciente está por encima de la media móvil, mientras que el precio de cierre anterior estaba por debajo de ella. Si se cumple esta condición cruzada, iteramos a través de las cestas existentes para asegurarnos de que la misma señal no se haya procesado ya. Si la señal es nueva, aumentamos el tamaño de la matriz «baskets», almacenamos la nueva cesta en el siguiente índice disponible y llamamos a la función «ExecuteInitialTrade()» con una orden POSITION_TYPE_BUY. Si la operación se ejecuta correctamente, registramos la hora de la señal para evitar entradas duplicadas.

Del mismo modo, para las señales de venta, comprobamos si el precio de cierre más reciente está por debajo de la media móvil, mientras que el precio de cierre anterior estaba por encima de ella. Si se cumple esta condición y no se encuentra ninguna señal duplicada, ampliamos la matriz «baskets», ejecutamos una operación de venta inicial utilizando la función «ExecuteInitialTrade()» con una orden POSITION_TYPE_SELL y almacenamos la hora de la señal para mantener la unicidad. La función para ejecutar las operaciones es la siguiente.

//+------------------------------------------------------------------+
//--- Execute Initial Trade
//+------------------------------------------------------------------+
bool ExecuteInitialTrade(int basketIdx, double ask, double bid, int direction) {
   baskets[basketIdx].basketId = nextBasketId++; //--- Assign a unique basket ID and increment the counter
   baskets[basketIdx].magic = baseMagic + baskets[basketIdx].basketId * 10000; //--- Calculate a unique magic number for the basket
   baskets[basketIdx].initialLotSize = inpLotSize; //--- Set the initial lot size for the basket from input
   baskets[basketIdx].currentLotSize = inpLotSize; //--- Initialize current lot size to the same as the initial lot size
   baskets[basketIdx].direction = direction; //--- Set the trade direction (buy or sell) for the basket
   bool isTradeExecuted = false; //--- Initialize flag to track if the trade was successfully executed
   string comment = GetPositionComment(baskets[basketIdx].basketId, true); //--- Generate a comment string indicating an initial trade
   obj_Trade.SetExpertMagicNumber(baskets[basketIdx].magic); //--- Set the trade object's magic number to the basket's unique value

   if(direction == POSITION_TYPE_BUY) {
      baskets[basketIdx].gridSize = ask - gridSize_Spacing; //--- Set the grid level for subsequent buy orders below the current ask price
      baskets[basketIdx].takeProfit = ask + takeProfitPts; //--- Calculate the take profit level for the buy order
      if(obj_Trade.Buy(baskets[basketIdx].currentLotSize, _Symbol, ask, 0, baskets[basketIdx].takeProfit, comment)) {
         Print("Basket ", baskets[basketIdx].basketId, ": Initial BUY at ", ask, " | Magic: ", baskets[basketIdx].magic); //--- Log the successful buy order details
         isTradeExecuted = true; //--- Mark the trade as executed successfully
      } else {
         Print("Basket ", baskets[basketIdx].basketId, ": Initial BUY failed, error: ", GetLastError()); //--- Log the error if the buy order fails
         ArrayResize(baskets, ArraySize(baskets) - 1); //--- Remove the basket if trade execution fails
      }
   } else if(direction == POSITION_TYPE_SELL) {
      baskets[basketIdx].gridSize = bid + gridSize_Spacing; //--- Set the grid level for subsequent sell orders above the current bid price
      baskets[basketIdx].takeProfit = bid - takeProfitPts; //--- Calculate the take profit level for the sell order
      if(obj_Trade.Sell(baskets[basketIdx].currentLotSize, _Symbol, bid, 0, baskets[basketIdx].takeProfit, comment)) {
         Print("Basket ", baskets[basketIdx].basketId, ": Initial SELL at ", bid, " | Magic: ", baskets[basketIdx].magic); //--- Log the successful sell order details
         isTradeExecuted = true; //--- Mark the trade as executed successfully
      } else {
         Print("Basket ", baskets[basketIdx].basketId, ": Initial SELL failed, error: ", GetLastError()); //--- Log the error if the sell order fails
         ArrayResize(baskets, ArraySize(baskets) - 1); //--- Remove the basket if trade execution fails
      }
   }
   return (isTradeExecuted); //--- Return the status of the trade execution
}

Definimos la función «ExecuteInitialTrade()» para garantizar que cada cesta tenga un identificador único, asignar un número mágico distinto e inicializar los parámetros clave de negociación antes de realizar la orden. En primer lugar, asignamos un «basketId» incrementando la variable «nextBasketId». A continuación, generamos un número mágico único para la cesta añadiendo un desplazamiento escalado al valor «baseMagic», lo que garantiza que cada cesta funcione de forma independiente. Los tamaños inicial y actual de los lotes se establecen en «inpLotSize» para determinar el tamaño básico de las operaciones de esta cesta. La «dirección» se almacena para diferenciar entre cestas de compra y venta.

Para garantizar que las operaciones sean identificables, llamamos a la función «GetPositionComment()» para generar un comentario descriptivo y aplicamos el número mágico de la cesta al objeto de la operación utilizando el método «SetExpertMagicNumber()». La función se define a continuación, donde utilizamos la función StringFormat para obtener el comentario mediante un operador ternario.

//+------------------------------------------------------------------+
//--- Generate Position Comment
//+------------------------------------------------------------------+
string GetPositionComment(int basketId, bool isInitial) {
   return StringFormat("Basket_%d_%s", basketId, isInitial ? "Initial" : "Grid"); //--- Generate a standardized comment string for a position indicating basket ID and trade type
}

Si la dirección es POSITION_TYPE_BUY, calculamos el nivel de la cuadrícula restando «gridSize_Spacing» del precio de venta y determinamos el nivel de take profit sumando «takeProfitPts» al precio de venta. A continuación, utilizamos la función «Buy()» de la clase «CTrade» para realizar la orden. Si tiene éxito, registramos los detalles de la operación utilizando la función Print() y marcamos la operación como ejecutada. Si la operación falla, registramos el error utilizando la función GetLastError() y utilizamos la función ArrayResize() para reducir el tamaño de la matriz «baskets», eliminando la cesta fallida.

Para una operación de venta (POSITION_TYPE_SELL), calculamos el nivel de la cuadrícula añadiendo «gridSize_Spacing» al precio de compra y determinamos el nivel de take profit restando «takeProfitPts» del precio de compra. La operación se ejecuta utilizando la función «Sell()». Al igual que con las operaciones de compra, la ejecución correcta se registra mediante la función «Print()», y el fallo da lugar a un registro de errores con GetLastError, seguido del redimensionamiento de la matriz «baskets» mediante «ArrayResize()» para eliminar la cesta fallida.

Antes de ejecutar cualquier operación, la función se asegura de que la matriz tenga suficiente espacio llamando a «ArrayResize()» para aumentar su tamaño. Por último, la función devuelve «true» si la operación se ha ejecutado correctamente y «false» en caso contrario. Al ejecutar el programa, obtenemos el siguiente resultado.

POSICIONES INICIALES CONFIRMADAS EN LAS CESTA

En la imagen podemos ver que hemos confirmado las posiciones iniciales según las cestas o señales realizadas. A continuación, debemos pasar a gestionar estas posiciones gestionando cada cesta de forma individual. Para lograrlo, utilizamos un bucle for para la iteración.

//--- Loop through all active baskets to manage grid positions and potential closures
for(int i = 0; i < ArraySize(baskets); i++) {
   ManageGridPositions(i, ask, bid); //--- Manage grid trading for the current basket
}

Aquí, iteramos a través de todas las cestas activas utilizando un bucle for, asegurándonos de que cada cesta se gestione adecuadamente. La función ArraySize determina el número actual de cestas en la matriz «baskets», proporcionando el límite superior del bucle. Esto garantiza que procesemos todas las cestas existentes sin sobrepasar los límites de la matriz. Para cada cesta, llamamos a la función «ManageGridPositions()», pasando el índice de la cesta junto con los precios normalizados de «venta» y «compra». La función es la siguiente.

//+------------------------------------------------------------------+
//--- Manage Grid Positions
//+------------------------------------------------------------------+
void ManageGridPositions(int basketIdx, double ask, double bid) {
   bool newPositionOpened = false; //--- Flag to track if a new grid position has been opened
   string comment = GetPositionComment(baskets[basketIdx].basketId, false); //--- Generate a comment for grid trades in this basket
   obj_Trade.SetExpertMagicNumber(baskets[basketIdx].magic); //--- Ensure the trade object uses the basket's unique magic number

   if(baskets[basketIdx].direction == POSITION_TYPE_BUY) {
      if(ask <= baskets[basketIdx].gridSize) { //--- Check if the ask price has reached the grid level for a buy order
         baskets[basketIdx].currentLotSize *= inpMultiplier; //--- Increase the lot size based on the defined multiplier
         if(obj_Trade.Buy(baskets[basketIdx].currentLotSize, _Symbol, ask, 0, baskets[basketIdx].takeProfit, comment)) {
            newPositionOpened = true; //--- Set flag if the grid buy order is successfully executed
            Print("Basket ", baskets[basketIdx].basketId, ": Grid BUY at ", ask); //--- Log the grid buy execution details
            baskets[basketIdx].gridSize = ask - gridSize_Spacing; //--- Adjust the grid level for the next potential buy order
         } else {
            Print("Basket ", baskets[basketIdx].basketId, ": Grid BUY failed, error: ", GetLastError()); //--- Log an error if the grid buy order fails
         }
      }
   } else if(baskets[basketIdx].direction == POSITION_TYPE_SELL) {
      if(bid >= baskets[basketIdx].gridSize) { //--- Check if the bid price has reached the grid level for a sell order
         baskets[basketIdx].currentLotSize *= inpMultiplier; //--- Increase the lot size based on the multiplier for grid orders
         if(obj_Trade.Sell(baskets[basketIdx].currentLotSize, _Symbol, bid, 0, baskets[basketIdx].takeProfit, comment)) {
            newPositionOpened = true; //--- Set flag if the grid sell order is successfully executed
            Print("Basket ", baskets[basketIdx].basketId, ": Grid SELL at ", bid); //--- Log the grid sell execution details
            baskets[basketIdx].gridSize = bid + gridSize_Spacing; //--- Adjust the grid level for the next potential sell order
         } else {
            Print("Basket ", baskets[basketIdx].basketId, ": Grid SELL failed, error: ", GetLastError()); //--- Log an error if the grid sell order fails
         }
      }
   }

   //--- If a new grid position was opened and there are multiple positions, adjust the take profit to breakeven
   if(newPositionOpened && CountBasketPositions(baskets[basketIdx].basketId) > 1) {
      double breakevenPrice = CalculateBreakevenPrice(baskets[basketIdx].basketId); //--- Calculate the weighted breakeven price for the basket
      double newTP = (baskets[basketIdx].direction == POSITION_TYPE_BUY) ?
                     breakevenPrice + (inpBreakevenPts * _Point) : //--- Set new TP for buy positions
                     breakevenPrice - (inpBreakevenPts * _Point);  //--- Set new TP for sell positions
      baskets[basketIdx].takeProfit = newTP; //--- Update the basket's take profit level with the new value
      for(int j = PositionsTotal() - 1; j >= 0; j--) { //--- Loop through all open positions to update TP where necessary
         ulong ticket = PositionGetTicket(j); //--- Get the ticket number for the current position
         if(PositionSelectByTicket(ticket) && 
            PositionGetString(POSITION_SYMBOL) == _Symbol && 
            PositionGetInteger(POSITION_MAGIC) == baskets[basketIdx].magic) { //--- Identify positions that belong to the current basket
            if(!obj_Trade.PositionModify(ticket, 0, newTP)) { //--- Attempt to modify the position's take profit level
               Print("Basket ", baskets[basketIdx].basketId, ": Failed to modify TP for ticket ", ticket); //--- Log error if modifying TP fails
            }
         }
      }
      Print("Basket ", baskets[basketIdx].basketId, ": Breakeven = ", breakevenPrice, ", New TP = ", newTP); //--- Log the new breakeven and take profit levels
   }
}

Aquí implementamos la función «ManageGridPositions()» para gestionar dinámicamente las operaciones basadas en cuadrículas dentro de cada cesta activa. Nos aseguramos de que las nuevas posiciones en la red se ejecuten a los niveles de precios correctos y de que se realicen los ajustes de beneficios cuando sea necesario. Comenzamos inicializando el indicador «newPositionOpened» para realizar un seguimiento de si se ha ejecutado una nueva operación de cuadrícula. Mediante la función «GetPositionComment()», generamos una cadena de comentarios específica para cada tipo de operación (inicial o cuadrícula). A continuación, llamamos a la función «SetExpertMagicNumber()» para asignar el número mágico único de la cesta, lo que garantiza que todas las operaciones dentro de la cesta se registren correctamente.

Para las cestas de compra, comprobamos si el precio de venta ha bajado hasta el umbral «gridSize» o por debajo de él. Si se cumple esta condición, ajustamos el tamaño del lote multiplicando «currentLotSize» por el parámetro de entrada «inpMultiplier». A continuación, intentamos realizar una orden de compra utilizando el método «Buy()» del objeto comercial «obj_Trade». Si la operación se ejecuta correctamente, actualizamos «gridSize» restándole «gridSize_Spacing», lo que garantiza que la siguiente operación de compra se posicione correctamente. También registramos la ejecución correcta utilizando la función Print(). Si la orden de compra falla, recuperamos y registramos el error utilizando la función GetLastError().

Para las cestas de venta, seguimos un proceso similar, pero en su lugar, comprobamos si el precio de oferta ha subido hasta el umbral gridSize o por encima de él. Si se cumple esta condición, ajustamos el tamaño del lote aplicando el «inpMultiplier» al «currentLotSize». A continuación, ejecutamos una orden de venta utilizando la función «Sell()» y actualizamos el gridSize añadiendo «gridSize_Spacing» para definir el siguiente nivel de venta. Si la orden se ejecuta correctamente, registramos los detalles con «Print()», y si falla, registramos el error con «GetLastError()».

Si se abre una nueva posición en la cuadrícula y la cesta ahora contiene varias operaciones, procedemos a ajustar la toma de ganancias a un nivel de equilibrio. Primero determinamos el precio de equilibrio llamando a la función «CalculateBreakevenPrice()». A continuación, calculamos un nuevo nivel de take profit basado en la dirección de la cesta:

  • Para las cestas de compra, el take profit se establece añadiendo «inpBreakevenPts» (convertido en puntos de precio) al precio de equilibrio.
  • Para las cestas de venta, el take profit se ajusta restando «inpBreakevenPts» del precio de equilibrio.

A continuación, recorremos todas las posiciones abiertas utilizando la función PositionsTotal(), recuperando el número de ticket de cada posición con PositionGetTicket(). Utilizamos PositionSelectByTicket() para seleccionar la posición y verificar su símbolo con la función «PositionGetString». También nos aseguramos de que la posición pertenezca a la cesta correcta comprobando su número mágico con el parámetro «POSITION_MAGIC». Una vez verificada, intentamos modificar su take profit utilizando el método «PositionModify()». Si esta modificación falla, registramos el error.

Por último, registramos el nuevo precio de equilibrio calculado y el nivel de take profit actualizado utilizando la función Print(). Esto garantiza que la estrategia de negociación en red se adapte dinámicamente, al tiempo que mantiene puntos de salida eficientes. La función responsable de calcular el precio promedio es la siguiente.

//+------------------------------------------------------------------+
//--- Calculate Weighted Breakeven Price for a Basket
//+------------------------------------------------------------------+
double CalculateBreakevenPrice(int basketId) {
   double weightedSum = 0.0; //--- Initialize sum for weighted prices
   double totalLots = 0.0;   //--- Initialize sum for total lot sizes
   for(int i = 0; i < PositionsTotal(); i++) { //--- Loop over all open positions
      ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket for the current position
      if(PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && 
         StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Check if the position belongs to the specified basket
         double lot = PositionGetDouble(POSITION_VOLUME); //--- Get the lot size of the position
         double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get the open price of the position
         weightedSum += openPrice * lot; //--- Add the weighted price to the sum
         totalLots += lot; //--- Add the lot size to the total lots
      }
   }
   return (totalLots > 0) ? (weightedSum / totalLots) : 0; //--- Return the weighted average price (breakeven) or 0 if no positions found
}

Implementamos la función «CalculateBreakevenPrice()» para determinar el precio de equilibrio ponderado para una cesta de operaciones determinada, lo que garantiza que el nivel de take profit se pueda ajustar dinámicamente en función de los precios de entrada ponderados por volumen de todas las posiciones abiertas dentro de la cesta. Comenzamos inicializando «weightedSum» para almacenar la suma de los precios ponderados y «totalLots» para realizar un seguimiento del tamaño total de todas las posiciones de la cesta. A continuación, iteramos a través de todas las posiciones abiertas.

Para cada posición, recuperamos su número de ticket utilizando PositionGetTicket() y seleccionamos la posición con PositionSelectByTicket(). Verificamos que la posición pertenezca al símbolo comercial actual. Además, comprobamos si la posición forma parte de la cesta especificada buscando el ID de la cesta en la cadena de comentarios utilizando la función StringFind(). El comentario debe contener «Basket_» + IntegerToString(basketId) para clasificarse en la misma cesta.

Una vez verificada la posición, extraemos el tamaño del lote utilizando «PositionGetDouble(POSITION_VOLUME)» y su precio de apertura utilizando POSITION_PRICE_OPEN. A continuación, multiplicamos el precio de apertura por el tamaño del lote y sumamos el resultado a «weightedSum», asegurándonos de que los lotes más grandes tengan una mayor influencia en el precio de equilibrio final. Al mismo tiempo, acumulamos el tamaño total del lote en «totalLots».

Después de recorrer todas las posiciones, calculamos el precio de equilibrio medio ponderado dividiendo «weightedSum» entre «totalLots». Si no hay posiciones en la cesta («totalLots» == 0), devolvemos 0 para evitar errores de división por cero. Al ejecutar el programa, obtenemos el siguiente resultado.

IMAGEN DE CUADRÍCULAS ABIERTAS 1

En la imagen podemos ver que las cestas se gestionan de forma independiente, abriendo las cuadrículas y calculando el promedio de los precios. Por ejemplo, la cesta 2 tiene los mismos niveles de take profit de 0,68074. Podemos confirmarlo en el diario, tal y como se muestra a continuación.

DIARIO DE POSICIONES DE LA CUADRÍCULA

En la imagen podemos ver que, una vez abierta la posición de compra en la cuadrícula para la cesta 4, también modificamos el take profit. Ahora, debemos cerrar las posiciones basándonos en los modos solo por motivos de seguridad, aunque no es necesario, ya que ya hemos modificado los niveles, como se indica a continuación.

if(closureMode == CLOSE_BY_PROFIT) CheckAndCloseProfitTargets(); //--- If using profit target closure mode, check for profit conditions
if(closureMode == CLOSE_BY_POINTS && CountBasketPositions(baskets[i].basketId) > 1) {
   CheckBreakevenClose(i, ask, bid); //--- If using points-based closure and multiple positions exist, check breakeven conditions
}

Aquí gestionamos los cierres comerciales basándonos en el «closureMode» seleccionado. Si se establece en «CLOSE_BY_PROFIT», llamamos a «CheckAndCloseProfitTargets()» para cerrar las cestas que alcanzan sus objetivos de beneficio. Si se establece en «CLOSE_BY_POINTS», nos aseguramos de que la cesta tenga varias posiciones utilizando «CountBasketPositions()» antes de llamar a «CheckBreakevenClose()» para cerrar las operaciones en el punto de equilibrio cuando se cumplan las condiciones. Las funciones son las siguientes.

//+------------------------------------------------------------------+
//--- Check and Close Profit Targets (for CLOSE_BY_PROFIT mode)
//+------------------------------------------------------------------+
void CheckAndCloseProfitTargets() {
   for(int i = 0; i < ArraySize(baskets); i++) { //--- Loop through each active basket
      int posCount = CountBasketPositions(baskets[i].basketId); //--- Count how many positions belong to the current basket
      if(posCount <= 1) continue; //--- Skip baskets with only one position as profit target checks apply to multiple positions
      double totalProfit = 0; //--- Initialize the total profit accumulator for the basket
      for(int j = PositionsTotal() - 1; j >= 0; j--) { //--- Loop through all open positions to sum their profits
         ulong ticket = PositionGetTicket(j); //--- Get the ticket for the current position
         if(PositionSelectByTicket(ticket) && 
            StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(baskets[i].basketId)) >= 0) { //--- Check if the position is part of the current basket
            totalProfit += PositionGetDouble(POSITION_PROFIT); //--- Add the position's profit to the basket's total profit
         }
      }
      if(totalProfit >= profitTotal_inCurrency) { //--- Check if the accumulated profit meets or exceeds the profit target
         Print("Basket ", baskets[i].basketId, ": Profit target reached (", totalProfit, ")"); //--- Log that the profit target has been reached for the basket
         CloseBasketPositions(baskets[i].basketId); //--- Close all positions in the basket to secure the profits
      }
   }
}

Aquí, comprobamos y cerramos las cestas cuando alcanzan el objetivo de beneficio en el modo «CLOSE_BY_PROFIT». Recorremos las «cestas» y utilizamos «CountBasketPositions()» para asegurarnos de que existen varias posiciones. A continuación, sumamos las ganancias utilizando «PositionGetDouble(POSITION_PROFIT)» para todas las posiciones de la cesta. Si el beneficio total alcanza o supera «profitTotal_inCurrency», registramos el evento y llamamos a «CloseBasketPositions()» para asegurar las ganancias. La función «CountBasketPositions» se define a continuación.

//+------------------------------------------------------------------+
//--- Count Positions in a Basket
//+------------------------------------------------------------------+
int CountBasketPositions(int basketId) {
   int count = 0; //--- Initialize the counter for positions in the basket
   for(int i = 0; i < PositionsTotal(); i++) { //--- Loop through all open positions
      ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket for the current position
      if(PositionSelectByTicket(ticket) && 
         StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Check if the position belongs to the specified basket
         count++; //--- Increment the counter if a matching position is found
      }
   }
   return count; //--- Return the total number of positions in the basket
}

Utilizamos la función «CountBasketPositions()» para contar las posiciones en una cesta específica. Recorremos todas las posiciones, recuperamos cada «ticket» con la función PositionGetTicket() y comprobamos si POSITION_COMMENT contiene el ID de la cesta. Si se encuentra una coincidencia, incrementamos «count». Por último, devolvemos el número total de posiciones en la cesta. La definición de la función «CloseBasketPositions()» también es la siguiente.

//+------------------------------------------------------------------+
//--- Close All Positions in a Basket
//+------------------------------------------------------------------+
void CloseBasketPositions(int basketId) {
   for(int i = PositionsTotal() - 1; i >= 0; i--) { //--- Loop backwards through all open positions
      ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket of the current position
      if(PositionSelectByTicket(ticket) && 
         StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Identify if the position belongs to the specified basket
         if(obj_Trade.PositionClose(ticket)) { //--- Attempt to close the position
            Print("Basket ", basketId, ": Closed position ticket ", ticket); //--- Log the successful closure of the position
         }
      }
   }
}

Utilizamos la misma lógica para iterar a través de todas las posiciones, verificarlas y cerrarlas utilizando el método «PositionClose». Por último, tenemos la función responsable de forzar el cierre de posiciones cuando superan los niveles objetivo definidos.

//+------------------------------------------------------------------+
//--- Check Breakeven Close
//+------------------------------------------------------------------+
void CheckBreakevenClose(int basketIdx, double ask, double bid) {
   double breakevenPrice = CalculateBreakevenPrice(baskets[basketIdx].basketId); //--- Calculate the breakeven price for the basket
   if(baskets[basketIdx].direction == POSITION_TYPE_BUY) {
      if(bid >= breakevenPrice + (inpBreakevenPts * _Point)) { //--- Check if the bid price exceeds breakeven plus threshold for buy positions
         Print("Basket ", baskets[basketIdx].basketId, ": Closing BUY positions at breakeven + points"); //--- Log that breakeven condition is met for closing positions
         CloseBasketPositions(baskets[basketIdx].basketId); //--- Close all positions for the basket
      }
   } else if(baskets[basketIdx].direction == POSITION_TYPE_SELL) {
      if(ask <= breakevenPrice - (inpBreakevenPts * _Point)) { //--- Check if the ask price is below breakeven minus threshold for sell positions
         Print("Basket ", baskets[basketIdx].basketId, ": Closing SELL positions at breakeven + points"); //--- Log that breakeven condition is met for closing positions
         CloseBasketPositions(baskets[basketIdx].basketId); //--- Close all positions for the basket
      }
   }
}

Aquí implementamos cierres basados en el punto de equilibrio utilizando «CheckBreakevenClose()». Primero determinamos el precio de equilibrio con «CalculateBreakevenPrice()». Si la cesta está en dirección de COMPRA y el precio de oferta supera el umbral de equilibrio más el umbral definido («inpBreakevenPts * _Point»), registramos el evento y ejecutamos «CloseBasketPositions()» para asegurar los beneficios. Del mismo modo, para las cestas SELL, comprobamos si el precio de venta cae por debajo del umbral de equilibrio menos el umbral, lo que desencadena el cierre. Esto garantiza que las posiciones se mantengan una vez que el movimiento del precio se alinea con las condiciones de equilibrio.

Por último, dado que cerramos las posiciones por take-profit al principio, esto significa que tenemos «cáscaras» vacías o cestas de posiciones que ensucian el sistema. Por lo tanto, para garantizar la limpieza, debemos identificar las cestas vacías que no contienen ningún elemento y eliminarlas. Implementamos la siguiente lógica.

//--- Remove inactive baskets that no longer have any open positions
for(int i = ArraySize(baskets) - 1; i >= 0; i--) {
   if(CountBasketPositions(baskets[i].basketId) == 0) {
      Print("Removing inactive basket ID: ", baskets[i].basketId); //--- Log the removal of an inactive basket
      for(int j = i; j < ArraySize(baskets) - 1; j++) {
         baskets[j] = baskets[j + 1]; //--- Shift basket elements down to fill the gap
      }
      ArrayResize(baskets, ArraySize(baskets) - 1); //--- Resize the baskets array to remove the empty slot
   }
}

Aquí nos aseguramos de que las cestas inactivas, que ya no contienen posiciones abiertas, se eliminen de manera eficiente. Iteramos a través de la matriz «baskets» en orden inverso para evitar problemas de desplazamiento de índices durante la eliminación. Utilizando «CountBasketPositions()», comprobamos si una cesta no tiene operaciones pendientes. Si está vacío, registramos su eliminación y desplazamos los elementos posteriores hacia abajo para mantener la estructura de la matriz. Por último, llamamos a ArrayResize() para ajustar el tamaño de la matriz, evitando el uso innecesario de memoria y asegurando que solo se realice un seguimiento de las cestas activas. Este enfoque mantiene la gestión de la cesta eficiente y evita el desorden en el sistema. Al ejecutarlo, obtenemos el siguiente resultado.

LIMPIAR EL DESORDEN

En la imagen podemos ver que gestionamos eficazmente la eliminación del desorden y que podemos controlar las posiciones de la cuadrícula, logrando así nuestro objetivo. Lo que queda por hacer es realizar pruebas retrospectivas del programa, lo cual se aborda en la siguiente sección.


Prueba retrospectiva

Tras realizar pruebas retrospectivas exhaustivas durante un año, 2023, utilizando la configuración predeterminada, obtenemos los siguientes resultados.

Gráfico de prueba retrospectiva:

GRÁFICO

Informe de prueba retrospectiva:

INFORME


Conclusión

En conclusión, hemos desarrollado un asesor experto MQL5 de negociación en cuadrícula multinivel que gestiona de manera eficiente las entradas de operaciones por capas, los ajustes dinámicos de la cuadrícula y la recuperación estructurada. Al integrar un espaciado escalable entre posiciones, una progresión controlada de lotes y salidas en punto de equilibrio, el sistema se adapta a las fluctuaciones del mercado al tiempo que optimiza el riesgo y la recompensa.

Descargo de responsabilidad: Este artículo tiene fines exclusivamente educativos. El trading conlleva un riesgo financiero significativo y las condiciones del mercado pueden ser impredecibles. Es esencial realizar pruebas retrospectivas adecuadas y gestionar los riesgos antes de la implementación en vivo.

Al aplicar estas técnicas, podrá mejorar sus habilidades en el trading algorítmico y perfeccionar su estrategia basada en cuadrículas. Sigue realizando pruebas y optimizaciones para lograr el éxito a largo plazo. ¡Mucha suerte!

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

johnsteed
johnsteed | 12 mar 2025 en 11:49

¡Un código muy bueno y un EA muy rápido!

Desafortunadamente hay un problema con el cálculo del tamaño del lote - multiplicadores con un decimal (como 1,3, 1,5, etc) puede causar problemas con las funciones de orden MQL como el tamaño del lote da a veces los códigos de error 4756 cuando el multiplicador no es 1 o 2.

Sería muy bueno si el cálculo de tamaño de lote podría ser cambiado marginalmente para asegurar que los tamaños de lote se calculan adecuadamente para la alimentación en las funciones de orden para todos los valores de multiplicador.

Allan Munene Mutiiria
Allan Munene Mutiiria | 17 mar 2025 en 19:19
johnsteed los códigos de error 4756 cuando el multiplicador no es 1 o 2.

Sería muy bueno si el cálculo de tamaño de lote podría ser cambiado marginalmente para asegurar que los tamaños de lote se calculan adecuadamente para la alimentación en las funciones de orden para todos los valores de multiplicador.

Gracias por sus amables comentarios. Por supuesto.

cbkiri
cbkiri | 6 jun 2025 en 03:18

Hola,

Despues de leer el articulo, lo encontre util y definitivamente lo probare. Sin embargo, parece que no estoy viendo o tal vez me perdí en el artículo sobre la separación de la primera posición TP que creo que también es útil y sostenible para la estrategia de negociación.


Gracias.

Allan Munene Mutiiria
Allan Munene Mutiiria | 6 jun 2025 en 11:19
cbkiri estrategia de negociación.


Gracias.

Claro, gracias.

Arbitraje de swaps en Forex: Reunimos un portafolio sintético y creamos un flujo de swaps estable Arbitraje de swaps en Forex: Reunimos un portafolio sintético y creamos un flujo de swaps estable
¿Quiere saber cómo aprovechar los spreads de los tipos de interés? En este artículo, veremos cómo usar el arbitraje de swaps en Forex para generar unos ingresos constantes cada noche construyendo un portafolio resistente a las fluctuaciones del mercado.
Trading de arbitraje en Forex: Análisis de movimientos de divisas sintéticas y reversión a la media Trading de arbitraje en Forex: Análisis de movimientos de divisas sintéticas y reversión a la media
En este artículo, intentaremos analizar los movimientos de divisas sintéticas utilizando Python + MQL5 y comprender cómo es el arbitraje de divisas real hoy en día. Asimismo, presentaremos cierto código Python listo para analizar divisas sintéticas y más información sobre qué son las divisas sintéticas en Forex.
Aplicación de la teoría de juegos a algoritmos comerciales Aplicación de la teoría de juegos a algoritmos comerciales
Hoy crearemos un asesor comercial adaptativo de autoaprendizaje basado en DQN de aprendizaje automático, con inferencia causal multivariante, que negociará con éxito simultáneamente en 7 pares de divisas, con agentes de diferentes pares intercambiando información entre sí.
El filtro de Kalman para estrategias de reversión a la media en Forex El filtro de Kalman para estrategias de reversión a la media en Forex
El filtro de Kalman es un algoritmo recursivo utilizado en el trading algorítmico para estimar el estado real de una serie temporal financiera filtrando el ruido de los movimientos de precios. Actualiza dinámicamente las predicciones basándose en nuevos datos del mercado, lo que lo hace valioso para estrategias adaptativas como la reversión a la media. Este artículo presenta primero el filtro de Kalman, cubriendo su cálculo e implementación. A continuación, aplicamos el filtro a una estrategia clásica de reversión a la media en el mercado de divisas como ejemplo. Por último, realizamos diversos análisis estadísticos comparando el filtro con una media móvil en diferentes pares de divisas.