//+------------------------------------------------------------------+ //| Partials.mqh | //| Copyright 2025, Niquel Mendoza. | //| https://www.mql5.com/es/users/nique_372/news | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, Niquel Mendoza." #property link "https://www.mql5.com/es/users/nique_372/news" #property strict #ifndef PARTIALS_MQH #define PARTIALS_MQH #include "..\\RM\\RiskManagement.mqh" //+------------------------------------------------------------------+ //| Structures | //+------------------------------------------------------------------+ struct PartialTakeProfit { double partial_price; double tp_level_percentage; double volume_percentage_to_close; inline void Reset() { ZeroMemory(this); } }; struct TrackedPosition { PartialTakeProfit tp_levels[]; ulong ticket; ENUM_POSITION_TYPE type; int current_partial_index; }; //+------------------------------------------------------------------+ //| Defines | //+------------------------------------------------------------------+ #define CPARCIAL_RESERVE_ARR 5 #define CPARCIAL_RESERVE_TO_DELETE 5 #define PARTIAL_NO_APPLIED "0" //+------------------------------------------------------------------+ //| Class that implements partial closures | //+------------------------------------------------------------------+ class CPartials : public CAccountGestor { private: //--- 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 //--- 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 //--- 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 //--- Arrays to take, both arrays must be the same size. double m_volume_percentages_to_close[]; double m_tp_level_percentages[]; //--- Temporary hashmap CHashMap m_temporary_hashmap; //--- Function to add a new partial to the internal array void AddToTrackedPositions(TrackedPosition & new_tracked_position); public: CPartials(); ~CPartials(); //--- Initialize function bool Init(ulong magic_number_, string symbol_, string volume_percentages_string, string tp_percentages_string); //--- Main function for position review void CheckTrackedPositions(); //--- Function that will be automatically invoked by account status each time a position is opened or closed void OnOpenClosePosition(const ROnOpenClosePosition &pos) override; //--- 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); //--- Function that returns true if partials are allowed to be used inline bool IsPartialsEnabled() const { return m_disable_partials_flag == false; } }; //+------------------------------------------------------------------+ //| Constructor and Destructor | //+------------------------------------------------------------------+ 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) { } //+------------------------------------------------------------------+ CPartials::~CPartials() { // Empty destructor } //+------------------------------------------------------------------+ //| Initialization function | //+------------------------------------------------------------------+ 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; } //--- 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); //--- Initial resize ArrayResize(m_tracked_positions, 0, CPARCIAL_RESERVE_ARR); ArrayResize(m_indices_to_remove, 0, CPARCIAL_RESERVE_TO_DELETE); //--- Convert strings to double arrays StrTo::CstArray(m_volume_percentages_to_close, volume_percentages_string, ','); StrTo::CstArray(m_tp_level_percentages, tp_percentages_string, ','); //--- 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; } //--- 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"); } //--- 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)); //--- 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; } //--- 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; } //+--------------------------------------------------------------------+ //| 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); } } //+------------------------------------------------------------------+ //| Adds a PositionParcial type structure to the main array | //+------------------------------------------------------------------+ void CPartials::AddToTrackedPositions(TrackedPosition &new_tracked_position) { AddArrayNoVerification2(m_tracked_positions, new_tracked_position, CPARCIAL_RESERVE_ARR) } //+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+ //| 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); } //+-------------------------------------------------------------------------+ //| Receives 2 aligned pairs of tp - volume, as output sets | //| the two strings passed by reference with the values ​​separated by "," | //+-------------------------------------------------------------------------+ void PartialSet(bool use_partials, double volume_percentage1, double tp_percentage1, double volume_percentage2, double tp_percentage2, string &out_volume_percentages_string, string &out_tp_percentages_string) { if(use_partials) { out_tp_percentages_string = StringFormat("%.2f, %.2f", tp_percentage1, tp_percentage2); out_volume_percentages_string = StringFormat("%.2f, %.2f", volume_percentage1, volume_percentage2); } else { out_tp_percentages_string = "0"; out_volume_percentages_string = "0"; } } //+------------------------------------------------------------------+ #endif //+------------------------------------------------------------------+