Most EAs size each trade carefully. Risk one percent of equity, compute the lot size from the stop distance, done. Then the EA opens a second trade, and a third, on the same or related symbols, and each one is sized as if it were the only trade on the account. Five positions at one percent each is five percent of equity that can disappear on one bad afternoon.
A total open risk cap fixes that. Before any new entry, the EA adds up what it would lose if every stop it already has were hit, adds the risk of the new trade, and refuses the entry if the sum is above a limit.
This tutorial builds that check in three small functions.
Step 1: the inputs
input double MaxOpenRiskPercent = 3.0; // total money at risk at the stops, percent of equity
input long MagicNumber = 20261103; // only positions with this magic are counted
Validate the cap in OnInit so a zero or negative value never reaches the math:
int OnInit()
{
if(MaxOpenRiskPercent <= 0.0 || MaxOpenRiskPercent > 100.0)
{
Print("MaxOpenRiskPercent must be above 0 and at most 100");
return INIT_PARAMETERS_INCORRECT;
}
return INIT_SUCCEEDED;
}
Step 2: the risk of one position
The money lost if a position hits its stop depends on the symbol's contract size, the profit currency and the account currency. Instead of rebuilding that from tick values, let the terminal do it with OrderCalcProfit, which computes the result of a trade between two prices in the account currency.
There is a choice to make about where to measure from. Measuring from the entry price gives the risk you planned when the trade was opened. Measuring from the current price gives how much equity can still fall from here, which is what a cap on the account should care about. This version uses the current price, so a position whose stop has been moved into profit still counts the profit it can give back.
// Money lost from the current price if this position hits its stop.
// Returns a negative value if the position has no stop or the calculation fails.
double PositionRiskMoney(const ulong ticket)
{
if(!PositionSelectByTicket(ticket))
return -1.0;
double sl = PositionGetDouble(POSITION_SL);
if(sl == 0.0)
return -1.0;
string symbol = PositionGetString(POSITION_SYMBOL);
double volume = PositionGetDouble(POSITION_VOLUME);
double current = PositionGetDouble(POSITION_PRICE_CURRENT);
bool isBuy = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
ENUM_ORDER_TYPE type = isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
double result = 0.0;
if(!OrderCalcProfit(type, symbol, volume, current, sl, result))
return -1.0;
// A loss comes back as a negative number. A stop on the far side of the
// current price would be hit already, so treat a positive result as zero risk.
return (result < 0.0) ? MathAbs(result) : 0.0;
}
A position without a stop has no defined risk. Returning a negative value lets the caller decide, and the safe decision is to block new trades until that position has a stop.
Step 3: the total for the EA
Loop over every position, keep the ones with this EA's magic number, and add up their risk. On a hedging account there can be several positions per symbol, so loop by index with PositionGetTicket rather than selecting by symbol.
// Sum of money at risk at the stops for all positions of this EA.
// Returns a negative value if any of them has no usable stop.
double TotalOpenRisk()
{
double total = 0.0;
int count = PositionsTotal();
for(int i = 0; i < count; i++)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0)
continue;
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber)
continue;
double risk = PositionRiskMoney(ticket);
if(risk < 0.0)
{
PrintFormat("Position %I64u has no usable stop, new entries blocked", ticket);
return -1.0;
}
total += risk;
}
return total;
}
If several EAs share the account and the cap should cover all of them, drop the magic filter. Then the cap protects the account rather than one strategy.
Step 4: the gate before every entry
The new trade has not been sent yet, so its risk is computed from the planned entry price and stop.
// True if opening this trade keeps the total open risk within the cap.
bool OpenRiskAllows(const string symbol, const ENUM_ORDER_TYPE type,
const double volume, const double price, const double sl)
{
double openRisk = TotalOpenRisk();
if(openRisk < 0.0)
return false;
double result = 0.0;
if(!OrderCalcProfit(type, symbol, volume, price, sl, result))
{
Print("OrderCalcProfit failed for the new trade, error ", GetLastError());
return false;
}
double newRisk = (result < 0.0) ? MathAbs(result) : 0.0;
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double cap = equity / (100.0 / MaxOpenRiskPercent);
if(openRisk + newRisk > cap)
{
PrintFormat("Entry skipped: open risk %.2f plus new %.2f is above cap %.2f",
openRisk, newRisk, cap);
return false;
}
return true;
}
The cap divides equity by 100 over the percent, which is the same as taking that percent of it. Call the gate right before sending the order, with the exact volume and stop the order will carry:
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = ask - stopDistance;
if(OpenRiskAllows(_Symbol, ORDER_TYPE_BUY, lots, ask, sl))
trade.Buy(lots, _Symbol, ask, sl, 0.0);
What this check does not cover
- Commission, swap and slippage. OrderCalcProfit returns the price result only. If stops often fill worse than their level, keep the cap lower than the number you have in mind.
- Gaps. Over a weekend or a news spike, a stop can fill far beyond its price, so the real loss can be larger than the computed one.
- Correlation. Three positions on EURUSD, GBPUSD and EURGBP are not three independent risks. The cap limits the total, but it treats them as if they could all lose at once, which is the cautious assumption.
- Pending orders. A buy stop that fills adds risk the moment it fills. If the EA uses pending orders, include them in the total with their own entry and stop prices.
Testing it
Run the EA in the Strategy Tester on a period with several overlapping signals and set the cap low, for example one and a half percent with one percent per trade. The log should show the second entry being skipped while the first is open, and allowed again after the first closes or its stop moves closer to the price. If the log never shows a skipped entry, the gate is not being called where you think it is.
Sizing each trade is half of risk control. The other half is knowing how much is on the table at the same time.
Top comments (0)