454 lines
39 KiB
MQL5
454 lines
39 KiB
MQL5
//+------------------------------------------------------------------+
|
||
//| 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 "," |
|
||
//+-------------------------------------------------------------------------+
|
||
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
|
||
//+------------------------------------------------------------------+
|