MQLArticles/PosManagement/Partials.mqh

454 lines
39 KiB
MQL5
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+