MQLArticles/PosManagement/Partials.mqh

455 lines
39 KiB
MQL5
Raw Permalink Normal View History

2025-09-25 11:35:54 -05:00
<EFBFBD><EFBFBD>//+------------------------------------------------------------------+
//| 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<double, double> 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 "," |
//+-------------------------------------------------------------------------+
2025-09-27 18:24:30 -05:00
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)
{
2025-09-27 18:24:30 -05:00
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);
2025-09-27 18:24:30 -05:00
}
else
{
out_tp_percentages_string = "0";
out_volume_percentages_string = "0";
2025-09-27 18:24:30 -05:00
}
}
2025-09-25 11:35:54 -05:00
//+------------------------------------------------------------------+
#endif
//+------------------------------------------------------------------+