Implementación de los cierres parciales en MQL5
- Introducción sobre los cierres parciales, ventajas y desventajas
- Integramos los parciales en MQL5
- Creación de la clase CPartials
- Declaración y definición de las funciones para la clase CPartials
- Implementación de la clase CPartials en un Ea
- Backtesting del bot de order blocks: ¿los parciales mejoran significativamente el rendimiento?
- Conclusión
Introducción sobre los cierres parciales, ventajas y desventajas
Los cierres parciales son una técnica de gestión de posiciones que consiste en definir niveles de "takeprofit" y esperar a que el precio llegue a ellos para cerrar una parte de la posición. Por ejemplo, se puede cerrar el 30% del volumen actual. Esto permite asegurar ganancias sin necesidad de finalizar toda la operación. De allí proviene su nombre: cierre parcial.
Ventajas frente a otras técnicas
Existen distintas formas de gestionar posiciones, entre ellas el breakeven y el trailing stop. Sin embargo, los cierres parciales ofrecen un enfoque distinto.
En el caso del breakeven, se asegura muy poca ganancia o incluso ninguna, ya que en muchos casos el "stoploss" queda igual al precio de entrada. El trailing stop, por otro lado, suele cerrar la operación con ganancias limitadas. Esto sucede sobre todo en mercados laterales, donde el movimiento del precio no permite que el trailing stop acompañe de forma eficaz.
El cierre parcial puede mejorar este panorama porque da margen para asegurar una parte de las ganancias, pero al mismo tiempo deja abierta la posibilidad de obtener más beneficios si el precio sigue avanzando. A diferencia de los métodos mencionados, no se finaliza por completo la operación al primer movimiento adverso.
Posibles desventajasUn aspecto a considerar es que cada vez que se ejecuta un cierre parcial, el broker puede cobrar una comisión. Esto hace que la técnica pueda resultar menos rentable en algunas condiciones.
Otro punto es que, si el precio no alcanza los niveles de "takeprofit" definidos, el cierre parcial no se activa, lo que significa que no se logra asegurar ninguna ganancia. Por esta razón, la efectividad depende mucho del tipo de estrategia utilizada.
Por ejemplo, en un bot de tipo "swing", donde los "takeprofit" suelen ser más amplios, tomar parciales puede ser útil para ir asegurando resultados intermedios. En cambio, en un bot de tipo scalper, donde los recorridos de precio son más cortos, quizás no tenga tanto sentido aplicar esta técnica.
Integramos los parciales en MQL5
Para integrar cierres parciales en MQL5 podemos usar una forma simple: definir "niveles de tp" en los que se cerrará un porcentaje del volumen actual de la posición. Por ejemplo, cerrar un 30% del volumen cuando el precio alcance cierto nivel.
Existen muchas formas de establecer esos niveles. Una opción es usar soportes o resistencias como referencia. Otra es esperar condiciones de mercado específicas, como sobrecompra o sobreventa, y cerrar parcialmente cuando se cumplan. Ambas son posibles de programar, pero en este artículo nos centraremos en la forma más directa: trabajar con "niveles de tp" predefinidos.
Definición de los niveles de tp
Llamaremos "nivel de tp" al precio donde se realizará el cierre parcial.
- En una posición de compra, el nivel debe ser mayor al precio de apertura, pero menor que el "takeprofit" inicial.
- En una posición de venta, el nivel debe ser menor al precio de apertura, pero mayor que el "takeprofit" inicial.
De esta manera, los parciales se pueden representar de forma visual en el gráfico. Por ejemplo, si se definen tres niveles, se verán como puntos intermedios entre el precio de entrada y el "takeprofit" final.

Figura 1: Ejemplo de niveles parciales
Cálculo de los niveles
Para calcular niveles como tp1, tp2 y tp3, se puede usar un porcentaje de la distancia entre el precio de entrada y el "takeprofit" inicial. Por ejemplo:
- tp1 al 30% de esa distancia.
- tp2 al 60% de esa distancia.
- tp3 al 90% de esa distancia.
El usuario puede ajustar estos porcentajes según sus necesidades en un string separado por ",".
Implementación en MQL5
Con todo lo explicado, el siguiente paso será implementar la lógica en MQL5. Antes de mostrar código, es importante aclarar un cambio en la estructura. Desde el último artículo sobre la integración del breakeven, he estado reorganizando la arquitectura de gestión de riesgo.
La clase CPartials no es totalmente autónoma, ya que depende de otra clase para su funcionamiento. Esto es relevante porque la idea no es tener un bloque aislado, sino que la gestión de parciales forme parte del sistema completo de control de posiciones.
Nuevos cambios a la gestión de riesgo
He realizado una serie de cambios importantes en la arquitectura de gestión de riesgo.
A continuación, explico los puntos más destacados.
1. Uso de CLoggerBase
Todas las clases ahora heredan de CLoggerBase. Esta clase fue creada para mejorar los "prints" del asesor experto. Es una clase simple que permite a sus descendientes generar mensajes con distintos tipos, como advertencias, errores fatales o errores comunes.
2. Librería Utils
Se ha creado una librería llamada Utils. Su propósito es reunir utilidades que pueden usarse en EAs, librerías y proyectos en general. Está compuesta por varios .mqh que contienen:
Clases (control de apertura de barra, ATR, cálculo de mínima diferencia, entre otras).
Funciones (tiempo, matemáticas simples, manejo de strings, conversiones, "array to string", ordenamiento, operaciones con arrays, etc.).
3. Reestructuración de la gestión de riesgo
La gestión de riesgo anterior estaba centralizada en un único archivo. Ahora ha sido dividida en varios módulos para mejorar la organización y la extensibilidad:
- AccountStatus.mqh: módulo encargado de ejecutar funciones de objetos CAccountGestor.
- LossProfit.mqh: implementación de CLossProfit, clase dedicada al cálculo y validación de superación de una "máxima ganancia o pérdida".
- LoteSizeCalc.mqh: módulo para el cálculo del lotaje.
- Modificators.mqh: módulo destinado a modificadores de "máximas pérdidas o ganancias".
- OcoOrder.mqh: implementación básica de órdenes OCO.
- OrdersGestor.mqh: implementación de la clase COrderGestor, que gestiona órdenes pendientes.
- RiskManagement.mqh: módulo principal donde se implementa la clase CRiskManagement.
- RiskManagementBases.mqh: contiene la clase base CRiskManagementBases.
- RM_Defines.mqh: definiciones de estructuras, macros y enums utilizadas por los demás módulos.
- RM_Functions.mqh: conjunto de funciones de apoyo que usan los módulos anteriores.
Los únicos módulos que no participan de forma directa en CRiskManagement son OrdersGestor y OcoOrder.
4. AccountStatus y CAccountGestor
La clase AccountStatus actúa como un puente para facilitar el trabajo con eventos relacionados con las posiciones: apertura, cierre, eliminación de órdenes, entre otros. Para lograr esto, se emplea la clase CAccountGestor, que funciona como una pseudo-interfaz.
class CAccountGestor : public CSpecializedManager { public: virtual void OnOpenClosePosition(const ROnOpenClosePosition &pos) = 0; //Function that will be executed each time a position is opened or closed virtual void OnOrderDelete(const ROnOrderDelete& order) { } //Function that will be executed each time an order is closed, by deletion, expiration, etc. //-- Function that is executed only once, where only the account profit fields are filled, such as account_gross_profit //daily, weekly, etc. virtual void OnNewProfit(const ROnOpenClosePosition &profit) { } //--- Function that is executed each time TesterDeposit or TesterWithdrawal is called... or capital is added to the account virtual void OnWithdrawalDeposit(double value) { } //If the value is positive it means a deposit, otherwise a withdrawal //-- Function that is executed every new day virtual void OnNewDay(datetime init_time) { } //-- Function that will be executed every new week virtual void OnNewWeek(datetime init_time) { } //-- Function that is executed every new month virtual void OnNewMonth(datetime init_time) { } //--- Function that is executed only once, only if there are previously open trades, only the position structure virtual void OnInitNewPos(const ROnOpenClosePosition &pos) { } };
Esta clase será utilizada por otras que necesiten ejecutar código cuando se abra o cierre una posición. Un ejemplo son las clases relacionadas con la gestión de posiciones.
5. Modificators.mqh y CExtraModifications
Un cambio novedoso es la incorporación del módulo Modificators.mqh. Este no se había implementado en los artículos anteriores sobre gestión de riesgo. La clase CExtraModifications permite crear modificadores para una "máxima pérdida o ganancia".
Por ejemplo, se puede establecer que cada vez que se obtenga un profit, el valor máximo permitido de pérdida o ganancia (GMLPO) se duplique. Si ocurre una pérdida, dicho valor se reinicia a su estado inicial.
//+--------------------------------------------------------------------+ //| Class to integrate external modifications to risk management | //+--------------------------------------------------------------------+ class CExtraModifications : public CLoggerBase { protected: CLossProfit* m_modifier; ENUM_TYPE_LOSS_PROFIT m_property_to_modify; string m_modifier_name; public: CExtraModifications(ENUM_TYPE_LOSS_PROFIT _property_to_modify = WRONG_VALUE) { m_property_to_modify = _property_to_modify; } //--- Non-modifiable functions // Function that returns the type of "maximum loss or profit" that this class is modifying inline ENUM_TYPE_LOSS_PROFIT MaximumProfitOrLossAModify() const { return m_property_to_modify; }; // Function that will be used in CRiskManagement to assign the "maximum loss or profit" based on the type of "maximum loss or profit" chosen // in the constructor. void SetPointer(CLossProfit* _modifier); // Name of the custom modifier (to differentiate from others) inline string Name() const { return m_modifier_name; } //--- Virtual functions // Function that will be executed each time a position is closed virtual void OnClosePosition(ModifierOnOpenCloseStruct &on_open_close_struct) { } // Function that will be executed each time an operation is opened virtual void OnOpenPosition(ModifierOnOpenCloseStruct &on_open_close_struct) { } // Function that will be executed only once (at the moment of creating the m_modifier) virtual void OnInitModifier(ModfierInitInfo &on_init) { } // Function that will be executed each new day virtual void OnNewDay() { } };
Con esta base, se pueden implementar clases personalizadas, como CDynamicRisk. Esta clase ajusta dinámicamente el riesgo usando diferentes métodos: multiplicación, exponencial o sumatoria.
//+------------------------------------------------------------------+ //| Clase para aumentar el riesgo | //+------------------------------------------------------------------+ enum ENUM_MULTIPLIER_METHOD_DR { DR_MULTIPLIER = 0, // Multiplier DR_EXPONECIAL = 1, // Exponential DR_SUMATORIO = 2 // Additive (Summation) }; //--- Clase class CDynamicRisk : public CExtraModifications { private: double paso; double val; double percentage_a_empezar_modfiicacioneS; ENUM_MULTIPLIER_METHOD_DR metod; void Aumentar(); bool is_set; public: CDynamicRisk(double step, double percentage_to_empezar, ENUM_TYPE_LOSS_PROFIT property_to_modify, ENUM_MULTIPLIER_METHOD_DR multiplier_); void OnClosePosition(ModifierOnOpenCloseStruct &on_open_close_struct) override; void OnInitModifier(ModfierInitInfo &on_init) override; }; //+------------------------------------------------------------------+ CDynamicRisk::CDynamicRisk(double step, double percentage_to_empezar, ENUM_TYPE_LOSS_PROFIT property_to_modify, ENUM_MULTIPLIER_METHOD_DR multiplier_) : CExtraModifications(property_to_modify) { m_modifier_name = "Risk enhancer by default"; paso = step; percentage_a_empezar_modfiicacioneS = percentage_to_empezar; metod = multiplier_; is_set = false; if((int)metod > 2 || metod < 0) { LogFatalError(StringFormat("The method %s is invalid", EnumToString(metod)), FUNCION_ACTUAL); Remover(); } } //+------------------------------------------------------------------+ void CDynamicRisk::OnInitModifier(ModfierInitInfo & on_init) { percentage_a_empezar_modfiicacioneS /= 100.0; percentage_a_empezar_modfiicacioneS *= on_init.balance; Print("Balance to increase risk: ", percentage_a_empezar_modfiicacioneS); val = m_modifier.GetPercentage(); //Print("Valor del porcentaje: ", val); } //+------------------------------------------------------------------+ void CDynamicRisk::OnClosePosition(ModifierOnOpenCloseStruct & on_open_close_struct) { if(on_open_close_struct.position.profit > 0 && on_open_close_struct.profit_total > percentage_a_empezar_modfiicacioneS) { LogInfo(StringFormat("By increasing the risk, a profit of %.2f has been obtained. The initial balance has been exceeded by %.2f.", on_open_close_struct.position.profit, percentage_a_empezar_modfiicacioneS), FUNCION_ACTUAL); Aumentar(); m_modifier.SetPercentage(val); } else { m_modifier.SetInitialPercentageOrMoney(); val = m_modifier.GetPercentage(); } } //+------------------------------------------------------------------+ void CDynamicRisk::Aumentar(void) { switch(metod) { case DR_MULTIPLIER: val *= paso; break; case DR_EXPONECIAL: val *= MathExp(paso); break; case DR_SUMATORIO: val += paso; break; default: Remover(); break; } }
En el método sumatorio, por ejemplo, se añade un valor step al porcentaje actual de la "máxima pérdida o ganancia".
Creación de la clase CPartials
Antes de codificar la clase incluiremos la gestión de riesgo.
#include "..\\RM\\RiskManagement.mqh"
Para aplicar cierres parciales necesitamos detectar cuándo se abren y cierran operaciones. Para ello, la clase CPartials heredará de CAccountGestor, lo que nos permitirá aprovechar los eventos ya definidos y que luego serán llamados desde AccountStatus.
//+------------------------------------------------------------------+ //| Class that implements partial closures | //+------------------------------------------------------------------+ class CPartials : public CAccountGestor
Variables privadas
Variables generales
Para filtrar operaciones y aplicar parciales solo a ciertas posiciones, usaremos un ulong que almacenará el número mágico. Con este valor evitamos aplicar la lógica de parciales a trades que no correspondan.
Además, necesitaremos una instancia de CTrade, que nos dará acceso a la función PositionClosePartial. También incluimos un booleano como bandera de estado, que indique si la clase puede ejecutarse (true) o si está deshabilitada (false).
//--- General variables ulong m_magic_number; // Magic number used to apply partials only to certain operations CTrade m_trade_executor; // CTrade type instance to apply partials bool m_disable_partials_flag; // Boolean that indicates if partials can be executed
Variables del símbolo
Como la gestión depende del símbolo, necesitamos información como volumen mínimo, paso de volumen y número de dígitos. También guardaremos el último MqlTick disponible.
//--- Symbol variables string m_trading_symbol; // Symbol from which minimum volume and volume step data will be obtained double m_volume_step; // Volume step double m_minimum_volume; // Minimum volume int m_price_digits; // Symbol digits MqlTick m_latest_tick; // MqlTick type structure
Estructuras para parciales
Definimos primero una estructura PartialTakeProfit, que representa un nivel de cierre parcial:
struct PartialTakeProfit { double partial_price; // Price where the partial will be executed double tp_level_percentage; // Percentage of TP level double volume_percentage_to_close; // Percentage of volume to close inline void Reset() { ZeroMemory(this); } };
Luego, como una posición puede tener varios parciales, creamos la estructura TrackedPosition. Esta almacenará todos los niveles parciales definidos por el usuario, el ticket de la posición y un índice que indique qué parcial corresponde aplicar.
struct TrackedPosition { PartialTakeProfit tp_levels[]; // Array of partial levels ulong ticket; // Position ticket ENUM_POSITION_TYPE type; // Position type int current_partial_index; // Next partial to be applied };
Arrays de control
Con las estructuras listas, declaramos las variables principales para gestionar múltiples posiciones y parciales.
//--- Variables for partials int m_max_partial_levels; // Number of partials to take from each operation TrackedPosition m_tracked_positions[]; // Array of operations to which partials will be applied int m_indices_to_remove[]; // Indices to remove from the m_tracked_positions array
Además, para aplicar parciales necesitamos almacenar dos arrays.
- Los porcentajes de volumen a cerrar.
- Los porcentajes de niveles de TP en los que se aplicarán.
//--- Arrays to take, both arrays must be the same size. double m_volume_percentages_to_close[]; double m_tp_level_percentages[];
Hashmap temporal
Por último, declaramos un CHashMap<double, double>. Su función es mantener los niveles de TP ordenados y facilitar la búsqueda del siguiente nivel a revisar. La idea es que este "puntero interno" avance cada vez que se toma un parcial, similar al puntero de lectura en un archivo.
//--- Temporary hashmap CHashMap<double, double> m_temporary_hashmap;
En la siguiente sección se explicará cómo será utilizado este hashmap dentro de la lógica de ejecución.
Constructor y Destructor
Empezaremos definiendo el constructor donde inicializaremos las variables miembro de la clase con un valor por defecto.
CPartials::CPartials() : m_magic_number(NOT_MAGIC_NUMBER), m_disable_partials_flag(true), m_trading_symbol(NULL), m_volume_step(0.00), m_minimum_volume(0.00), m_price_digits(0), m_max_partial_levels(0) { }
El destructor lo dejaremos vacío
//+------------------------------------------------------------------+ CPartials::~CPartials() { // Empty destructor }
Declaración y definición de las funciones para la clase CPartials
En esta sección continuaremos con la implementación de la clase CPartials, declarando y definiendo sus funciones.
La primera función que necesitamos es Init, encargada de inicializar la clase y establecer los parámetros principales.
Función Init
Como parámetros recibirá:
- magic_number_: número mágico para filtrar operaciones.
- symbol_: símbolo sobre el cual se aplicarán los parciales.
- volume_percentages_string: cadena de porcentajes de volumen a cerrar en cada nivel.
- tp_percentages_string: cadena con los niveles de TP en % de la distancia entre open price y take profit.
Ejemplo:
- TP levels: "20,40,60" - niveles al 20%, 40% y 60% del TP.
- Volumen a cerrar: "30,30,30" - se cerrará el 30% del volumen actual en cada nivel.
Si el volumen inicial era de 0.90 lotes, cuando se alcance el primer TP (20%) se cerrarán 0.27 lotes. Después de ejecutar esta acción, el índice current_partial_index avanzará al siguiente nivel de TP.
La declaración de la función queda así.
//--- Initialize function bool Init(ulong magic_number_, string symbol_, string volume_percentages_string, string tp_percentages_string);
Definiciones iniciales
Definimos una constante para indicar que no se aplicarán parciales (valor "0").
#define PARTIAL_NO_APPLIED "0"
La función completa comienza con la validación de cadenas.
bool CPartials::Init(ulong magic_number_, string symbol_, string volume_percentages_string, string tp_percentages_string) { //--- Validate if the strings are valid if(volume_percentages_string == PARTIAL_NO_APPLIED || tp_percentages_string == PARTIAL_NO_APPLIED) // "0" as an invalid value { m_disable_partials_flag = true; return false; }
Configuración de variables internas
Establecemos los valores internos de la clase, incluyendo parámetros del símbolo y configuración del objeto CTrade.
//--- Set the variables m_disable_partials_flag = false; m_magic_number = magic_number_; m_trading_symbol = symbol_; m_volume_step = SymbolInfoDouble(m_trading_symbol, SYMBOL_VOLUME_STEP); m_minimum_volume = SymbolInfoDouble(m_trading_symbol, SYMBOL_VOLUME_MIN); m_price_digits = (int)SymbolInfoInteger(m_trading_symbol, SYMBOL_DIGITS); m_trade_executor.SetExpertMagicNumber(m_magic_number);
Reserva inicial de arrays
Definimos macros de reserva inicial para los arrays de posiciones y de índices a eliminar:
#define CPARCIAL_RESERVE_ARR 5 #define CPARCIAL_RESERVE_TO_DELETE 5
Сódigo.
//--- Initial resize ArrayResize(m_tracked_positions, 0, CPARCIAL_RESERVE_ARR); ArrayResize(m_indices_to_remove, 0, CPARCIAL_RESERVE_TO_DELETE);
Conversión de cadenas a arrays
Usamos el namespace StrTo de nuestra librería Utils\FA\StringToArray.mqh, que contiene funciones para convertir cadenas en arrays del tipo requerido (las funciones están sobrecargadas, por lo que se resuelve en tiempo de compilación).
//--- Convert strings to double arrays StrTo::CstArray(m_volume_percentages_to_close, volume_percentages_string, ','); StrTo::CstArray(m_tp_level_percentages, tp_percentages_string, ',');
Validación de arrays
Primero verificamos que no estén vacíos y luego que tengan el mismo tamaño:
//--- 2nd validation, we validate that the array size is valid if(m_volume_percentages_to_close.Size() < 1 || m_tp_level_percentages.Size() < 1) { LogCriticalError(StringFormat("The size of the partial arrays to take %u or percentages to take %u are invalid", m_volume_percentages_to_close.Size(), m_tp_level_percentages.Size()), FUNCION_ACTUAL); m_disable_partials_flag = true; return false; } //--- 3rd validation, we validate that both arrays are the same size if(m_volume_percentages_to_close.Size() != m_tp_level_percentages.Size()) { LogCriticalError(StringFormat("The size of the partial arrays to take %u and percentages to take %u are not equal", m_volume_percentages_to_close.Size(), m_tp_level_percentages.Size()), FUNCION_ACTUAL); m_disable_partials_flag = true; return false; }
Si el log de tipo info está habilitado, imprimimos el estado inicial de los arrays.
//--- Initial log only if "info" type log is enabled if(IsInfoLogEnabled()) { FastLog(FUNCION_ACTUAL, INFO_TEXT, "Arrays before revision"); PrintArrayAsTable(m_tp_level_percentages, "Percentage of position where partials will be taken", "percentages simplified"); PrintArrayAsTable(m_volume_percentages_to_close, "Percentage to take", "percentages simplified"); }
Limpieza de arrays e inserción en el hashmap
En esta etapa entra en juego el HashMap, que funciona como un contenedor temporal de pares clave–valor para almacenar únicamente los datos válidos. Posteriormente, utilizaremos los valores "key" para obtener los valores "value".
El uso de HashMap surge por el siguiente problema: si los niveles de takeprofit se ordenan, ocasionaría que pierdan el alineamiento con el array de "porcentajes de volumen a cerrar".
Para evitar este problema, necesitamos una estructura que preserve la relación entre clave y valor, y qué mejor opción que la clase CHashMap.
- Clave (key): porcentaje del nivel de takeprofit.
- Valor (value): porcentaje de volumen que se cerrará parcialmente.
Si durante el proceso encontramos valores inválidos o duplicados (en el array "niveles de tp"), se almacenan sus índices en un array temporal indices_to_remove_temp, para luego eliminarlos del array de valores "porcentaje del nivel de takeprofit", seguidamente se ordenara dicho array de forma ascendente.
//--- Declaration of indices to remove and hashmap cleanup m_temporary_hashmap.Clear(); int indices_to_remove_temp[]; //--- Iteration and array cleanup for(int i = 0; i < ArraySize(m_tp_level_percentages); i++) { if(m_tp_level_percentages[i] <= 0.00) { LogWarning(StringFormat("The percentage where partials will be taken %f with index %d is invalid, therefore it will not be considered in partials", m_tp_level_percentages[i], i), FUNCION_ACTUAL); AddArrayNoVerification(indices_to_remove_temp, i, 0); continue; } if(m_volume_percentages_to_close[i] <= 0.00) { LogWarning(StringFormat("The percentage to take %f with index %d is invalid, therefore it will not be considered in partials", m_volume_percentages_to_close[i], i), FUNCION_ACTUAL); AddArrayNoVerification(indices_to_remove_temp, i, 0); continue; } if(m_temporary_hashmap.ContainsKey(m_tp_level_percentages[i])) AddArrayNoVerification(indices_to_remove_temp, i, 0); else m_temporary_hashmap.Add(m_tp_level_percentages[i], m_volume_percentages_to_close[i]); } //--- Sort and removal RemoveMultipleIndexes(m_tp_level_percentages, indices_to_remove_temp, 0); ArraySort(m_tp_level_percentages); ArrayResize(m_volume_percentages_to_close, ArraySize(m_tp_level_percentages));
Reescritura y normalización de valores
En este paso.
- Convertimos los porcentajes a fracciones (división entre 100).
- Reescribimos los arrays con los valores válidos obtenidos desde el hashmap.
//--- Final iteration for(int i = 0; i < ArraySize(m_tp_level_percentages); i++) { double val; m_temporary_hashmap.TryGetValue(m_tp_level_percentages[i], val); m_volume_percentages_to_close[i] = val / 100.0; m_tp_level_percentages[i] /= 100.0; }
Log final y asignación de parciales máximos
Finalmente, imprimimos los arrays corregidos y asignamos el número máximo de parciales por posición:
//--- Final log if(IsInfoLogEnabled()) { FastLog(FUNCION_ACTUAL, INFO_TEXT, "Arrays after revision"); PrintArrayAsTable(m_tp_level_percentages, "Percentage of position where partials will be taken", "percentages simplified"); PrintArrayAsTable(m_volume_percentages_to_close, "Percentage to take", "percentages simplified"); } m_max_partial_levels = ArraySize(m_volume_percentages_to_close); return true; }
Funciones para agregar posiciones a seguimiento en la clase CPartials
Para aplicar parciales a las operaciones abiertas, necesitamos primero agregar esas posiciones al array interno m_tracked_positions.
Definiremos dos funciones.
- AddPositionToTrack (pública):
- Permite agregar posiciones al seguimiento.
- Se usa tanto automáticamente al abrir operaciones (cuando cumplen las condiciones) como manualmente, por ejemplo, desde un panel donde el usuario selecciona qué operaciones tendrán parciales.
2. AddToTrackedPositions (privada):
- Función de soporte que agrega directamente un objeto TrackedPosition al array interno.
- Se invoca desde AddPositionToTrack.
Función pública AddPositionToTrack
Declaración en la sección pública de la clase:
//--- Function to add a position to the internal array manually (this is automatically invoked in OnOpen..... for all positions // opened with the magic number and with a valid tp). void AddPositionToTrack(ulong position_ticket, double position_tp, double entry_price, ENUM_POSITION_TYPE position_type);
El cuerpo:
//+------------------------------------------------------------------+ //| Adds a position with a set of variables to the internal array | //| Inputs: | //| - position_ticket: position ticket. | //| - position_tp: position takeprofit. | //| - entry_price: position entry price. | //| - position_type: position type (buy or sell) | //| | //| Outputs: | //| - The function returns nothing. | //| | //| Notes: | //| - The mentioned properties can be consulted with the native | //| function: PositionGetDouble(...); | //| - The position must have a valid tp. | //| - The function does not check if the ticket is valid, so if | //| you manually introduce an operation, its ticket must be valid. | //+------------------------------------------------------------------+ void CPartials::AddPositionToTrack(ulong position_ticket, double position_tp, double entry_price, ENUM_POSITION_TYPE position_type) { if(m_disable_partials_flag) return; //--- Initial check, we verify that the tp is valid if(position_tp <= 0.00000000000001) { LogError(StringFormat("The take profit with a value of %f from position %I64u is less than or equal to 0", position_tp, position_ticket), FUNCION_ACTUAL); return; } //--- TrackedPosition new_tracked_position; new_tracked_position.ticket = position_ticket; new_tracked_position.type = position_type; new_tracked_position.current_partial_index = 0; ArrayResize(new_tracked_position.tp_levels, ArraySize(m_volume_percentages_to_close)); for(int i = 0; i < ArraySize(new_tracked_position.tp_levels); i++) { new_tracked_position.tp_levels[i].Reset(); new_tracked_position.tp_levels[i].tp_level_percentage = m_tp_level_percentages[i]; new_tracked_position.tp_levels[i].volume_percentage_to_close = m_volume_percentages_to_close[i]; if(position_type == POSITION_TYPE_BUY) { double calculated_partial_price = entry_price + ((position_tp - entry_price) * m_tp_level_percentages[i]); new_tracked_position.tp_levels[i].partial_price = calculated_partial_price; } else { double calculated_partial_price = entry_price - ((entry_price - position_tp) * m_tp_level_percentages[i]); new_tracked_position.tp_levels[i].partial_price = calculated_partial_price; } } //--- Log about the "tps" where partials will be taken if(IsCautionLogEnabled()) { FastLog("TPS: ", CAUTION_TEXT, FUNCION_ACTUAL); ArrayPrint(new_tracked_position.tp_levels, m_price_digits, " | "); } //--- Add to internal array AddToTrackedPositions(new_tracked_position); }
Explicación del flujo
- Verificación inicial: si los parciales están deshabilitados (m_disable_partials_flag), no se hace nada.
- Validación de TP: el TP de la posición debe ser mayor que cero.
- Construcción del objeto TrackedPosition: se inicializan los datos de la posición, y se reservan los niveles parciales (tp_levels).
- Cálculo de precios parciales: para cada nivel se define el precio donde se ejecutará el parcial.
- En compras (POSITION_TYPE_BUY) el precio parcial se calcula sumando el porcentaje de la distancia entry_price → takeprofit.
- En ventas (POSITION_TYPE_SELL) se calcula restando.
- Registro de logs: opcional, muestra en consola los niveles de TP configurados.
- Agregado al array interno: se invoca a la función privada AddToTrackedPositions.
Función privada AddToTrackedPositions
Declaración en la sección privada de la clase:
//--- Function to add a new partial to the internal array void AddToTrackedPositions(TrackedPosition & new_tracked_position);
Definición.
void CPartials::AddToTrackedPositions(TrackedPosition &new_tracked_position)
{
AddArrayNoVerification2(m_tracked_positions, new_tracked_position, CPARCIAL_RESERVE_ARR)
} La macro AddArrayNoVerification2 (definida en Utils\FA\FuncionesBases.mqh) se encarga de agregar al final del array dinámico el nuevo elemento new_tracked_position, sin validaciones adicionales y con reserva si es necesario.
Detección de aperturas y cierres de posiciones en CPartials
Como vimos en la definición de la clase CAccountGestor, tenemos varias funciones que se pueden sobrescribir para reaccionar a eventos de cuenta (apertura, cierre, eliminación de órdenes, etc.).
En nuestro caso, la única función que necesitamos implementar es OnOpenClosePosition, ya que es la que se ejecuta automáticamente cada vez que se abre o cierra una operación.
Declaración en la clase
En la parte pública de CPartials declaramos:
//--- Function that will be automatically invoked by account status each time a position is opened or closed void OnOpenClosePosition(const ROnOpenClosePosition &pos) override;
Definición de la función
Si se abre un trade, verificamos que el número mágico de dicha posición corresponda con el número mágico interno almacenado en la clase, o bien que el número mágico miembro de la clase tenga el valor NOT_MAGIC_NUMBER.
Si se cierra un trade, llamamos a la función RemoveIndexFromAnArrayOfPositions, la cual eliminará la posición, en caso de existir, del array m_tracked_positions.
//+--------------------------------------------------------------------+ //| Function that runs every time a position is opened or closed | //+--------------------------------------------------------------------+ void CPartials::OnOpenClosePosition(const ROnOpenClosePosition &pos) { if(m_disable_partials_flag) return; //--- if(pos.deal_entry_type == DEAL_ENTRY_OUT) { // Once the position is closed we verify if that ticket is in the internal array, if so the function // will remove it. RemoveIndexFromAnArrayOfPositions(m_tracked_positions, pos.position.ticket, CPARCIAL_RESERVE_ARR); } else if(pos.deal_entry_type == DEAL_ENTRY_IN && (pos.position.magic == m_magic_number || m_magic_number == NOT_MAGIC_NUMBER)) { LogInfo(StringFormat("A trade has just been opened with ticket %I64u", pos.position.ticket), FUNCION_ACTUAL); AddPositionToTrack(pos.position.ticket, pos.position.first_tp, pos.position.open_price, pos.position.type); } }
Con esta función lista, solo nos queda lo más importante: la lógica de los parciales.
Desarrollo de la función para cierres parciales
En la parte pública de la clase declararemos la función principal de revisión de posiciones.
Esta función no recibirá parámetros ni devolverá valores.
//--- Main function for position review void CheckTrackedPositions();
Ahora definiremos el cuerpo de la función. Lo primero que haremos es implementar dos "early exits".
El primero verifica si la bandera m_disable_partials_flag está activa. Si lo está, ejecutamos return y evitamos que el resto del código se ejecute.
El segundo comprueba si no hay operaciones abiertas, en cuyo caso también devolvemos return para no recorrer el array m_tracked_positions.
//--- Initial check to verify not to execute the function if partials are not allowed if(m_disable_partials_flag) return; //--- If the internal array m_partials is empty we do an early exit. if(m_tracked_positions.Size() < 1) return;
Seguidamente, utilizaremos SymbolInfoTick para obtener el último tick y guardarlo en la variable privada m_latest_tick.
Luego, prepararemos el array que usaremos para almacenar los índices de las posiciones que deban ser eliminadas. Para esto reservamos cinco elementos y lo inicializamos en cero.
//--- SymbolInfoTick(m_trading_symbol, m_latest_tick); ArrayResize(m_indices_to_remove, 0, CPARCIAL_RESERVE_TO_DELETE);
Ahora, para la lógica principal de los parciales que implementaremos, lo que debemos hacer es Iterar sobre todos los elementos del array m_tracked_positions y verificar, mediante un if, si se ha superado el nivel de take profit (TP) actual.
En caso de que lo mencionado sea cierto, cerraremos parcialmente la posición y moveremos el puntero interno (índice++) al siguiente nivel de TP.
Este flujo se representa en las siguientes ilustraciones.
Para posiciones de compra.

Figura 2: "algoritmo" para los parciales en posiciones de compra
Para posiciones de venta:

Figura 3: "algoritmo" para los parciales en posiciones de venta
El primer paso será diferenciar si la posición es de compra o de venta. Para ello, usamos el miembro type de la estructura TrackedPosition y lo comparamos con POSITION_TYPE_BUY o POSITION_TYPE_SELL.
//--- Main iteration for(int i = 0; i < ArraySize(m_tracked_positions); i++) { const ulong current_ticket = m_tracked_positions[i].ticket; // Current position ticket if(m_tracked_positions[i].type == POSITION_TYPE_BUY) { } else if(m_tracked_positions[i].type == POSITION_TYPE_SELL) { } }
Dentro de estos bloques (cuerpo de los ifs) se agrega la lógica específica. El primer paso para este bloque es comprobar si el precio bid o ask ha superado el partial_price. Si se cumple, seleccionamos el ticket de la posición.
for(int i = 0; i < ArraySize(m_tracked_positions); i++) { const ulong current_ticket = m_tracked_positions[i].ticket; // Current position ticket if(m_tracked_positions[i].type == POSITION_TYPE_BUY) { const int current_level_index = m_tracked_positions[i].current_partial_index; if(m_latest_tick.ask > m_tracked_positions[i].tp_levels[current_level_index].partial_price) { // Failed to select the ticket if(!PositionSelectByTicket(current_ticket)) { LogError(StringFormat("Could not select ticket %I64u, last error = %d", current_ticket, GetLastError()), FUNCION_ACTUAL); continue; } } } else if(m_tracked_positions[i].type == POSITION_TYPE_SELL) { const int current_level_index = m_tracked_positions[i].current_partial_index; if(m_latest_tick.bid < m_tracked_positions[i].tp_levels[current_level_index].partial_price) { if(!PositionSelectByTicket(current_ticket)) { LogError(StringFormat("Could not select ticket %I64u, last error = %d", current_ticket, GetLastError()), FUNCION_ACTUAL); continue; } } } }
Si se pudo seleccionar el ticket de la posición correctamente, calcularemos el volumen que deberá cerrarse.
const double current_position_volume = PositionGetDouble(POSITION_VOLUME); // Current operation volume double volume_to_close = current_position_volume * m_tracked_positions[i].tp_levels[current_level_index].volume_percentage_to_close; // Volume that will be "removed" from the position
Si el volumen calculado es menor que m_minimum_volume, se ignora el cierre parcial, se mueve el puntero interno y, si ya no quedan más niveles, la operación se elimina de m_tracked_positions (se agrega al array temporal m_indices_to_remove, para su posterior eliminación).
if(volume_to_close < m_minimum_volume) { m_tracked_positions[i].current_partial_index += 1; LogError(StringFormat("The lot to take %f is less than the minimum lot %f", volume_to_close, m_minimum_volume), FUNCION_ACTUAL); if(m_tracked_positions[i].current_partial_index == m_max_partial_levels) // Maximum partials reached { // Remove the partial AddArrayNoVerification2(m_indices_to_remove, i, CPARCIAL_RESERVE_TO_DELETE) } continue; }
Si el volumen es válido, se cierra parcialmente con ese valor, se incrementa el puntero interno y, si ya no quedan más niveles, la operación también se elimina.
volume_to_close = RoundToStep(volume_to_close, m_volume_step); m_trade_executor.PositionClosePartial(current_ticket, volume_to_close); m_tracked_positions[i].current_partial_index += 1; if(m_tracked_positions[i].current_partial_index == m_max_partial_levels) // Maximum partials reached { // Remove the partial AddArrayNoVerification2(m_indices_to_remove, i, CPARCIAL_RESERVE_TO_DELETE) }
Finalmente, se eliminan del array principal los índices guardados en el array temporal.
//---
RemoveMultipleIndexes(m_tracked_positions, m_indices_to_remove, CPARCIAL_RESERVE_ARR); Con esto, el código completo de la función queda de la siguiente manera.
//+------------------------------------------------------------------+ //| Function that will iterate over the m_partials array | //| and if necessary will partially close the operation | //+------------------------------------------------------------------+ void CPartials::CheckTrackedPositions(void) { //--- Initial check to verify not to execute the function if partials are not allowed if(m_disable_partials_flag) return; //--- If the internal array m_partials is empty we do an early exit. if(m_tracked_positions.Size() < 1) return; //--- SymbolInfoTick(m_trading_symbol, m_latest_tick); ArrayResize(m_indices_to_remove, 0, CPARCIAL_RESERVE_TO_DELETE); //--- Main iteration for(int i = 0; i < ArraySize(m_tracked_positions); i++) { const ulong current_ticket = m_tracked_positions[i].ticket; // Current position ticket if(m_tracked_positions[i].type == POSITION_TYPE_BUY) { const int current_level_index = m_tracked_positions[i].current_partial_index; if(m_latest_tick.ask > m_tracked_positions[i].tp_levels[current_level_index].partial_price) { // Failed to select the ticket if(!PositionSelectByTicket(current_ticket)) { LogError(StringFormat("Could not select ticket %I64u, last error = %d", current_ticket, GetLastError()), FUNCION_ACTUAL); continue; } const double current_position_volume = PositionGetDouble(POSITION_VOLUME); // Current operation volume double volume_to_close = current_position_volume * m_tracked_positions[i].tp_levels[current_level_index].volume_percentage_to_close; // Volume that will be "removed" from the position if(volume_to_close < m_minimum_volume) { m_tracked_positions[i].current_partial_index += 1; LogError(StringFormat("The lot to take %f is less than the minimum lot %f", volume_to_close, m_minimum_volume), FUNCION_ACTUAL); if(m_tracked_positions[i].current_partial_index == m_max_partial_levels) // Maximum partials reached { // Remove the partial AddArrayNoVerification2(m_indices_to_remove, i, CPARCIAL_RESERVE_TO_DELETE) } continue; } volume_to_close = RoundToStep(volume_to_close, m_volume_step); m_trade_executor.PositionClosePartial(current_ticket, volume_to_close); m_tracked_positions[i].current_partial_index += 1; if(m_tracked_positions[i].current_partial_index == m_max_partial_levels) // Maximum partials reached { // Remove the partial AddArrayNoVerification2(m_indices_to_remove, i, CPARCIAL_RESERVE_TO_DELETE) } } } else if(m_tracked_positions[i].type == POSITION_TYPE_SELL) { const int current_level_index = m_tracked_positions[i].current_partial_index; if(m_latest_tick.bid < m_tracked_positions[i].tp_levels[current_level_index].partial_price) { if(!PositionSelectByTicket(current_ticket)) { LogError(StringFormat("Could not select ticket %I64u, last error = %d", current_ticket, GetLastError()), FUNCION_ACTUAL); continue; } const double current_position_volume = PositionGetDouble(POSITION_VOLUME); // Current trade volume double volume_to_close = current_position_volume * m_tracked_positions[i].tp_levels[current_level_index].volume_percentage_to_close; // Volume to close if(volume_to_close < m_minimum_volume) // Volume too small { m_tracked_positions[i].current_partial_index += 1; // Continue with the next one LogError(StringFormat("The lot to take %f is less than the minimum lot %f", volume_to_close, m_minimum_volume), FUNCION_ACTUAL); if(m_tracked_positions[i].current_partial_index == m_max_partial_levels) { AddArrayNoVerification2(m_indices_to_remove, i, CPARCIAL_RESERVE_TO_DELETE) } continue; } //--- volume_to_close = RoundToStep(volume_to_close, m_volume_step); m_trade_executor.PositionClosePartial(current_ticket, volume_to_close); // Partial closure m_tracked_positions[i].current_partial_index += 1; // Move to next index if(m_tracked_positions[i].current_partial_index == m_max_partial_levels) // Limit reached, remove this index { AddArrayNoVerification2(m_indices_to_remove, i, CPARCIAL_RESERVE_TO_DELETE) } } } } //--- RemoveMultipleIndexes(m_tracked_positions, m_indices_to_remove, CPARCIAL_RESERVE_ARR); }
Implementación de la clase CPartials en un Ea
Para poner a prueba la clase CPartials usaremos el EA de order blocks que ya se presentó en artículos anteriores. El archivo del EA se ubicará en la carpeta OrderBlock.
En este mismo EA integraremos la lógica de breakeven (desarrollada en la serie de artículos sobre breakeven) y de cierres parciales desarrollada en este artículo.

Figura 4: Ubicación del archivo "Order Block EA MetaTrader 5.mq5" en el repositorio
#include "..\\PosManagement\\Breakeven.mqh" #include "..\\PosManagement\\Partials.mqh"
Una vez indicados los archivos a incluir, pasamos a la definición de los inputs del EA.
Primera sección: configuración del EA de order blocks
En esta parte se ubican los parámetros relacionados con el indicador de order blocks.
sinput group "-------| Order Block EA settings |-------" input ulong InpMagic = 545244; //Magic number input ENUM_TIMEFRAMES InpTimeframeOrderBlock = PERIOD_M5; //Order block timeframe sinput group "-- Order Block --" input int InpRangoUniversalBusqueda = 500; //search range of order blocks input int InpWidthOrderBlock = 1; //Width order block input bool InpBackOrderBlock = true; //Back order block? input bool InpFillOrderBlock = true; //Fill order block? input color InpColorOrderBlockBajista = clrRed; //Bearish order block color input color InpColorOrderBlockAlcista = clrGreen; //Bullish order block color input double InpTransparency = 0.5; // Transparency from 0.0 (invisible) to 1.0 (opaque) sinput group "" sinput group "-------| Strategy |-------" input ENUM_TP_SL_STYLE InpTpSlStyle = ATR;//Tp and sl style: sinput group "- TP SL by ATR " input double InpAtrMultiplier1 = 9.3;//Atr multiplier 1 (SL) input double InpAtrMultiplier2 = 24.4;//Atr multiplier 2 (TP) sinput group "- TP SL by POINT " input int InpTpPoint = 1000; //TP in Points input int InpSlPoint = 1000; //SL in Points
También se añaden los parámetros generales de la estrategia, incluyendo estilos de takeprofit y stoploss.
Segunda sección: parámetros de "AccountManager"
Aquí solo se define el nivel de log.
sinput group "" sinput group "-------| Account Status |-------" input ENUM_VERBOSE_LOG_LEVEL InpLogLevelAccountStatus = VERBOSE_LOG_LEVEL_ERROR_ONLY; //(Account Status|Ticket Mangement) log level:
Tercera sección: gestión de riesgo
En esta parte se definen los parámetros principales de gestión de riesgo.
sinput group "" sinput group "-------| Risk Management |-------" input ENUM_LOTE_TYPE InpLoteType = Dinamico; //Lote Type: input double InpLote = 0.1; //Lot size (only for fixed lot) input ENUM_MODE_RISK_MANAGEMENT InpRiskMode = risk_mode_personal_account; //type of risk management mode input bool InpUpdateDailyLossRiskModeProp = true; //Update the MDL, if the risk-management type is propfirm? input ENUM_GET_LOT InpGetMode = GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION; //How to get the lot input double InpPropFirmBalance = 0; //If risk mode is Prop Firm FTMO, then put your ftmo account balance input ENUM_VERBOSE_LOG_LEVEL InpLogLevelRiskManagement = VERBOSE_LOG_LEVEL_ERROR_ONLY; //Risk Management log level: sinput group "- ML/Maximum loss/Maximum loss -" input double InpPercentageOrMoneyMlInput = 0; //percentage or money (0 => not used ML) input ENUM_RISK_CALCULATION_MODE InpModeCalculationMl = percentage; //Mode calculation Max Loss input ENUM_APPLIED_PERCENTAGES InpAppliedPercentagesMl = Balance; //ML percentage applies to: sinput group "- MWL/Maximum weekly loss/Maximum weekly loss -" input double InpPercentageOrMoneyMwlInput = 0; //percentage or money (0 => not used MWL) input ENUM_RISK_CALCULATION_MODE InpModeCalculationMwl = percentage; //Mode calculation Max weekly Loss input ENUM_APPLIED_PERCENTAGES InpAppliedPercentagesMwl = Balance;//MWL percentage applies to: sinput group "- MDL/Maximum daily loss/Maximum daily loss -" input double InpPercentageOrMoneyMdlInput = 3.0; //percentage or money (0 => not used MDL) input ENUM_RISK_CALCULATION_MODE InpModeCalculationMdl = percentage; //Mode calculation Max daily loss input ENUM_APPLIED_PERCENTAGES InpAppliedPercentagesMdl = Balance;//MDL percentage applies to: sinput group "- GMLPO/Gross maximum loss per operation/Percentage to risk per operation -" input ENUM_OF_DYNAMIC_MODES_OF_GMLPO InpModeGmlpo = NO_DYNAMIC_GMLPO; //Select GMLPO mode: input double InpPercentageOrMoneyGmlpoInput = 2.0; //percentage or money (0 => not used GMLPO) input ENUM_RISK_CALCULATION_MODE InpModeCalculationGmlpo = percentage; //Mode calculation Max Loss per operation input ENUM_APPLIED_PERCENTAGES InpAppliedPercentagesGmlpo = Balance;//GMPLO percentage applies to:
Comparando esta versión con la anterior, el bloque GMLPO fue simplificado al eliminar una sección. Además, se añadió el nuevo parámetro InpUpdateDailyLossRiskModeProp. Este valor indica a la clase si debe actualizar el Maximum Daily Loss de forma automática en cuentas tipo propfirm.
Cuarta sección: parámetros de GMLPO dinámico y MDP
Aquí se incluyen los ajustes opcionales de riesgo dinámico (GMLPO) y los relacionados con el Maximum Daily Profit.
sinput group "-- Optional GMLPO settings, Dynamic GMLPO" sinput group "--- Full customizable dynamic GMLPO" input string InpNote1 = "subtracted from your total balance to establish a threshold."; //This parameter determines a specific percentage that will be input string InpStrPercentagesToBeReviewed = "15,30,50"; //percentages separated by commas. input string InpNote2 = "a new risk level will be triggered on your future trades: "; //When the current balance (equity) falls below this threshold input string InpStrPercentagesToApply = "10,20,25"; //percentages separated by commas. input string InpNote3 = "0 in both parameters => do not use dynamic risk in gmlpo"; //Note: sinput group "--- Fixed dynamic GMLPO with parameters" sinput group "- 1 -" input string InpNote11 = "subtracted from your total balance to establish a threshold."; //This parameter determines a specific percentage that will be input double InpBalancePercentageToActivateTheRisk1 = 2.0; //percentage 1 that will be exceeded to modify the risk separated by commas input string InpNote21 = "a new risk level will be triggered on your future trades: "; //When the current balance (equity) falls below this threshold input double InpPercentageToBeModified1 = 1.0;//new percentage 1 to which the gmlpo is modified sinput group "- 2 -" input double InpBalancePercentageToActivateTheRisk2 = 5.0;//percentage 2 that will be exceeded to modify the risk separated by commas input double InpPercentageToBeModified2 = 0.7;//new percentage 2 to which the gmlpo is modified sinput group "- 3 -" input double InpBalancePercentageToActivateTheRisk3 = 7.0;//percentage 3 that will be exceeded to modify the risk separated by commas input double InpPercentageToBeModified3 = 0.5;//new percentage 3 to which the gmlpo is modified sinput group "- 4 -" input double InpBalancePercentageToActivateTheRisk4 = 9.0;//percentage 4 that will be exceeded to modify the risk separated by commas input double InpPercentageToBeModified4 = 0.33;//new percentage 4 1 to which the gmlpo is modified sinput group "-- MDP/Maximum daily profit/Maximum daily profit --" input bool InpMdpIsStrict = true; //MDP is strict? input double InpPercentageOrMoneyMdpInput = 11.0; //percentage or money (0 => not used MDP) input ENUM_RISK_CALCULATION_MODE InpModeCalculationMdp = percentage; //Mode calculation Max Daily Profit input ENUM_APPLIED_PERCENTAGES InpAppliedPercentagesMdp = Balance;//MDP percentage applies to:
Quinta sección: modificadores de riesgo
En esta parte se declaran tres modificadores de riesgo.
- Risk Modifier 1: corresponde al modificador CDynamicRisk (tipo "booster") presentado al inicio del artículo.
- Risk Modifier 2: ajusta el riesgo según una lista de porcentajes. Si se encadenan pérdidas, aplica los niveles definidos. Si ocurre un takeprofit, el riesgo vuelve al inicial.
- Risk Modifier 3: modifica el riesgo por operación GMLPO únicamente si el rendimiento de la cuenta es negativo. Para el cálculo utiliza la función FunctionKevin.
inline double FunctionKevin(double defect_risk, double loss_percentage, double constante) { return (defect_risk * MathExp(loss_percentage / constante)); }
Código de la sección 5.
sinput group "" sinput group "-------| Risk Modifier |-------" input bool InpActivarModificadorDeRiesgo = false; // Enables dynamic risk adjustment (Booster) input double InpStepMod = 2.0; // Increment applied to risk each time the condition is met input double InpStart = 2.0; // Profit percentage from which risk adjustment begins input ENUM_MULTIPLIER_METHOD_DR InpMethodDr = DR_EXPONECIAL; // Type of progression used to increase risk sinput group "" sinput group "-------| Risk modifier 2 |-------" input bool InpActivarModificadorDeRiesgo2 = false; //Activate risk modifier 2 input string InpStrPercentagesToApplyRiesgoCts = "6, 8, 10, 20, 25"; //Percentages to apply sinput group "" sinput group "-------| Risk modifier 3 by Kevin |-------" input bool InpActivarModificadorDeRiesgo3 = false; //Activate risk modifier 3 input double InpConstante = 8.0; //Function constant
Sexta sección: sesión operativa
En esta sección se ubicarán los parámetros de la sesión operativa
sinput group "" sinput group "-------| Session |-------" input char InpPaSessionStartHour = 1; // Start hour to operate (0-23) input char InpPaSessionStartMinute = 0; // Start minute to operate (0-59) input char InpPaSessionEndHour = 23; // End hour to operate (1-23) input char InpPaSessionEndMinute = 0; // End minute to operate (0-59)
Séptima sección: parámetros de breakeven
En esta parte se ubicarán los parámetros del breakeven.
sinput group "" sinput group "-------| Breakeven |-------" input bool InpUseBe = true; // Enable Breakeven logic input ENUM_BREAKEVEN_TYPE InpTypeBreakEven = BREAKEVEN_TYPE_RR; // Calculation method (RR, ATR, or fixed points) input ENUM_VERBOSE_LOG_LEVEL InpLogLevelBe = VERBOSE_LOG_LEVEL_ERROR_ONLY; // Break Even log level: sinput group "--- Breakeven based on Risk/Reward (RR) ---" input string InpBeRrAdv = "Requires a configured Stop Loss."; // Warning: Stop Loss is required to calculate RR input double InpBeRrDbl = 1.0; // Risk/Reward ratio to activate Breakeven (e.g. 1.0) input ENUM_TYPE_EXTRA_BE_BY_RRR InpBeTypeExtraRr = EXTRA_BE_RRR_BY_ATR; // Method to adjust Breakeven price (ATR or points) input double InpBeExtraPointsRrOrAtrMultiplier = 1.0; // Adjustment value: ATR multiplier (atr 14 period) if method = ATR, or fixed points if method = Points sinput group "--- Breakeven based solely on ATR ---" input double InpBeAtrMultiplier = 2.0; // ATR multiplier to trigger Breakeven (atr 14 period) input double InpBeAtrMultiplierExtra = 1.0; // Additional multiplier for precise Breakeven adjustment (atr 14 period) sinput group "--- Breakeven based on fixed points ---" input int InpBeFixedPointsToPutBe = 200; // Minimum distance (in points) to trigger Breakeven input int InpBeFixedPointsExtra = 100; // Extra points added when Breakeven is triggered
Octava sección: cierres parciales
Por último, en esta sección se ubicarán los parámetros de la clase CPartials.
sinput group "" sinput group "-------| Partial Closures |-------" input ENUM_VERBOSE_LOG_LEVEL InpLogLevelPartials = VERBOSE_LOG_LEVEL_ALL; // Partial Closures log level input string InpComentVolumen4 = "Use '0' in both to disable. Order from lowest to highest."; input string InpPartesDelTpDondeSeTomaraParciales = "0"; // Percentage of TP distance where partials trigger input string InpComentParciales1 = "List of TP levels (%) to trigger partials."; //-> input string InpComentParciales2 = "Ex: \"25,50,75\" triggers at 25%, 50%, 75%."; //-> input string InpComentParciales3 = "String allows multiple dynamic levels."; //-> input string InpSeparatorPar = ""; // ------- input string InpVolumenQueSeQuitaraDeLaPosicionEnPorcentaje = "0"; // Volume to close at each defined level input string InpComentVolumen1 = "Percentage of total volume closed at each level."; //-> input string InpComentVolumen2 = "Must match count of TP level entries."; //-> input string InpComentVolumen3 = "Ex: \"30,40,30\" closes 30%, 40%, then 30%."; //
Sección global del EA
Concluida la parte de inputs, pasamos ahora a la sección global.
En este bloque se declaran las variables e instancias que serán utilizadas a lo largo del EA.
Primero, se define un puntero del tipo CRiskManagement para la gestión de riesgo, además de una instancia de CBreakEven y otra de CPartials.
CRiskManagemet *risk;
CBreakEven break_even(InpMagic, _Symbol);
CPartials g_partials; También se crean dos handles: uno para el indicador de order blocks y otro para el indicador de medias móviles.
//--- Handles int order_block_indicator_handle; int hanlde_ma;
Se declaran cuatro arrays de tipo double, que almacenarán la información obtenida de los buffers del indicador de order blocks.
//--- Global buffers double tp1[]; double tp2[]; double sl1[]; double sl2[];
Luego, se declaran dos variables de tipo datetime para guardar el inicio y fin de sesión.
//--- Session datetime start_sesion; datetime end_sesion;
Se instancian varios objetos CBarControler con el fin de controlar la apertura de velas en diferentes marcos temporales: actual, diario, semanal y mensual.
//--- General CBarControler bar_d1(PERIOD_D1, _Symbol); CBarControler bar_w1(PERIOD_W1, _Symbol); CBarControler bar_mn1(PERIOD_MN1, _Symbol); CBarControler bar_curr(PERIOD_CURRENT, _Symbol);
Posteriormente, se crean instancias de los modificadores de riesgo: uno dinámico (CDynamicRisk), uno por porcentajes (CModifierDynamicRisk) y uno basado en la función de Kevin (CMathDynamicRisk).
CDynamicRisk riesgo_c(InpStepMod, InpStart, LP_GMLPO, InpMethodDr); CModifierDynamicRisk risk_modificator(InpStrPercentagesToApplyRiesgoCts); CMathDynamicRisk risk_modificator_kevin(InpConstante);
Finalmente, se define una variable global "opera", que funciona como bandera para habilitar o no la operativa del EA, además de una instancia de la clase CAtrUltraOptimized, la cual implementa el cálculo del ATR.
bool opera = true; CAtrUltraOptimized atr_ultra_optimized;
Código completo.
CRiskManagemet *risk; CBreakEven break_even(InpMagic, _Symbol); CPartials g_partials; //--- Handles int order_block_indicator_handle; int hanlde_ma; //--- Global buffers double tp1[]; double tp2[]; double sl1[]; double sl2[]; //--- Session datetime start_sesion; datetime end_sesion; //--- General CBarControler bar_d1(PERIOD_D1, _Symbol); CBarControler bar_w1(PERIOD_W1, _Symbol); CBarControler bar_mn1(PERIOD_MN1, _Symbol); CBarControler bar_curr(PERIOD_CURRENT, _Symbol); CDynamicRisk riesgo_c(InpStepMod, InpStart, LP_GMLPO, InpMethodDr); CModifierDynamicRisk risk_modificator(InpStrPercentagesToApplyRiesgoCts); CMathDynamicRisk risk_modificator_kevin(InpConstante); //--- bool opera = true; CAtrUltraOptimized atr_ultra_optimized;
OnInit
Dentro de la función OnInit se configuran todos los elementos principales del EA: cálculo de ATR, breakeven, parciales, indicadores, gestión de riesgo, modificadores y buffers globales.
Configuración del ATR
Primero se inicializa la clase CAtrUltraOptimized, asignándole el período, el símbolo y los parámetros internos.
//--- Atr atr_ultra_optimized.SetVariables(_Period, _Symbol, 0, 14); atr_ultra_optimized.SetInternalPointer();
Configuración del breakeven
Si la opción de breakeven está activada, se establecen los diferentes métodos de cálculo: por ATR, por puntos fijos y por risk/reward.
Además, se agrega el nivel de log.
//--- We set the breakeven values so its use is allowed if(InpUseBe) { break_even.SetBeByAtr(InpBeAtrMultiplier, InpBeAtrMultiplierExtra, GetPointer(atr_ultra_optimized)); break_even.SetBeByFixedPoints(InpBeFixedPointsToPutBe, InpBeFixedPointsExtra); break_even.SetBeByRR(InpBeRrDbl, InpBeTypeExtraRr, InpBeExtraPointsRrOrAtrMultiplier, GetPointer(atr_ultra_optimized)); break_even.SetInternalPointer(InpTypeBreakEven); break_even.obj.AddLogFlags(InpLogLevelBe); }
Configuración de cierres parciales
Se inicializa la clase CPartials. Si la función Init devuelve verdadero, la instancia se agrega a la gestión de cuenta mediante account_status.AddItemFast().
//--- Partials g_partials.AddLogFlags(InpLogLevelPartials); if(g_partials.Init(InpMagic, _Symbol, InpVolumenQueSeQuitaraDeLaPosicionEnPorcentaje, InpPartesDelTpDondeSeTomaraParciales)) { account_status.AddItemFast(&g_partials); }
Nota: En el caso de breakeven no se hace este paso manual, porque la propia clase ya llama a la función pública de account_status para registrarse.
Creación de indicadores
Se crean los handles para el indicador de order blocks y para la media móvil exponencial. Si el handle de la EMA es inválido, se devuelve un error en la inicialización.
//--- Indicators // Create ob handle order_block_indicator_handle = CreateIndicatorObHandle(); // Create ma handle hanlde_ma = iMA(_Symbol, InpTimeframeOrderBlock, 30, 0, MODE_EMA, PRICE_CLOSE); // Check ma if(hanlde_ma == INVALID_HANDLE) { Print("The ema indicator is not available latest error: ", _LastError); return INIT_FAILED; } ChartIndicatorAdd(0, 0, hanlde_ma);
Configuración de gestión de riesgo
En esta nueva versión, la gestión de riesgo se configura con ayuda de la clase CRiskPointer.
Primero se crea un puntero temporal manager, que recibe como parámetros el número mágico y el modo de obtención del lote.
CRiskPointer* manager = new CRiskPointer(InpMagic, InpGetMode); Si el modo de gestión es propfirm, se asigna el balance correspondiente.
manager.SetPropirm(InpPropFirmBalance);
Después, se obtiene el puntero para asignarlo a risk desde el manager.
risk = manager.GetRiskPointer(InpRiskMode);
Se añaden las banderas de log y se establece el puntero de CGetLote.
risk.AddLogFlags(InpLogLevelRiskManagement);
risk.SetLote(CreateLotePtr(_Symbol)); Configuración de pérdidas y ganancias máximas
Se añaden las reglas de Maximum Daily Loss, GMLPO, Maximum Loss, Maximum Weekly Loss y Maximum Daily Profit.
// We set the parameters string to_apply = InpStrPercentagesToApply, to_modfied = InpStrPercentagesToBeReviewed; if(InpModeGmlpo == DYNAMIC_GMLPO_FIXED_PARAMETERS) SetDynamicUsingFixedParameters(InpBalancePercentageToActivateTheRisk1, InpBalancePercentageToActivateTheRisk2, InpBalancePercentageToActivateTheRisk3 , InpBalancePercentageToActivateTheRisk4, InpPercentageToBeModified1, InpPercentageToBeModified2, InpPercentageToBeModified3, InpPercentageToBeModified4 , to_modfied, to_apply); risk.AddLoss(InpPercentageOrMoneyMdlInput, InpAppliedPercentagesMdl, InpModeCalculationMdl, LP_MDL); risk.AddLoss(InpPercentageOrMoneyGmlpoInput, InpAppliedPercentagesGmlpo, InpModeCalculationGmlpo, LP_GMLPO, CLOSE_POSITION_AND_EQUITY, (InpModeGmlpo != NO_DYNAMIC_GMLPO), to_modfied, to_apply); risk.AddLoss(InpPercentageOrMoneyMlInput, InpAppliedPercentagesMl, InpModeCalculationMl, LP_ML); risk.AddLoss(InpPercentageOrMoneyMwlInput, InpAppliedPercentagesMwl, InpModeCalculationMwl, LP_MWL); risk.AddProfit(InpPercentageOrMoneyMdpInput, InpAppliedPercentagesMdp, InpModeCalculationMdp, LP_MDP, InpMdpIsStrict); risk.EndAddProfitLoss(); // Execute this every time we finish setting the maximum losses and profits
Si el tipo de gestión de riesgo es "propfirm dinámico", se configura la actualización del Maximum Daily Loss.
//--- If the risk-management type is prop we need to configure whether to update the MDL (as FTMO) if(risk.ModeRiskManagement() == risk_mode_propfirm_dynamic_daiy_loss) { CRiskManagemetPropFirm* temp_ptr = dynamic_cast<CRiskManagemetPropFirm *>(risk); // Convert from normal to prop firm temp_ptr.UpdateLoss(InpUpdateDailyLossRiskModeProp); }
Agregar modificadores de riesgo
Se añaden los modificadores activos al puntero de riesgo.
// We add modifiers if(InpActivarModificadorDeRiesgo) risk.AddModificator(riesgo_c); if(InpActivarModificadorDeRiesgo2) risk.AddModificator(risk_modificator); if(InpActivarModificadorDeRiesgo3) risk.AddModificator(risk_modificator_kevin);
Luego se registra la gestión de riesgo en account_status y se elimina el puntero temporal.
// We finish by adding it account_status.AddItemFast(risk); // We delete the temporary pointer delete manager;
Inicialización de account_status
Se llama al evento de inicialización de la instancia global account_status.
//--- We initialize the account
account_status.AddLogFlagTicket(InpLogLevelAccountStatus);
account_status.AddLogFlags(InpLogLevelAccountStatus);
account_status.OnInitEvent(); Configuración de arrays globales
Finalmente, se establecen los arrays que almacenarán los datos de los buffers como series temporales.
//--- We configure the arrays in series ArraySetAsSeries(tp1, true); ArraySetAsSeries(tp2, true); ArraySetAsSeries(sl1, true); ArraySetAsSeries(sl2, true); return(INIT_SUCCEEDED);
OnTick
Dentro de la función OnTick se organiza la ejecución general del asesor experto.
Primero, se crea una variable local time_curr con el valor del tiempo actual del símbolo. También se invoca la macro CAccountStatus_OnTickEvent, que actualiza el beneficio actual de la cuenta. Este dato lo usa "CRiskManagement" para verificar si una pérdida o ganancia máxima ha sido alcanzada.
Después se llaman las funciones de account_status: OnNewDay, OnNewWeek y OnNewMonth. Este bloque se ejecuta cada nueva vela diaria y reinicia la variable global opera en true.
//--- General const datetime time_curr = TimeCurrent(); CAccountStatus_OnTickEvent //--- Code that runs every new day if(bar_d1.IsNewBar(time_curr)) { account_status.OnNewDay(); if(bar_w1.IsNewBar(time_curr)) account_status.OnNewWeek(); if(bar_mn1.IsNewBar(time_curr)) account_status.OnNewMonth(); opera = true; }
A continuación se comprueba la sesión de trabajo. Si la hora actual supera el fin de la sesión, se calculan los valores de inicio y cierre de la siguiente.
//--- Check Session if(time_curr > end_sesion) { start_sesion = HoraYMinutoADatetime(InpPaSessionStartHour, InpPaSessionStartMinute, time_curr); end_sesion = HoraYMinutoADatetime(InpPaSessionEndHour, InpPaSessionEndMinute, time_curr); if(start_sesion > end_sesion) end_sesion = end_sesion + 86400; }
Luego se ejecutan las funciones principales de los objetos globales "break_even" y "g_partials". Si opera es falso, se devuelve sin continuar la ejecución.
//--- Breakeven if(InpUseBe) break_even.obj.BreakEven(); //--- Partials g_partials.CheckTrackedPositions(); //--- Check to operate if(!opera) return;
Después se entra en la lógica principal de la estrategia. Aquí se revisa si hay una nueva vela y si el tiempo actual está dentro de la sesión configurada. Si no existen posiciones abiertas, se copian los buffers del indicador y se preparan los valores de takeprofit y stoploss. Con esas referencias se decide si corresponde abrir una orden de compra o una de venta.
//--- Strategy if(bar_curr.IsNewBar(time_curr)) { if(time_curr > start_sesion && time_curr < end_sesion) { if(risk.GetPositionsTotal() == 0) { CopyBuffer(order_block_indicator_handle, 2, 0, 5, tp1); CopyBuffer(order_block_indicator_handle, 3, 0, 5, tp2); CopyBuffer(order_block_indicator_handle, 4, 0, 5, sl1); CopyBuffer(order_block_indicator_handle, 5, 0, 5, sl2); if(tp1[0] > 0 && tp2[0] > 0 && sl1[0] > 0 && sl2[0] > 0) { if(tp2[0] > sl2[0]) // buy orders { const double ASK = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); risk.SetStopLoss(ASK - sl1[0]); const double lot = (InpLoteType == Dinamico ? risk.GetLote(ORDER_TYPE_BUY, ASK, 0, 0) : InpLote); tradep.Buy(lot, _Symbol, ASK, sl1[0], tp2[0], "Order Block EA Buy"); } else if(sl2[0] > tp2[0]) // sell orders { const double BID = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); risk.SetStopLoss(sl1[0] - BID); const double lot = (InpLoteType == Dinamico ? risk.GetLote(ORDER_TYPE_SELL, BID, 0, 0) : InpLote); tradep.Sell(lot, _Symbol, BID, sl1[0], tp2[0], "Order Block EA Sell"); } } } } }Finalmente, se revisan las condiciones de pérdida o ganancia máxima. Si alguna se supera, se cierran las posiciones o se ejecutan las acciones definidas según el modo de riesgo configurado.
//--- Checking maximum losses and profits if(account_status_positions_open) { if(risk[LP_ML].IsSuperated()) { if(InpRiskMode == risk_mode_propfirm_dynamic_daiy_loss) { Print("The expert advisor lost the funding test"); Remover(); } else { risk.CloseAllPositions(); Print("Maximum loss exceeded now"); opera = false; } } if(risk[LP_MDL].IsSuperated()) { risk.CloseAllPositions(); Print("Maximum daily loss exceeded now"); opera = false; } if(risk[LP_MDP].IsSuperated()) { risk.CloseAllPositions(); Print("Excellent Maximum daily profit achieved"); opera = false; } }
OnTradeTransaction
En esta función se procesa cada transacción realizada por la cuenta. Para ello se llama al método público "OnTradeTransactionEvent" de la instancia global account_status.
//+------------------------------------------------------------------+ //| TradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction & trans, const MqlTradeRequest & request, const MqlTradeResult & result) { account_status.OnTradeTransactionEvent(trans); }
OnDeinit
En la fase de desinicialización se eliminan los indicadores cargados en el gráfico actual. También se liberan los handles con la función IndicatorRelease, evitando que queden recursos en memoria después de detener el asesor experto.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- ChartIndicatorDelete(0, 0, ChartIndicatorName(0, 0, GetMovingAverageIndex())); ChartIndicatorDelete(0, 0, "Order Block Indicator"); if(hanlde_ma != INVALID_HANDLE) IndicatorRelease(hanlde_ma); if(order_block_indicator_handle != INVALID_HANDLE) IndicatorRelease(order_block_indicator_handle); }
Backtesting del bot de order blocks: ¿los parciales mejoran significativamente el rendimiento?
Antes de comparar los resultados del asesor experto con y sin el uso de parciales, veamos primero cómo se ejecutan en la práctica dentro del código.
Para esta comprobación configuramos el parámetro "Percentage of TP distance where partials trigger" con el valor "25, 45, 65", y el parámetro "Volume to close at each defined level" con el valor "30, 30, 30". Como el objetivo es analizar la ejecución de los cierres parciales, se activaron todos los niveles de log relacionados con ellos.

Figura 5: Configuración de los parciales para el 1.er test
Con la configuración lista, realizamos un test en el par oro, marco temporal M5, desde el 01/01/2024 hasta el 22/09/2023 en modo de ticks reales.
Cada vez que el asesor ejecuta una operación, se imprimen los niveles de takeprofit. En este caso, como definimos tres niveles, se muestran tres precios. Al revisarlos, observamos que en una venta el precio indexado como nivel 0 es el mayor, mientras que el nivel 2 es el menor. Esto es lo esperado, ya que en posiciones de venta los niveles de salida son descendentes.

Figura 6: Impresión del ea sobre los niveles de tp en una posición de venta
Posteriormente, en los registros se observa que la posición se cerró parcialmente.
En este ejemplo, se cerraron 0.05 lotes de la operación abierta.

Figura 7: Impresión del ea sobre un cierre parcial que se ejecuto exitosamente
Ese valor de 0.05 se obtiene de la siguiente manera: la operación inicial tenía un tamaño de 0.15 lotes. Al aplicarse el primer parcial, el cálculo es 0.15 * 0.30 = 0.045. Sin embargo, en este broker el paso mínimo de volumen es de 0.01, por lo que no es posible cerrar exactamente 0.045 lotes.
El código de cierre parcial contiene una validación para este caso, donde el volumen calculado se redondea al múltiplo más cercano permitido por el step:
volume_to_close = RoundToStep(volume_to_close, m_volume_step);
Con este ajuste, el valor final a cerrar se convierte en 0.05, que coincide con lo ejecutado en la práctica.
Con esta prueba verificamos que los cierres parciales funcionan de manera correcta. Ahora pasaremos a lo más interesante de la sección: comparar el rendimiento del asesor con parciales frente a la ejecución sin ellos.
El backtest lo llevaremos a cabo con estas configuraciones:

Figura 8: Configuración del backtest utilizado en el test 1 y 2
En estas pruebas no se aplicaron límites de ganancia o pérdida diaria. El único límite activo fue GMLPO (10 %). Tanto el takeprofit como el stoploss se mantuvieron iguales en ambos escenarios, además configuraremos la sesión operativa de 3 a 14, siendo la activación de los parciales la única diferencia entre los dos backtests.
Primer test: comparación con y sin parciales
En la primera prueba se utilizó un stoploss de 2.0 y un takeprofit de 6.0 (tp y sl por atr, definidos como multiplicadores de ATR).

Figura 9: Backtest del bot sin parciales activados
Al comparar este resultado con el del backtest usando parciales:

Figura 10: Backtest del bot con parciales activados
Podemos observar que el rendimiento del bot sin parciales fue superior, alcanzando un balance final mayor en aproximadamente 4000 USD. La principal razón de esta diferencia es el costo adicional en comisiones generado por cada cierre parcial.

Figura 11: Comisiones cobradas en el test con parciales
Esto era previsible, ya que en escenarios de tipo scalper, donde los objetivos de ganancia y pérdida son pequeños, el impacto de las comisiones se vuelve más notorio.
Segundo test: marcos amplios con enfoque swing
En la segunda prueba se configuró un stoploss de 4.0 y un takeprofit de 25.0. Estos valores se ajustan más a un estilo de operación swing, donde las operaciones pueden durar varios días o incluso semanas.
Backtest con el uso de parciales.

Figura 12: Backtest del bot con parciales activados
Backets sin el uso de parciales

Figura 13: Backtest del bot sin parciales activados
La diferencia en este escenario es mucho más marcada. El backtest con parciales terminó con un balance aproximado de 33 000 USD, mientras que el backtest sin parciales cerró cerca de 22 234 USD. La diferencia supera los 11 000 USD.
Este resultado se explica porque, al tener un takeprofit más amplio, la probabilidad de que el precio lo alcance en su totalidad disminuye. En consecuencia, los cierres parciales permiten asegurar beneficios intermedios. Un ejemplo claro se observó en diciembre de 2024: en el backtest sin parciales, la curva de equidad muestra un gran pico que finalmente no se consolidó porque la operación no alcanzó el takeprofit completo. En cambio, en el backtest con parciales, esa misma operación sí generó beneficios parciales, lo que dio como resultado que dicha operación se cerrara parcialmente, por lo que se pudieron obtener beneficios de dicha operación.
Podemos finalizar estos test hechos, concluyendo que el impacto de los cierres parciales depende del estilo de la estrategia. En enfoques de tipo scalper, donde los objetivos de stoploss y takeprofit son reducidos, las comisiones adicionales suelen afectar el rendimiento.
En cambio, en un enfoque swing, donde los objetivos son más amplios y el mercado puede tardar días o semanas en alcanzar el takeprofit completo, los cierres parciales se muestran beneficiosos. Al asegurar ganancias intermedias, permiten mejorar la curva de balance y reducir la exposición a movimientos adversos que impiden llegar al objetivo final.
Si bien este comportamiento se observó en nuestro EA, no necesariamente se repetirá en todos los asesores expertos. Por ello, la recomendación es probar los cierres parciales en cada estrategia concreta y evaluar si aportan valor en el rendimiento general.
Por lo que invito a los lectores a implementar el uso de parciales en sus eas y comprueben por sí mismos, si los cierres parciales implementados en este artículo han logrado mejorar e incluso duplicar los beneficios de sus asesores expertos.
Conclusion
En este artículo vimos todas las mejoras aplicadas en la gestión de riesgo, además de construir paso a paso una clase en MQL5 para manejar los cierres parciales. Finalizamos con un test comparativo del EA de Order Blocks con y sin el uso de parciales.
En un inicio, los resultados parecían indicar que los parciales no aportaban mejoras, e incluso empeoraban el rendimiento.
Sin embargo, al realizar un segundo test con configuraciones más amplias, orientadas a un estilo de trading swing, se evidenció que los beneficios de los cierres parciales comienzan a notarse con claridad. En este escenario, la diferencia en balance final superó los 11 000 USD, mostrando que para estrategias de mayor plazo los parciales pueden ayudar a maximizar y asegurar beneficios.
También quiero mencionar que, para este y otros artículos anteriores, he creado un repositorio público en mql algo forge donde están todos los códigos tratados: el indicador de Order Blocks, las librerías de gestión de riesgo, breakeven, parciales y otros ejemplos prácticos. El repositorio será constantemente actualizado y mejorado.
Clic aquí para acceder al repositorio público.
| Carpeta | Archivos | Descripción |
|---|---|---|
| Examples | - Get_Lot_By_Risk_Per_Trade_and_SL.mq5 - Get_Sl_by_risk_per_operation_and_lot.mq5 - Risk_Management_Panel.mq5 | Ejemplos prácticos del uso de la librería de gestión de riesgo (RM). |
| OrderBlock | - Main.mqh - Order Block EA MetaTrader 5.mq5 - OrderBlockIndPart2.mq5 | Contiene el indicador y el bot de Order Blocks usados como ejemplos en la serie de artículos de gestión de riesgo, breakeven y parciales. |
| PosManagement | - Breakeven.mqh - Partials.mqh | Librerías específicas para la gestión de posiciones: breakeven y cierre parcial. |
| RM | - AccountStatus.mqh - LossProfit.mqh - LoteSizeCalc.mqh - Modificators.mqh - OcoOrder.mqh - OrdersGestor.mqh - RiskManagement.mqh - RiskManagementBases.mqh - RM_Defines.mqh - RM_Functions.mqh | Todos los módulos que conforman la librería de gestión de riesgo RM. |
| Utils | - FA \ - Atr.mqh - AtrCts.ex5 - BarControler.mqh - ClasesBases.mqh - Events.mqh - FuncionesBases.mqh - Managers.mqh - SimpleLogger.mqh - Sort.mqh - StringToArray.mqh - CustomOptimization.mqh - Fibonacci.mqh - File.mqh - Funciones Array.mqh - Objectos 2D.mqh - RandomSimple.mqh | Librería de utilidades para la creación de librerías, EAs e indicadores. Incluye funciones para trabajar con arrays, tiempo, conversiones, strings, matemáticas simples, además de clases para manejo de patrones de velas, ATR optimizado, validación de suspensión del PC, entre otros. |
| Sets | - Article_Partials \ - ARTICLE_PARTIAL_SET_TEST_2_OB_WITHOUT_PARTIALS.set - ARTICLE_PARTIAL_SET_OB_TEST_2_WITH_PARTIALS.set - ARTICLE_PARTIAL_SET_TEST_1_WITH_PARTIALS.set - ARTICLE_PARTIAL_SET_TEST_1_WITHOUT_PARTIALS.set | Carpeta que contiene los sets utilizados en el test 1y 2. |
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
La estrategia comercial de captura de liquidez
Dominando los registros (Parte 3): Exploración de controladores para guardar registros
Sistemas neurosimbólicos en trading algorítmico: Combinación de reglas simbólicas y redes neuronales
Modelos ocultos de Markov para la predicción de la volatilidad siguiendo tendencias
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso