Risk Management (Part 4): Completing the Key Class Methods
- Introduction
- Article summary
- New enums and structures
- Constructor and main function improvements
- New class variables
- Managing open positions
- Methods for handling limit exceedances
- Event handling
- Functions for dynamic risk
- Building dynamic risk
- Conclusion
Introduction
Continuing from the previous article on risk management, we will explain the main variables and initial methods for our specialized class. This time we will focus on completing the methods needed to check whether established maximum loss or profit limits have been exceeded. In addition, we will present two dynamic approaches to per-trade risk management.
Article summary
We will start by optimizing some functions, as well as the class constructor and destructor. Then we will define new structures, enums, and core constants. Next, we will implement functions for managing positions opened by the Expert Advisor, including mechanisms for detecting when established maximum profit or loss values are exceeded. Finally, we will add methods for managing dynamic per-trade risk.
New enums and structures
Let’s start by defining the new structures, enums, and constants we will use later in this article.
1. Dynamic per-trade risk
As mentioned earlier, I added a new concept: dynamic per-trade risk.
What is it?
Dynamic per-trade risk is not a fixed value but a variable that adjusts based on profit or loss relative to a specified initial balance. This can be especially useful for protecting a trading account. For example, if the balance decreases to a certain percentage of the initial balance, risk can be automatically adjusted to minimize further losses. This means that each time a specified level is reached, per-trade risk adjusts.
Let’s consider a practical example shown in the table. Assume the initial account balance is $10,000.
Let’s choose the following thresholds for adjusting risk: 3%, 5%, and 7%.
| Condition | New risk % |
|---|---|
| If the balance drops by 3% ($300, that is, below $9,700) | Risk adjusts to 0.7%. |
| If the balance drops by 5% ($500, that is, below $9,500) | Risk adjusts to 0.5%. |
| If the balance drops by 7% ($700, that is, below $9,300) | Risk adjusts to 0.25%. |
Important notes on dynamic risk parameters and operation:
- All values mentioned in this article are fully customizable parameters that can be adjusted according to the user's needs and preferences. This includes both the thresholds that trigger risk adjustments and the initial balance to which these percentages apply.
The initial balance can be determined in two ways:
- PropFirm account (e.g., FTMO): If this option is selected via risk management parameter (propfirm_ftmo), the initial balance must be entered manually via an input parameter in Expert Advisor settings. This balance remains fixed throughout trading, meaning it does not change over time and is a static value set by the user at start.
- Personal account: If account type personal_account is selected, the initial balance is automatically determined by the Expert Advisor when calling AccountInfoDouble(), which returns the current account balance at system initialization. In this case, the balance is dynamic and accurately reflects the current account funds.
- Significantly reduces risk of blowing up the account or losing trading capital entirely.
- Increases overall trading safety by reducing exposure to extended losing streaks.
Disadvantages of dynamic per-trade risk:
-
Recovery after a losing streak or significant drawdown can be slower due to preemptive exposure reduction. Although recovery is slower, it is safer and more controlled.
Structure for implementing dynamic per-trade risk
To manage and configure dynamic per-trade risk, we must access and modify the assigned_percentage property within the gmlpo variable. To implement these automatic balance-based adjustments, we must create a structure containing two main elements:
- Specific balance at which risk adjustment triggers (e.g., when the balance drops to $9,700 from initial $10,000, as shown in table).
- New risk percentage applied to balance upon reaching the specified level (e.g., adjust risk to 0.7% in this case).
Initially, you might consider creating a simple structure with two separate double variables, but this approach is insufficient. Instead, we use two separate arrays within the structure.
Why use arrays instead of individual variables?
We use arrays instead of individual variables because we must sort multiple related values efficiently. Sorting is required due to the specific method we use for efficient dynamic risk management. Specifically, we use ArraySort() function to sort balances at which risk adjustments trigger (balance_to_activate_the_risk[]).
Why sorting with ArraySort() is required
The main reason is that we implement a loop-free method designed to continuously check whether the current balance has crossed specific thresholds that trigger risk adjustments. This loop-free approach is chosen to optimize system performance and speed, which is critical when performing frequent checks (e.g., on each tick or when closing each trade).
If values are not sorted correctly (i.e., in ascending order), serious issues may arise. Let’s consider a practical example to clarify this.
Let’s assume we initially define the following thresholds:
- First threshold: 3% (activation balance: $9,700)
- Second threshold: 7% (activation balance: $9,300)
- Third threshold: 5% (activation balance: $9,500)
As you can see, these values are sorted incorrectly (second threshold is greater than third). The problem with incorrect ordering is due to our loop-free method using an integer variable (counter) to track current risk state.
Let’s imagine what will happen:
- When the initial balance drops by 3% (to $9,700), the counter increases by one, adjusting risk to 0.7%.
- Then, when the balance drops to the next level (7%), the account reaches $9,300 and the counter increases again, skipping the intermediate value (5% at balance of $9,500).
- The intermediate value ($9,500) remains unused, creating confusion and serious calculation issues because dynamic risk is not adjusted properly.
Moreover, if the account starts recovering from the lowest level (in this case mistakenly from 5%), it must exceed $9,300 to return to the previous value, which is incorrect. But since the original method did not account for order correctly, recovery will also not work properly, causing additional errors.
For these reasons, proper sorting is crucial to ensure optimal functioning of the loop-free method. The simplest structure we suggest looks as follows:
struct Dynamic_gmlpo { double balance_to_activate_the_risk[]; double risk_to_be_adjusted[]; };
However, although this structure is simple, an important limitation remains. Since these two arrays represent key-value pairs (balance – adjusted risk percentage), maintaining this mapping during sorting is crucial. This is where the CHashMap structure comes into play.
The implementation with CHashMap allows directly linking each specific balance with its relevant risk percentage. When sorting the main array (balances), mapping to the relevant risk is automatically preserved. This guarantees absolute accuracy of all operations and calculations.
So, what we have implemented so far is a simple initial solution using two separate arrays, which allows us to temporarily simplify application of the ArraySort() method. However, for a more reliable and accurate implementation, especially if you plan to process multiple key-value pairs efficiently, we recommend using CHashMap. This structure ensures correct mapping of each balance to its relevant adjusted risk, which helps avoid potential errors when sorting and querying dynamic values.
In the following article, we will examine how to implement this solution with CHashMap, providing practical examples and step-by-step explanations.
Balance check
Continuing with the enums needed for proper dynamic risk implementation, we must add two additional options that clearly define when the balance check is performed. This check is key because it determines the exact moment to assess whether the current balance has fallen below predefined percentage thresholds, thereby activating a change in dynamic risk.
There are two main check types:
-
Check at every market tick: In this method, the check is performed continuously with every price movement (tick). This option provides high accuracy because it continuously checks whether the equity (current balance) has fallen below a specific threshold. However, there is a significant drawback: constant comparison with the current equity can lead to awkward or inefficient situations.
For example, let’s assume the initial balance is $10,000 and the first risk activation level occurs when the balance reaches $9,700. If the equity fluctuates between $9,701 and $9,699, dynamic risk redefinition will trigger and deactivate multiple times, which is not only inconvenient but also creates excessive strain on system resources due to high check frequency.
-
Check when closing trades: This second method performs the check only when closing a trade, not on every tick. This option is more efficient in terms of resource usage because the balance is checked only at specific moments. However, it may be less accurate since the check is performed only at trade closing and may not account for significant intermediate fluctuations that could trigger a timely change in dynamic risk.
To make the choice between these two methods easier, we will define them clearly in code using an enum:
//--- enum ENUM_REVISION_TYPE { REVISION_ON_CLOSE_POSITION, //Check GMLPO only when closing positions REVISION_ON_TICK //Check GMLPO on all ticks };
This gives users a clear choice of how and when to perform balance checks, allowing them to customize dynamic risk behavior based on their individual needs and preferences regarding system performance and accuracy.
Dynamic per-trade risk (GMLPO) mode
To manage the mode in which dynamic per-trade risk (GMLPO) is applied, we will use a special enum offering three clearly distinct options. Next, I will explain the purpose of each option and why I decided to implement them using an enum.
Initially, user-entered text strings were used to set specific percentages at which risk should change and to specify new risk values. In this original method, the user had to manually enter negative balance percentage values in one line (string input) that would trigger a risk change, and in another line, the new risk values to be applied. Although the approach was quite workable, it had a significant drawback: text strings could not be automatically optimized using the Expert Advisor optimization function. This made the process of setting up and testing various scenarios slow, impractical, and quite tedious.
To overcome these limitations, I decided to implement an enum allowing easy selection between three clearly defined modes, providing flexibility and convenience in optimization:
-
DYNAMIC_GMLPO_FULL_CUSTOM: This mode is fully customizable and allows the user to manually set multiple risk activation levels and the associated new percentage values. Although this mode maintains the use of text strings, the user can specify the desired number of changes, achieving maximum flexibility at the expense of automatic optimization capabilities.
-
DYNAMIC_GMLPO_FIXED_PARAMETERS: This mode significantly simplifies dynamic risk configuration by limiting the maximum number of allowable changes to four. Here the user directly specifies negative balance percentages and their relevant risk percentages via numeric parameters, which makes optimization much easier. This option provides a balance between customization flexibility and automated testing/optimization performance.
-
NO_DYNAMIC_GMLPO: This mode completely disables the dynamic risk function. It is ideal for users who prefer to maintain a fixed risk throughout trading without dynamic changes based on balance fluctuations.
//--- enum ENUM_OF_DYNAMIC_MODES_OF_GMLPO { DYNAMIC_GMLPO_FULL_CUSTOM, //Customisable dynamic risk per operation DYNAMIC_GMLPO_FIXED_PARAMETERS,//Risk per operation with fixed parameters NO_DYNAMIC_GMLPO //No dynamic risk for risk per operation };
Such an enum-based implementation provides clarity, convenience, and the ability to easily optimize various configurations, allowing quick identification of the best solution according to specific user preferences and strategies.
Trade accounting
To improve our risk management system, we will add a special feature allowing precise tracking of all open positions in the account, regardless of whether they were opened manually by a user or Expert Advisor.
For positions opened by the Expert Advisor, we will also be able to clearly determine whether a specific ticket matches the magic number assigned by that Expert Advisor. This is especially useful when you need to know exactly how many positions are opened by this Expert Advisor and easily distinguish them from positions opened manually.
To achieve this goal, we will define a simple yet effective structure that will retain the essential information about each position:
struct Positions { ulong ticket; //position ticket ENUM_POSITION_TYPE type; //position type };
Exceeding maximum profit or loss limits
Now we will add a separate enum indicating which criteria will be taken into account to determine whether the previously established maximum profit or loss limit has been exceeded.
The function responsible for performing this check will return true when specified conditions are met according to the selected criterion.
The enum will consist of three clearly distinct options:
//--- Mode to check if a maximum loss or gain has been exceeded enum MODE_SUPERATE { EQUITY, //Only Equity CLOSE_POSITION, //Only for closed positions CLOSE_POSITION_AND_EQUITY//Closed positions and equity };
-
EQUITY:
This option evaluates solely the current account equity (i.e., the real-time balance considering open and closed positions). It does not take into account profit or loss from trades already closed during the current day. The function will indicate that the maximum profit or loss limit has been exceeded only if the real-time equity directly exceeds the established limit. -
CLOSED_POSITIONS:
This method takes into account only profit or loss from positions closed during the current day. It completely ignores the open positions and the current equity. Thus, a limit breach is determined solely by the cumulative result of closed trades. -
CLOSED_POSITION_AND_EQUITY:
This is the most comprehensive and accurate method because it combines both approaches mentioned above. The function simultaneously assesses the profit or loss on positions closed during the day and the current real-time equity. This means the overall daily result is analyzed, which provides a more accurate and rigorous assessment of whether established limits have been exceeded.
By using this enum in the risk management system, we provide users with flexibility to choose the limit verification method, allowing easy adaptation to various strategies and accuracy levels required in risk management.
Defines
Continuing with new variables, it is important to define some constants using the #define directive.
First, we will set the prefix define to easily identify operations and messages generated by our Expert Advisor. Such a prefix can include the Expert Advisor name, making it easily distinguishable in logs or comments generated by the system. For example, in this case we will use:
#define EA_NAME "CRiksManagement | " //Prefix
In addition, we need to define several constants (flags) to be used further for precise control of both open positions and pending orders. These flags will allow quick identification of trade type (buy, sell, limit orders, stop orders, etc.), which will simplify effective risk management, position closing, and execution of specific requests regarding current market state and our positions.
Below are the respective define constants:
//--- positions #define FLAG_POSITION_BUY 2 #define FLAG_POSITION_SELL 4 //--- orders #define FLAG_ORDER_TYPE_BUY 1 #define FLAG_ORDER_TYPE_SELL 2 #define FLAG_ORDER_TYPE_BUY_LIMIT 4 #define FLAG_ORDER_TYPE_SELL_LIMIT 8 #define FLAG_ORDER_TYPE_BUY_STOP 16 #define FLAG_ORDER_TYPE_SELL_STOP 32 #define FLAG_ORDER_TYPE_BUY_STOP_LIMIT 64 #define FLAG_ORDER_TYPE_SELL_STOP_LIMIT 128 #define FLAG_ORDER_TYPE_CLOSE_BY 256
These constants will provide clearer and more efficient implementation in future functions, significantly simplifying the readability, maintenance, and scalability of the Expert Advisor code.
Constructor and main function improvements
We will start optimizing risk management with the CRiskManagement class constructor. Now that dynamic per-trade risk has been added, several important improvements have been made to the code:
First, we explicitly defined the lot type used (type_get_lot) and the parameter related to the initial balance (account_propfirm_balance), which is useful when using a PropFirm account. In addition, note that the EA_NAME definition will be constantly displayed in comments generated by the main class functions. This will help quickly identify them in terminal logs.
The improved constructor implementation looks as follows:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CRiskManagemet::CRiskManagemet(bool mdp_strict_, ENUM_GET_LOT type_get_lot_, ulong magic_number_ = NOT_MAGIC_NUMBER, ENUM_MODE_RISK_MANAGEMENT mode_risk_management_ = personal_account, double account_propfirm_balance = 0) { if(magic_number_ == NOT_MAGIC_NUMBER) { Print(EA_NAME, " (Warning) No magic number has been chosen, taking into account all the magic numbers and the user's trades"); } //--- this.mdp_is_strict = mdp_strict_; this.type_get_lot = type_get_lot_; //--- this.account_balance_propfirm = account_propfirm_balance ; trade = new CTrade(); trade.SetExpertMagicNumber(this.magic_number); this.account_profit = GetNetProfitSince(true, this.magic_number, D'1972.01.01 00:00'); this.magic_number = magic_number_; this.mode_risk_managemet = mode_risk_management_; this.ActivateDynamicRiskPerOperation = false; //--- this.last_day_time = iTime(_Symbol, PERIOD_D1, 0); this.last_weekly_time = iTime(_Symbol, PERIOD_W1, 0); this.init_time = magic_number_ != NOT_MAGIC_NUMBER ? TimeCurrent() : D'1972.01.01 00:00'; //--- this.positions_open = false; this.curr_profit = 0; UpdateProfit(); //--- for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong position_ticket = PositionGetTicket(i); if(!PositionSelectByTicket(position_ticket)) continue; ulong position_magic = PositionGetInteger(POSITION_MAGIC); ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); if(position_magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER) { this.positions_open = true; Positions new_pos; new_pos.type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); new_pos.ticket = position_ticket; ExtraFunctions::AddArrayNoVerification(open_positions, new_pos); } } }
New important variables have been added:
- curr_profit: Stores current profit, thus enabling constant results monitoring.
- ActivateDynamicRiskPerOperation: Boolean variable that determines whether dynamic risk will be used during Expert Advisor operation.
- mdp_is_strict: Variable that determines whether risk management will strictly monitor mdp.
- type_get_lot: Variable that stores the lot type.
Destructor improvements
Several significant destructor improvements have been made, particularly regarding proper dynamic memory management. Now the CTrade class pointer is validated using the CheckPointer() function, ensuring deletion only if it is dynamic, thereby preventing potential memory deallocation errors.
The optimized destructor implementation looks as follows:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CRiskManagemet::~CRiskManagemet() { if(CheckPointer(trade) == POINTER_DYNAMIC) delete trade; ArrayFree(this.open_positions); ArrayFree(this.dynamic_gmlpos.balance_to_activate_the_risk); ArrayFree(this.dynamic_gmlpos.risk_to_be_adjusted); }
Proper use of CheckPointer() ensures that the trade pointer is released correctly and only when required. In addition, by using the ArrayFree() function, we effectively free memory used by class arrays, ensuring proper memory management and making the Expert Advisor implementing this risk management more stable and effective.
General improvements
I made several important changes aimed at overall strengthening the risk management system and preventing potential errors in Expert Advisor operation. These are described in more detail below.
1. Checks when retrieving lot volume and stop loss (SL)
Key checks have been added to ensure that the per-trade risk value (gmlpo.assigned_percentage) is not invalid or zero. These checks allow timely identification of critical errors and provide messages to users, enabling quick adjustment of incorrect settings.
The main criterion is direct validation of gmlpo.assigned_percentage. If this value is less than or equal to zero, a critical message is output to the console and the function terminates, returning safe values to prevent Expert Advisor malfunction.
if(gmlpo.assigned_percentage <= 0) { PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value); return 0; }
Example in the GetSL() function:
//+----------------------------------------------------------------------------------+ //| Get the ideal stop loss based on a specified lot and the maximum loss per trade | //+----------------------------------------------------------------------------------+ long CRiskManagemet::GetSL(const ENUM_ORDER_TYPE type, double DEVIATION = 100, double STOP_LIMIT = 50) { if(gmlpo.assigned_percentage <= 0) { PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value); return 0; } double lot; return CalculateSL(type, this.gmlpo.value, lot, DEVIATION, STOP_LIMIT); }
Example in the GetLote() function:
//+-----------------------------------------------------------------------------------------------+ //| Function to obtain the ideal lot based on the maximum loss per operation and the stop loss | //+-----------------------------------------------------------------------------------------------+ double CRiskManagemet::GetLote(const ENUM_ORDER_TYPE order_type) { if(gmlpo.assigned_percentage <= 0) { PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value); this.lote = 0.00; return this.lote; } //--- if(this.type_get_lot == GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION) { double MaxLote = GetMaxLote(order_type); SetNMPLO(this.lote, MaxLote); PrintFormat("%s Maximum loss in case the next operation fails %.2f ", EA_NAME, this.nmlpo); } else { this.lote = GetLotByRiskPerOperation(this.gmlpo.value, order_type); } //--- return this.lote; }
2. Parameter assignment function (SetEnums()) improvements
This function is critically important and must be executed in the OnInit() event. Now it includes an additional check ensuring correct assignment of monetary or percentage values based on user choice. This validation prevents assignment of incorrect or negative values, especially when using a fixed monetary amount (money) as criterion.
Improved SetEnums() implementation:
//+----------------------------------------------------------------------------------------+ //| Function to set how losses or gains are calculated, | //| by percentage applied to (balance, equity, free margin or net profit) or simply money. | //+----------------------------------------------------------------------------------------+ //Note: This method is mandatory, it must be executed in the OnInit event. void CRiskManagemet::SetEnums(ENUM_RISK_CALCULATION_MODE mode_mdl_, ENUM_RISK_CALCULATION_MODE mode_mwl_, ENUM_RISK_CALCULATION_MODE mode_gmlpo_, ENUM_RISK_CALCULATION_MODE mode_ml_, ENUM_RISK_CALCULATION_MODE mode_mdp_) { this.gmlpo.mode_calculation_risk = mode_gmlpo_; this.mdl.mode_calculation_risk = mode_mdl_; this.mdp.mode_calculation_risk = mode_mdp_; this.ml.mode_calculation_risk = mode_ml_; this.mwl.mode_calculation_risk = mode_mwl_; //-- En caso se haya escojido el modo dinero, asignamos la variable que guarda el dinero o porcentage alas varialbes correspondientes if(this.gmlpo.mode_calculation_risk == money) { this.gmlpo.value = this.gmlpo.percentage_applied_to; this.ActivateDynamicRiskPerOperation = false; } else this.gmlpo.value = 0; this.mdp.value = this.mdp.mode_calculation_risk == money ? (this.mdp.percentage_applied_to > 0 ? this.mdp.percentage_applied_to : 0) : 0; this.mdl.value = this.mdl.mode_calculation_risk == money ? (this.mdl.percentage_applied_to > 0 ? this.mdl.percentage_applied_to : 0) : 0; this.ml.value = this.ml.mode_calculation_risk == money ? (this.ml.percentage_applied_to > 0 ? this.ml.percentage_applied_to : 0) : 0; this.mwl.value = this.mwl.mode_calculation_risk == money ? (this.mwl.percentage_applied_to > 0 ? this.mwl.percentage_applied_to : 0) : 0; }
New class variables
Continuing with improvements, we will add variables allowing more precise and effective control of open trades.
First, let’s define a special array to store information about all open positions in the account, both opened manually by the user and automatically by the Expert Advisor:
//--- Positions open_positions[];
In addition, we will add two key variables:
- Boolean variable positions_open, which will indicate whether there are open positions in the market. This variable plays an important role in optimizing performance and helps avoid redundant checks when there are no active positions.
//--- Boolean variable to check if there are any open operations by the EA or user bool positions_open;
-
Double variable called curr_profit, which will store the current accumulated profit or loss in real time. This variable enables quick calculations of the current trade state:
//--- Variable to store the current profit of the EA or user double curr_profit;
Dynamic per-trade risk
To effectively manage dynamic per-trade risk (dynamic GMLPO), we must add and clearly define several specific variables in our class. These variables will provide effective control, allowing automatic risk adjustment according to user-defined parameters. Below is a detailed explanation of each:
1. Structure for storing balances and dynamic risks
We will use a custom structure called Dynamic_gmlpo, which will contain two dynamic arrays:
Dynamic_gmlpo dynamic_gmlpos;
This structure allows storage of multiple specific balance levels along with respective risk percentages, thereby facilitating dynamic risk management.
2. Variable for determining dynamic risk check type
We will define an enum variable named revision_type, which will allow users to choose how dynamic risk checks will be performed (on each tick or when closing positions):
ENUM_REVISION_TYPE revision_type;
3. Boolean variable for activating or deactivating dynamic risk
This Boolean variable will store the decision on whether to use dynamic per-trade risk:
bool ActivateDynamicRiskPerOperation; 4. Variable for storing current dynamic risk index
To maintain precise control over dynamic risk level changes, an index variable is used to indicate which dynamic array element we are currently at:
int index_gmlpo; 5. Variable for storing the base (initial) balance
This variable stores the initial or reference balance selected by the user, to which specified percentages will later be applied to activate dynamic risk.
double chosen_balance; 6. Variable indicating next target balance for changing per-trade risk (in positive direction)
This variable stores the next balance level that must be exceeded to dynamically adjust the risk percentage:
double NewBalanceToOvercome; 7. Boolean variable to prevent errors when index exceeds allowable range
This Boolean variable indicates whether the minimum possible balance set by the user (maximum allowable negative percentage) has been reached. If the variable’s value becomes true, index increment will be paused to prevent exceeding the maximum allowable range.
bool TheMinimumValueIsExceeded; 8. Variable for storing the initial risk percentage per trade
This variable stores the initial percentage set for per-trade risk. It is mainly used to restore the original value when the balance recovers after reaching lower levels.
double gmlpo_percentage; These variables allow effective implementation of a reliable, secure, and easy-to-manage mechanism that ensures clarity and precise control of dynamic per-trade risk in various operational scenarios.
Variable for controlling maximum daily profit
For stricter control over daily profits, we will introduce a Boolean variable mdp_is_strict. This variable will help the risk management system determine whether the calculation of maximum profit should be adjusted to account for daily losses.
- If mdp_is_strict is set to true, the maximum daily profit will be considered exceeded only if all previous losses are recovered and then the original target level is reached. For example, if maximum daily profit is $50 and $20 was lost during the day, then a total of $70 must be earned (to recover the $20 loss and achieve a net profit of $50) to consider the target level reached.
- If mdp_is_strict is false, daily losses do not affect the maximum profit calculation. In this case, if target profit is $50 and $40 has been lost, it will be enough to earn an additional $10 (to compensate for the $40 loss plus $10 net profit) to reach maximum daily profit.
bool mdp_is_strict; Lot type variable
To simplify the lot assignment functionality, we will modify the GetBatch() function so it no longer requires manual lot type specification. Instead, the lot type will be initialized in the constructor or can be changed through additional functions we will develop. This approach will provide more direct configuration and reduce the risk of errors associated with manual parameter input.
ENUM_GET_LOT type_get_lot;
Thanks to these improvements, we expect to enhance efficiency and accuracy in both risk management and lot allocation on our trading platform.
Managing open positions
In this section, we will focus on properly managing all positions opened by magic number or by the user. To do this, we will create several useful and clear functions that allow keeping track of open trades at any time.
1. Function to check for ticket presence in internal array (open_positions)
The TheTicketExists() function is designed for a quick check to see if a specific ticket is contained in our internal open_positions array. This operation is especially important when confirming whether a specific position is already under management or if additional actions must be taken.
The logic is simple: we iterate through the array and compare each element with the provided ticket. If a match is found, return true; otherwise, false.
Declaration:
bool TheTicketExists(const ulong ticket); //Check if a ticket is in the operations array
Implementation:
//+------------------------------------------------------------------+ //| Function to check if the ticket is in the array | //+------------------------------------------------------------------+ bool CRiskManagemet::TheTicketExists(const ulong ticket) { for(int i = 0; i < this.GetPositionsTotal() ; i++) if(this.open_positions[i].ticket == ticket) return true; return false; }
2. Function to retrieve total number of open positions
The GetPositionsTotal() function simply returns the number of elements present in the internal open_positions array. It allows easy and quick determination of how many positions are managed in real time.
Declaration and implementation:
inline int GetPositionsTotal() const { return (int)this.open_positions.Size(); } //Get the total number of open positions
3. Function to retrieve total number of positions with flags
The GetPositions() function uses a flag system to provide greater flexibility in counting open positions based on specific criteria, such as trade type (buy or sell). To do this, we convert the position type (ENUM_POSITION_TYPE) into values compatible with binary flags (usually powers of 2, e.g., 2, 4, 8, etc.).
The logic involves iterating through all open positions and checking each against the specified flags, then incrementing the counter for each match found.
Implementation:
//+------------------------------------------------------------------+ //| Function to obtain the number of open positions | //+------------------------------------------------------------------+ int CRiskManagemet::GetPositions(int flags) const { int count = 0; for(int i = 0; i < ArraySize(this.open_positions) ; i++) { if(this.open_positions[i].type == POSITION_TYPE_BUY && (flags & FLAG_POSITION_BUY) != 0 ) { count++; } else if(this.open_positions[i].type == POSITION_TYPE_SELL && (flags & FLAG_POSITION_SELL) != 0 ) { count++; } } return count; }
4. Function to check for open positions at the moment
Finally, the ThereAreOpenOperations() function provides a quick and efficient way to find out if our Expert Advisor is currently processing open positions. It simply returns the value of the internal Boolean variable (positions_open).
Declaration and implementation:
inline bool ThereAreOpenOperations() const { return this.positions_open; } //Check if there are any open operations
5. Additional helper functions
In addition to the functions that are an integral part of the CRiskManagement class, external helper functions will be created to facilitate execution of specific tasks, such as closing orders based on flags.
5.1 Function to close pending orders by flags
Before closing orders based on specific criteria, we must convert order types (ENUM_ORDER_TYPE) into compatible binary flags. This step is crucial for preventing errors when performing bitwise operations (&).
Below is a simple function that converts a specific order type into the respective flag.
If an invalid or generalized value (WRONG_VALUE) is passed, the function returns a combination of all flags:
// Converts an order type to its corresponding flag int OrderTypeToFlag(ENUM_ORDER_TYPE type) { if(type == ORDER_TYPE_BUY) return FLAG_ORDER_TYPE_BUY; else if(type == ORDER_TYPE_SELL) return FLAG_ORDER_TYPE_SELL; else if(type == ORDER_TYPE_BUY_LIMIT) return FLAG_ORDER_TYPE_BUY_LIMIT; else if(type == ORDER_TYPE_SELL_LIMIT) return FLAG_ORDER_TYPE_SELL_LIMIT; else if(type == ORDER_TYPE_BUY_STOP) return FLAG_ORDER_TYPE_BUY_STOP; else if(type == ORDER_TYPE_SELL_STOP) return FLAG_ORDER_TYPE_SELL_STOP; else if(type == ORDER_TYPE_BUY_STOP_LIMIT) return FLAG_ORDER_TYPE_BUY_STOP_LIMIT; else if(type == ORDER_TYPE_SELL_STOP_LIMIT) return FLAG_ORDER_TYPE_SELL_STOP_LIMIT; else if(type == ORDER_TYPE_CLOSE_BY) return FLAG_ORDER_TYPE_CLOSE_BY; return (FLAG_ORDER_TYPE_BUY | FLAG_ORDER_TYPE_SELL | FLAG_ORDER_TYPE_BUY_LIMIT | FLAG_ORDER_TYPE_SELL_LIMIT | FLAG_ORDER_TYPE_BUY_STOP | FLAG_ORDER_TYPE_SELL_STOP | FLAG_ORDER_TYPE_BUY_STOP_LIMIT | FLAG_ORDER_TYPE_SELL_STOP_LIMIT | FLAG_ORDER_TYPE_CLOSE_BY); }
The main function iterates through all existing orders, closing those that match the specified flags:
// Close all orders that match the flags in `flags` void CloseAllOrders(int flags, CTrade &obj_trade, ulong magic_number_ = NOT_MAGIC_NUMBER) { ResetLastError(); for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(OrderSelect(ticket)) { ENUM_ORDER_TYPE type_order = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); ulong magic = OrderGetInteger(ORDER_MAGIC); int bandera = OrderTypeToFlag(type_order); if((bandera & flags) != 0 && (magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER)) { if(type_order == ORDER_TYPE_BUY || type_order == ORDER_TYPE_SELL) obj_trade.PositionClose(ticket); else obj_trade.OrderDelete(ticket); } } else { PrintFormat("Error selecting order %d, last error %d", ticket, GetLastError()); } } }
5.2 Functions to retrieve total number of open positions
A simple external function is also added to count the total number of open positions without having to rely solely on the CRiskManagement class. First, we must convert position types (ENUM_POSITION_TYPE) into compatible flags.
5.2.1 Function to convert ENUM_POSITION_TYPE to valid flags
int PositionTypeToFlag(ENUM_POSITION_TYPE type) { if(type == POSITION_TYPE_BUY) return FLAG_POSITION_BUY; else if(type == POSITION_TYPE_SELL) return FLAG_POSITION_SELL; return FLAG_POSITION_BUY | FLAG_POSITION_SELL; }
5.2.2 Function to retrieve the total number of open positions by flags
This function iterates through all existing positions and counts only those matching the specified flags:
//--- int GetPositions(int flags = FLAG_POSITION_BUY | FLAG_POSITION_SELL, ulong magic_number_ = NOT_MAGIC_NUMBER) { int counter = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong position_ticket = PositionGetTicket(i); if(!PositionSelectByTicket(position_ticket)) continue; // Si la selección falla, pasa a la siguiente posición ulong position_magic = PositionGetInteger(POSITION_MAGIC); ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); // Check if the position type matches the flags if((flags & PositionTypeToFlag(type)) != 0 && (position_magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER)) { counter++; } } return counter; }
Handling limit exceedances
Now we will define several functions that allow us to determine whether we have exceeded the maximum loss or maximum daily profit.
Let’s start by developing two main functions: one for checking loss exceedance and the other for confirming achievement of maximum expected profit.
Each of these modes is encapsulated in a function that, depending on the option we choose, will inform us whether we have achieved the set goal (returning true) or if we still have a long way to go (returning false). For example, in CLOSED_POSITIONS mode, it is extremely important to know what profit has already been obtained because we must compare it with established maximum loss or profit limits (daily or weekly).
Let’s see how this can be implemented in code:
//+------------------------------------------------------------------+ //| Boolean function to check if a loss was overcome | //+------------------------------------------------------------------+ bool CRiskManagemet::IsSuperated(double profit_, double loss_, const MODE_SUPERATE mode) const { if(loss_ <= 0 || !this.positions_open) return false; //if loss is zero return false (the loss is not being used) //--- if(mode == EQUITY) //--- { if(this.curr_profit * -1 > loss_) return true; } else if(mode == CLOSE_POSITION) { if(profit_ * -1 > loss_) return true; } else if(mode == CLOSE_POSITION_AND_EQUITY) { double new_loss = profit_ < 0 ? loss_ - MathAbs(profit_) : loss_; if(this.curr_profit * -1 > new_loss) return true; } return false; }
Mode breakdown:
-
EQUITY: In this mode, we directly compare the current profit (equity minus account balance). If this negative value exceeds the specified loss level, we have exceeded the set limit.
-
CLOSE_POSITION: In this mode, we analyze the profit from positions already closed and, for a proper comparison, multiply it by -1.
-
CLOSE_POSITION_AND_EQUITY: This mode is more complex. Here we adjust the maximum loss to account for the current profit. If the day is unprofitable and the profit is negative, we subtract that value from the allowable loss limit. If the negative current profit exceeds the adjusted value, the threshold has been exceeded as well.
Specialized functions for maximizing profit
As with losses, we also need a way to check whether we have exceeded the maximum expected profit. To do this, we will create a dedicated function to ensure that our trades do not unintentionally change the value of the maximum daily profit (mdp).
Below is the code for this function:
//+------------------------------------------------------------------+ //| Function to check if the maximum profit per day was exceeded | //+------------------------------------------------------------------+ bool CRiskManagemet::MDP_IsSuperated(const MODE_SUPERATE mode) const { if(this.mdp.value <= 0 || !this.positions_open) return false; //if loss is zero return false (the loss is not being used) //--- if(mode == EQUITY) //--- { if(this.curr_profit > this.mdp.value) return true; } else if(mode == CLOSE_POSITION) { if(this.daily_profit > this.mdp.value) return true; } else if(mode == CLOSE_POSITION_AND_EQUITY) { double new_mdp = this.daily_profit > 0 ? this.mdp.value - this.daily_profit : (this.mdp_is_strict == false ? this.mdp.value : this.mdp.value + (this.daily_profit * -1)); if(this.curr_profit > new_mdp) return true; } //--- return false; }
How the function works:
-
EQUITY: Here, the current profit (curr_profit) is directly compared with the maximum daily profit (mdp). If the current profit is higher, the target is considered exceeded.
-
CLOSE_POSITION: This checks whether the profit from positions closed during the day only (daily_profit) exceeds the value of mdp.
-
CLOSE_POSITION_AND_EQUITY: This case is the most complex and covers two situations:
- If the daily profit is positive, we subtract that value from mdp to set a new adjusted target.
- If the daily profit is negative and the profit management policy is strict (mdpisstrict), we add the absolute value of that loss to mdp to offset it before the target metric can be considered exceeded. If the policy is not strict, we use the initial value of mdp and ignore daily losses.
This feature provides precise control over profit targets, ensuring that even on volatile trading days we can accurately assess whether we have met or exceeded our profit target.
Maximum loss in PropFirm
Continuing the topic of loss management, it is important to note a key feature of PropFirm accounts, especially FTMO-style accounts. In these accounts, the maximum loss limit is fixed. It does not change and remains constant from the start of the test. For example, if we start with an account balance of 10,000 USD, the maximum loss limit is 9,000 USD. This means that if the account equity ever falls below this threshold, eligibility for funding is automatically lost.
To simplify monitoring of this threshold and avoid additional complications, we will implement a special method that will check whether the maximum loss limit has been reached or exceeded:
//--- Function to check if the maximum loss has been exceeded in a PropFirm account of the FTMO type inline bool IsSuperatedMLPropFirm() const { return (this.ml.value == 0 || !this.positions_open) ? false : AccountInfoDouble(ACCOUNT_EQUITY) < (account_balance_propfirm - (this.ml.value)); }
Logic is simple: if there are no open positions, or the variable that controls maximum loss (ml) equals zero, the check is skipped and false is returned. If there are open positions and ml has a value, the function compares the current equity with the initial test balance minus the maximum allowed loss.
Verification functions
In addition to the method above, we will create practical functions to quickly determine whether preset profit or loss levels have been reached or exceeded. These functions are simplified wrapper calls for the general IsSuperated method:
//--- functions to verify if the established losses were exceeded inline bool ML_IsSuperated(const MODE_SUPERATE mode) const {return this.mode_risk_managemet == personal_account ? IsSuperated(this.gross_profit, this.ml.value, mode) : IsSuperatedMLPropFirm(); } inline bool MWL_IsSuperated(const MODE_SUPERATE mode) const {return IsSuperated(this.weekly_profit, this.mwl.value, mode); } inline bool MDL_IsSuperated(const MODE_SUPERATE mode) const {return IsSuperated(this.daily_profit, this.mdl.value, mode); } inline bool GMLPO_IsSuperated() const {return IsSuperated(0, this.gmlpo.value, EQUITY); } inline bool NMLPO_IsSuperated() const {return IsSuperated(0, this.nmlpo, EQUITY); } bool MDP_IsSuperated(const MODE_SUPERATE mode) const;
These functions provide an efficient, straightforward way to evaluate conditions for daily, weekly, and other loss limits.
FTMO maximum dynamic daily loss
A key aspect of risk management at FTMO is understanding that maximum daily loss is not fixed; it is dynamic. This value changes based on profit accumulated during the day. The more profit earned during the day, the larger the allowed room for potential losses; therefore, this limit fluctuates rather than staying constant.
Below is a function that updates maximum daily loss based on realized profit:
//--- Update Loss (only if ftmo propfirm FTMO is selected) inline void UpdateDailyLossFTMO() { this.mdl.value += this.daily_profit > 0 ? this.daily_profit : 0; PrintFormat("%s The maximum loss per operation has been modified, its new value: %.2f",EA_NAME,this.mdl.value); }
This function should be called from the method that processes Expert Advisor transactions, i.e., OnTradeTransaction.
//+------------------------------------------------------------------+ //| TradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result)
Event handling
Now let’s move on to the new events that will run within the Expert Advisor event management methods.
OnTradeTransaction
The OnTradeTransaction method is central to dynamic risk management because it triggers on any event related to account trading operations, such as opening or closing positions, modifying existing orders, or funding the account.
The function has three key parameters:
- trans: Now let’s move on to the new events that will run within the Expert Advisor event management methods.
- request: Information about trade requests that are pending or were recently completed.
- result: Results returned after processing these requests.
For risk management, we are primarily interested in the information in trans, because it provides accurate details about the transaction type and the associated operation.
Several trade transaction types are defined in the enumeration:
ENUM_TRADE_TRANSACTION_TYPE Trade transaction types:
| ID | Description |
|---|---|
| TRADE_TRANSACTION_ORDER_ADD | Adding a new order. |
| TRADE_TRANSACTION_ORDER_UPDATE | Updating an open order. Such changes include explicit edits made in the client terminal or on the trade server, as well as changes to the order state during placement (e.g., transition from ORDER_STATE_STARTED to ORDER_STATE_PLACED, or from ORDER_STATE_PLACED to ORDER_STATE_PARTIAL, etc.). |
| TRADE_TRANSACTION_ORDER_DELETE | Removing an order from the list of open orders. An order can be removed from the list of open orders either after the relevant request is placed, or after it is executed (filled) and added to the history. |
| TRADE_TRANSACTION_DEAL_ADD | Adding a deal to the history. Occurs after an order is executed or an account-balance operation is performed. |
| TRADE_TRANSACTION_DEAL_UPDATE | Updating a deal in the history. A previously completed deal may be modified on the server. For example, if it was adjusted in an external trading system (exchange) to which the broker sent it. |
| TRADE_TRANSACTION_DEAL_DELETE | Removing a deal from the history. A previously completed deal may be deleted on the server. For example, if it was removed in an external trading system (exchange) to which the broker sent it. |
| TRADE_TRANSACTION_HISTORY_ADD | Adding an order to the history after it is executed or canceled. |
| TRADE_TRANSACTION_HISTORY_UPDATE | Updating an order in the order history. This type is also intended to extend server functionality. |
| TRADE_TRANSACTION_HISTORY_DELETE | Removing an order from the order history. This type is also intended to extend server functionality. |
| TRADE_TRANSACTION_POSITION | Position change not related to deal execution. This transaction type means that the position was changed on the trade server. A position can change in volume, opening price, and Stop Loss and Take Profit levels. Information about these changes is passed in the MqlTradeTransaction structure via the OnTradeTransaction handler. A position change (addition, update, or deletion) caused by a deal does not later trigger a TRADE_TRANSACTION_POSITION transaction. |
| TRADE_TRANSACTION_REQUEST | Notification that the server has processed the trade request and returned the result. For this transaction type, only one field in MqlTradeTransaction needs to be analyzed: type (transaction type). For more information, analyze the second and third parameters of OnTradeTransaction (request and result). |
However, focus is on the following transaction type:
- TRADE_TRANSACTION_DEAL_ADD indicates that a new transaction was added to the history (a closed deal or a confirmed position opening).
Creating the OnTradeTransactionEvent function
Next, we will define a function for clear, efficient handling of this event:
void OnTradeTransactionEvent(const MqlTradeTransaction& trans);
The function requires only the trans parameter, which contains all the information about the current transaction.
The basic structure of our function starts by checking that the transaction type is TRADE_TRANSACTION_DEAL_ADD, then preselecting the deal from the history:
//+------------------------------------------------------------------+ //| OnTradeTransaction Event | //+------------------------------------------------------------------+ void CRiskManagemet::OnTradeTransactionEvent(const MqlTradeTransaction &trans) { HistoryDealSelect(trans.deal); if(trans.type == TRADE_TRANSACTION_DEAL_ADD) { ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal, DEAL_ENTRY); ulong position_magic = (ulong)HistoryDealGetInteger(trans.deal, DEAL_MAGIC); bool is_select = PositionSelectByTicket(trans.position); if(entry == DEAL_ENTRY_IN && is_select && (this.magic_number == position_magic || this.magic_number == NOT_MAGIC_NUMBER)) { Print(EA_NAME, " New position opened with ticket: ", trans.position); this.positions_open = true; Positions new_pos; new_pos.type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); new_pos.ticket = trans.position; AddArrayNoVerification(open_positions, new_pos); return; } if(entry == DEAL_ENTRY_OUT && TheTicketExists(trans.position) == true && !is_select) { Print(EA_NAME, " Position with ticket ", trans.position, " has been closed"); DeleteTicket(trans.position); //--- if(this.revision_type == REVISION_ON_CLOSE_POSITION) CheckAndModifyThePercentageOfGmlpo(); //--- if(GetPositionsTotal() == 0) this.positions_open = false; //--- UpdateProfit(); //--- if(this.mode_risk_managemet == propfirm_ftmo) UpdateDailyLossFTMO(); SetGMLPO(); Print(StringFormat("%-6s| %.2f", "GMLPO", this.gmlpo.value)); } } }
Inside this function, we handle two key scenarios:
-
Opening positions: We confirm that the position is open (is_select) and that its magic number matches the specified value before adding it to our internal list.
-
Closing positions: We check that the position has been closed (cannot be selected) and that it is present in our list of managed positions. In this case, we remove the ticket from the register, update key variables such as accumulated profit and the overall state of open positions, and dynamically adjust the maximum daily loss if FTMO mode is enabled.
This way, the framework can proactively respond to each registered transaction and provide clear, efficient, and dynamic risk management.
OnTickEvent
The OnTick event is one of the key events in Expert Advisor (EA) operation, as it triggers on each new market tick.
In our case, it is used to update the total accumulated profit for all open positions, filtered by the magic number if it is specified. If the magic number is not set, the update is performed for all open positions, regardless of the magic value.
The basic method definition is as follows:
void OnTickEvent(); This function is executed only when there are open positions to avoid unneeded computation.
The function is structured as follows:
//+------------------------------------------------------------------+ //| Function to execute in OnTick | //+------------------------------------------------------------------+ void CRiskManagemet::OnTickEvent(void) { if(!positions_open) return; //--- GetPositionsProfit(); //--- if(this.revision_type == REVISION_ON_TICK) CheckAndModifyThePercentageOfGmlpo(); }Detailed explanation of the process:
-
Updating the total profit:
-
The GetPositionsProfit() method retrieves and updates information about the current profit or loss on managed positions. This ensures that we always have up-to-date, accurate data on the overall performance of open trades.
-
-
Validating and dynamically updating GMLPO:
-
If we selected the REVIEW_ON_TICK option, the Expert Advisor continuously checks, on each new tick, whether the previously defined profit or loss threshold has been exceeded, thereby dynamically adjusting the allowable risk per trade. This allows us to regulate the volume of open positions (market exposure level) in real time and improves risk management performance.
-
Functions for dynamic risk and working with position arrays
In this section, we will implement key functions to manage dynamic risk effectively and handle practical tasks related to position arrays. These tools will be useful at various stages of our risk management strategy.
Function for converting a string to a data type
This function uses templates to adapt easily to various simple data types (excluding complex classes or structures). Its main goal is to convert a string into the required data type and simplify dynamic information processing in real time.
Implementation:
template <typename S> void StringToType(string token, S &value, ENUM_DATATYPE type) { if(StringLen(token) == 0) { Print("Error: String is empty."); return; } switch(type) { case TYPE_BOOL: value = (S)(StringToInteger(token) != 0); // Convertir a bool break; case TYPE_CHAR: value = (S)((char)StringToInteger(token)); // Convertir a char break; case TYPE_UCHAR: value = (S)((uchar)StringToInteger(token)); // Convertir a uchar break; case TYPE_SHORT: value = (S)((short)StringToInteger(token)); // Convertir a short break; case TYPE_USHORT: value = (S)((ushort)StringToInteger(token)); // Convertir a ushort break; case TYPE_COLOR: value = (S)((color)StringToInteger(token)); // Convertir a color break; case TYPE_INT: value = (S)(StringToColor(token)); // Convertir a int break; case TYPE_UINT: value = (S)((uint)StringToInteger(token)); // Convertir a uint break; case TYPE_DATETIME: value = (S)(StringToTime(token)); // Convertir a datetime break; case TYPE_LONG: value = (S)((long)StringToInteger(token)); // Convertir a long break; case TYPE_ULONG: value = (S)((ulong)StringToInteger(token)); // Convertir a ulong break; case TYPE_FLOAT: value = (S)((float)StringToDouble(token)); // Convertir a float break; case TYPE_DOUBLE: value = (S)(StringToDouble(token)); // Convertir a double break; case TYPE_STRING: value = (S)(token); // Mantener como string break; default: Print("Error: Unsupported data type in ConvertToType."); break; } }
This function is especially important when working with dynamic risk, because the parameters come as text strings and must be converted into numeric variables for further use.
Function for converting a text string into an array of primitive types
Similarly, this function uses templates to simplify converting a text string into an array of the required type. The function performs the following:
- Splits the input string using a specified separator (default is a comma ',').
- Stores each resulting element in a temporary array.
- Converts each element to the specified data type and assigns it to the target array.
//--- template <typename S> void StringToArray(S &array_receptor[], string cadena, ENUM_DATATYPE type_data, ushort separator = ',') { string result[]; int num = StringSplit(cadena, separator, result); ArrayResize(array_receptor, ArraySize(result)); for(int i = 0; i < ArraySize(array_receptor) ; i++) { S value; StringToType(result[i], value, type_data); array_receptor[i] = value; } }
For example, in practical dynamic-risk scenarios, we can receive data like “5.0,4.5,3.0”, which will be automatically converted into a double array. This significantly simplifies managing dynamic parameters within our system.
Advanced functions for working with arrays
Below are several useful functions that facilitate effective array management, especially when working with dynamic strategies and complex structures in risk management.
Function to remove multiple elements from an array by index
This function allows you to remove multiple elements from an array at once, improving performance and readability. The main logic is to sort the indices to remove, then copy only the elements to keep back into the original array.
Implementation:
//--- template <typename T> void RemoveMultipleIndexes(T &arr[], int &indexes_to_remove[]) { int oldSize = ArraySize(arr); int removeSize = ArraySize(indexes_to_remove); if(removeSize == 0 || oldSize == 0) return; // Ordenamos los índices para garantizar eficiencia al recorrerlos ArraySort(indexes_to_remove); int writeIndex = 0, readIndex = 0, removeIndex = 0; while(readIndex < oldSize) { if(removeIndex < removeSize && readIndex == indexes_to_remove[removeIndex]) { removeIndex++; } else { arr[writeIndex] = arr[readIndex]; writeIndex++; } readIndex++; } ArrayResize(arr, writeIndex); }
Function to add elements to an array
The following function makes it easy to add new elements to an array of any type and automatically adapts to the data type.
Implementation:
//--- template <typename X> void AddArrayNoVerification(X &array[], const X &value) { ArrayResize(array, array.Size() + 1); array[array.Size() - 1] = value; }
It is simple: we increase the array size by one and assign the new value to the last element.
Specialized function for removing a single item by ticket
This function is designed for arrays of structures that contain a ticket field. It removes a specific element based on that unique identifier.
Implementation:
//--- template<typename T> bool RemoveIndexFromAnArrayOfPositions(T &array[], const ulong ticket) { int size = ArraySize(array); int index = -1; // Search index and move elements in a single loop for(int i = 0; i < size; i++) { if(array[i].ticket == ticket) { index = i; } if(index != -1 && i < size - 1) { array[i] = array[i + 1]; // Move the elements } } if(index == -1) return false; // Reducir el tamaño del array if(size > 1) ArrayResize(array, size - 1); else if(size <= 1) ArrayFree(array); return true; }
It is important to note that this function requires the structure to include an element named ticket. If we try to use it with structures that do not have this element, an error occurs:
'ticket' - undeclared identifier
in template 'bool RemoveIndexFromAnArrayOfPositions(T&[],const ulong)' specified with [T=Message]
see template instantiation 'ExtraFunctions::RemoveIndexFromAnArrayOfPositions<Message>'
1 errors, 0 warnings
In this case, the "Message" structure does not contain the "ticket" element.
Additional function for repeating a text string
Finally, we have a useful function that generates repeating text as many times as we need. This is especially useful for printing tables or visually separating information.
Implementation:
string StringRepeat(string str, int count) { string result = ""; for(int i = 0; i < count; i++) result += str; return result; }
For example, when calling
StringRepeat("-", 10) мы получим "----------".
These advanced functions significantly simplify working with arrays and improve code readability, providing efficient and versatile tools for dynamic and precise risk management in our trading frameworks.
Building dynamic risk
Finally, we have reached the most important part: implementing dynamic risk. To do this, we will use two key functions that significantly simplify processing and improve adaptability.
Before we begin, you need to include the following library:
#include <Generic\HashMap.mqh> This library will help us organize and process data related to dynamic risk correctly.
Dynamic risk initialization function
Following the initial concept presented in previous articles, our dynamic risk system will be based on a structure that contains two double arrays: one to store the new risk values to apply, and the other to specify the balance levels or percentages that trigger these changes.
Due to the limitations of the MQL5 language, it is not possible to directly pass double arrays as parameters to the Expert Advisor. To work around this limitation, we will use comma-separated strings and then convert them into numeric arrays using functions implemented earlier.
The main dynamic-risk initialization function converts these strings into numeric arrays, checks for duplicates, and ensures the values are sorted correctly.
The function declaration is as follows:
void SetDynamicGMLPO(string percentages_to_activate, string risks_to_be_applied, ENUM_REVISION_TYPE revision_type_);
The internal procedure will be as follows:
1. Validating dynamic risk usage
Before performing any operation, we verify that the GMLPO percentage is defined correctly:
if(this.gmlpo.assigned_percentage == 0) return; if(this.gmlpo.mode_calculation_risk == money) { this.ActivateDynamicRiskPerOperation = false; Print(EA_NAME, __FUNCTION__, "::'Money' mode is not valid for dynamic risk, change it to 'Percentage %' or change the group mode to 'No dynamic risk for risk per operation' "); return; }
2. Purpose of the selected check type
this.revision_type = revision_type_;
3. Converting strings to numeric arrays
Using the functions created earlier, we will convert the strings into double arrays:
//--- ExtraFunctions::StringToArray(this.dynamic_gmlpos.balance_to_activate_the_risk, percentages_to_activate, TYPE_DOUBLE, ','); ExtraFunctions::StringToArray(this.dynamic_gmlpos.risk_to_be_adjusted, risks_to_be_applied, TYPE_DOUBLE, ',');
4. Validating the resulting arrays
//--- if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() < 1 && this.dynamic_gmlpos.balance_to_activate_the_risk.Size() < 1) { Print(EA_NAME, __FUNCTION__, "::Critical error: the size of the array is less than 1"); this.ActivateDynamicRiskPerOperation = false; return; } if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() != this.dynamic_gmlpos.balance_to_activate_the_risk.Size()) { Print(EA_NAME, __FUNCTION__, "::Critical error the double arrays for the risk due to dynamic operation are not equal"); this.ActivateDynamicRiskPerOperation = false; return; } Print(EA_NAME, " Arrays before revision"); PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance"); PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");
Next, we must ensure that both arrays have the same length and are not empty. Otherwise, we will deactivate dynamic risk to avoid errors.
5. Cleaning and preparing the final structure
Finally, we clear the HashMap, select the appropriate reference balance based on the account type (FTMO or personal), and prepare an auxiliary array for possible duplicate or invalid indices:
balanceRiskMap.Clear(); this.chosen_balance = this.mode_risk_managemet == propfirm_ftmo ? this.account_balance_propfirm : AccountInfoDouble(ACCOUNT_BALANCE); int indexes_to_remove[];
This function provides a reliable, robust dynamic-risk setup tailored to the needs of each account or trading strategy, ensuring effective and precise risk management.
6. Loop for adding valid elements to the HashMap
Next, we implement a key loop that adds only valid elements to the HashMap, ensuring organized and efficient dynamic-risk management. Elements are considered invalid under the following conditions:
- If the percentage that activates the risk is less than or equal to zero.
- if the new risk value is less than or equal to zero.
- If the element is already present in the HashMap (duplicate).
When we detect an invalid element, we temporarily add it to the indexes_to_remove array for later removal. When evaluating both arrays (balance_to_activate_the_risk and risk_to_be_adjusted), it is enough for either value in a pair to be invalid to exclude the entire pair and preserve data integrity and consistency.
Implementation of the loop:
//--- for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= 0) { Print(EA_NAME, " (Warning) The percentage value that will be exceeded to modify the risk is 0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); continue; } if(dynamic_gmlpos.risk_to_be_adjusted[i] <= 0) { Print(EA_NAME, " (Warning) The new percentage to which the field is modified is 0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); continue; } if(balanceRiskMap.ContainsKey(dynamic_gmlpos.balance_to_activate_the_risk[i]) == false) balanceRiskMap.Add(dynamic_gmlpos.balance_to_activate_the_risk[i], dynamic_gmlpos.risk_to_be_adjusted[i]); else ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); }
For example, let’s assume we have these arrays:
- [1, 3, 5]
- [0.1, 0.3, 0.0]
In this case, the last pair (5 - 0.0) is invalid because the adjusted risk value is zero, so 5 will be removed from the relevant array.
7. Removing duplicates and invalid elements
After identifying invalid or duplicate items, we move on to removing them. In addition, we reorganize the main balance_to_activate_the_risk array to ensure proper order and data consistency:
//--- ExtraFunctions::RemoveMultipleIndexes(dynamic_gmlpos.balance_to_activate_the_risk, indexes_to_remove); ArraySort(dynamic_gmlpos.balance_to_activate_the_risk); ArrayResize(dynamic_gmlpos.risk_to_be_adjusted, ArraySize(dynamic_gmlpos.balance_to_activate_the_risk));
Resizing the risk_to_be_adjusted array is required to keep both arrays consistent after deletion.
8. Adjusting and transforming risk values
Next, we adjust and convert the percentage values stored in balance_to_activate_the_risk into monetary amounts based on the selected account balance. The risk_to_be_adjusted array is updated with the relevant values:
//--- for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { double value; balanceRiskMap.TryGetValue(this.dynamic_gmlpos.balance_to_activate_the_risk[i], value); dynamic_gmlpos.risk_to_be_adjusted[i] = value; dynamic_gmlpos.balance_to_activate_the_risk[i] = this.chosen_balance - (this.chosen_balance * (dynamic_gmlpos.balance_to_activate_the_risk[i] / 100.0)); }
9. Final initialization of dynamic risk
Finally, we initialize the required variables to ensure that dynamic risk is ready to work correctly:
//--- this.index_gmlpo = 0; this.ActivateDynamicRiskPerOperation = true; this.TheMinimumValueIsExceeded = false; this.NewBalanceToOvercome = 0.00; Print(EA_NAME, " Arrays ready: "); PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance"); PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");
This way, dynamic risk adjustment is accurate, efficient, and continuously adapts to the current indicators of our trading strategy.
Full function:
//+------------------------------------------------------------------+ //| Function to set dynamic risks per operation | //+------------------------------------------------------------------+ void CRiskManagemet::SetDynamicGMLPO(string percentages_to_activate, string risks_to_be_applied, ENUM_REVISION_TYPE revision_type_) { if(this.gmlpo.assigned_percentage <= 0) return; if(this.gmlpo.mode_calculation_risk == money) { this.ActivateDynamicRiskPerOperation = false; Print(EA_NAME, __FUNCTION__, "::'Money' mode is not valid for dynamic risk, change it to 'Percentage %' or change the group mode to 'No dynamic risk for risk per operation' "); return; } //--- this.revision_type = revision_type_; //--- ExtraFunctions::StringToArray(this.dynamic_gmlpos.balance_to_activate_the_risk, percentages_to_activate, TYPE_DOUBLE, ','); ExtraFunctions::StringToArray(this.dynamic_gmlpos.risk_to_be_adjusted, risks_to_be_applied, TYPE_DOUBLE, ','); //--- if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() < 1 || this.dynamic_gmlpos.balance_to_activate_the_risk.Size() < 1) { Print(EA_NAME, __FUNCTION__, "::Critical error: the size of the array is less than 1"); this.ActivateDynamicRiskPerOperation = false; return; } if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() != this.dynamic_gmlpos.balance_to_activate_the_risk.Size()) { Print(EA_NAME, __FUNCTION__, "::Critical error the double arrays for the risk due to dynamic operation are not equal"); this.ActivateDynamicRiskPerOperation = false; return; } Print(EA_NAME, " Arrays before revision"); PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance"); PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk"); //--- balanceRiskMap.Clear(); this.chosen_balance = this.mode_risk_managemet == propfirm_ftmo ? this.account_balance_propfirm : AccountInfoDouble(ACCOUNT_BALANCE); int indexes_to_remove[]; //--- for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= 0) { Print(EA_NAME, " (Warning) The percentage value that will be exceeded to modify the risk is 0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); continue; } if(dynamic_gmlpos.risk_to_be_adjusted[i] <= 0) { Print(EA_NAME, " (Warning) The new percentage to which the field is modified is 0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); continue; } if(balanceRiskMap.ContainsKey(dynamic_gmlpos.balance_to_activate_the_risk[i]) == false) balanceRiskMap.Add(dynamic_gmlpos.balance_to_activate_the_risk[i], dynamic_gmlpos.risk_to_be_adjusted[i]); else ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); } //--- ExtraFunctions::RemoveMultipleIndexes(dynamic_gmlpos.balance_to_activate_the_risk, indexes_to_remove); ArraySort(dynamic_gmlpos.balance_to_activate_the_risk); ArrayResize(dynamic_gmlpos.risk_to_be_adjusted, ArraySize(dynamic_gmlpos.balance_to_activate_the_risk)); //--- for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { double value; balanceRiskMap.TryGetValue(this.dynamic_gmlpos.balance_to_activate_the_risk[i], value); dynamic_gmlpos.risk_to_be_adjusted[i] = value; dynamic_gmlpos.balance_to_activate_the_risk[i] = this.chosen_balance - (this.chosen_balance * (dynamic_gmlpos.balance_to_activate_the_risk[i] / 100.0)); } //--- this.index_gmlpo = 0; this.ActivateDynamicRiskPerOperation = true; this.TheMinimumValueIsExceeded = false; this.NewBalanceToOvercome = 0.00; Print(EA_NAME, " Arrays ready: "); PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance"); PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk"); }
Clear and detailed explanation of the function to change dynamic risk percentage (GMLPO)
Next, we will explain the CheckAndModifyThePercentageOfGmlpo function step by step. It enables dynamic risk management per trade and automatically adapts based on the equity levels reached in the trading account.
1. Initial validation
The function starts by checking whether dynamic risk management is enabled. If it is disabled, the function returns immediately.
if(!this.ActivateDynamicRiskPerOperation) return;
2. Validating the current balance
Next, the function retrieves the current account equity (the actual account value, including floating profits or losses). This value is compared with the previously selected balance (chosen_balance).
If the current equity exceeds the selected balance and the next target balance level has not been reached yet, the function makes no changes and returns.
double account_equity = AccountInfoDouble(ACCOUNT_EQUITY); if(account_equity > this.chosen_balance && this.NewBalanceToOvercome != this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]) return;
3. Changing risk as equity decreases
If the current equity falls below the specified levels, the framework determines the next lower balance level.
-
If the balance has fallen below a certain level:
- New risk percentage for this level is determined.
- Internal variables are updated to reflect the new risk level.
if(this.TheMinimumValueIsExceeded == false) { if(account_equity < this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]) { PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]); while(IsStopped() == false && index_gmlpo < (int)this.dynamic_gmlpos.balance_to_activate_the_risk.Size()) { if(index_gmlpo < (int)this.dynamic_gmlpos.balance_to_activate_the_risk.Size() - 1) { if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo] > account_equity && account_equity > this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo + 1]) { this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]; this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo]; index_gmlpo ++; PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome); break; } } else if(index_gmlpo == this.dynamic_gmlpos.balance_to_activate_the_risk.Size() - 1) { if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo] > account_equity) { this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]; this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo]; this.TheMinimumValueIsExceeded = true; PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome); PrintFormat("%s The minimum value %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]); break; } } index_gmlpo++; } PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage); SetGMLPO(); } }
4. Restoring risk as equity increases again
If equity later rises again and exceeds the previously reached target level:
-
Our framework adjusts the per-trade risk again, restoring the previous levels based on the levels defined in the arrays.
if(this.NewBalanceToOvercome > 0.00) { if(account_equity > this.NewBalanceToOvercome) { PrintFormat("%s Equity %.2f exceeded balance to shift risk to %.2f", EA_NAME, account_equity, NewBalanceToOvercome); while(!IsStopped() && index_gmlpo > 0) { if(index_gmlpo > 0) { if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo] < account_equity && account_equity < this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo - 1]) { break; } this.index_gmlpo--; } } this.TheMinimumValueIsExceeded = false; if(this.index_gmlpo == 0) { Print(EA_NAME, " Excellent, the balance has been positively exceeded"); PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]); this.gmlpo.assigned_percentage = this.gmlpo_percentage; this.NewBalanceToOvercome = 0.00; PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage); SetGMLPO(); } else if(index_gmlpo > 0) { Print(EA_NAME, " Excellent, the balance has been positively exceeded"); PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]); this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo - 1]; this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo - 1]; PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage); PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome); SetGMLPO(); } } }
Conclusion
Throughout this series of articles, we examined step by step how to develop a comprehensive, reliable risk management system. In the concluding part, we worked through the details and introduced an advanced, highly useful concept of dynamic per-trade risk. This tool automatically adjusts the risk level based on actual account results, which is key to preserving capital and improving trading performance.
I hope this material is helpful, especially if you are just starting your journey in the world of MQL5 programming.
In the next risk-management section, we will extend this approach further and learn how to apply everything we have studied in practice by integrating this advanced management system into a trading robot.
To do this, we will use the Order Blocks indicator developed earlier:
In addition, we will clearly see the specific advantages of effective risk management compared to working without any control. Finally, we will configure specific settings that greatly simplify the day-to-day use of these advanced tools in any automated strategy.
Files used or updated in this article:
| File name | Type | Description |
|---|---|---|
| Risk_Management.mqh | .mqh (include file) | The main file contains common functions and the CRiskManagement class implementation, which is responsible for risk management in the system. This file defines and extends all functions related to profit and loss management. |
Translated from Spanish by MetaQuotes Ltd.
Original article: https://www.mql5.com/es/articles/17508
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Price Action Analysis Toolkit Development (Part 61): Structural Slanted Trendline Breakouts with 3-Swing Validation
Beginner to Intermediate Level: Struct (IV)
MQL5 Trading Tools (Part 18): Rounded Speech Bubbles/Balloons with Orientation Control
ARIMA Forecasting Indicator in MQL5
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use