Your EA says it’s risking 2% per trade. Your account statement says otherwise. Consider a “2% risk” EA on XAUUSD that actually opens positions at 3.75% — nearly doubling the intended exposure on every trade for weeks before anyone notices. The lot size calculation has three bugs. All three compile clean, pass every backtest, and survive demo testing.
Most EAs calculate position size incorrectly because they confuse Point with tick size, bypass platform-provided tick value with manual formulas, and skip lot step rounding. In MT4 an off-step lot opens smaller than calculated, with no error; in MT5 it is rejected. Either way, an EA that does not check never learns that its actual risk differs from the intended risk.
The Risk You Think You’re Taking vs. the Risk You Actually Are
When lot size is wrong, the dollar risk per trade silently drifts from the trader’s target. A 2% risk setting might execute as 0.8% or 4.5% — the trader has no way to tell from the terminal alone, because the order fills successfully either way.
It is the kind of bug a code rescue review is for. In the XAUUSD example, the EA has been live for six weeks. It calculates 0.15 lots per trade where 0.08 is correct, so every single trade risks nearly double the intended amount. The trader only discovers the problem when a losing streak hits harder than the risk model predicted — because the risk model was based on a number the EA never actually used.
The root cause is always one of three formula errors. Sometimes all three at once.
Failure #1 — Point vs. Tick Size in Lot Size Calculation
MQL’s Point returns the smallest price increment for the current symbol. On a 5-digit EURUSD broker, Point = 0.00001. A pip is 0.0001. That is a factor-of-10 difference.
Here is the broken formula:
lots = AccountBalance * RiskPercent / (SL_pips * Point * TickValue)
The bug is Point. If the developer means “pip value” but writes Point, the result is 10x too large on 5-digit forex pairs. On XAUUSD, depending on broker configuration, the error can be 10x or 100x — because Point on gold is often 0.01 while the trader thinks in dollar-per-ounce moves.
The fix is straightforward: stop using Point as a pip proxy. Either define pip size explicitly per instrument category, or — better — work in ticks throughout the calculation and use SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) (opens in a new tab) instead of Point. Ticks are unambiguous. They represent the actual minimum price movement the broker recognizes, with no interpretation needed.
This is the most common of the three failures. It is also the one that produces the largest errors — off by a full order of magnitude on standard instruments. If your EA works on EURUSD but breaks on everything else, this is often the first place to look.
Failure #2 — Manual Pip Value Instead of TickValue
MQL provides SYMBOL_TRADE_TICK_VALUE (opens in a new tab) — the monetary value of one tick for a 1.0 lot position, already converted to the account currency at the current exchange rate. It handles USD accounts, EUR accounts, cross pairs, gold, indices — everything. It updates every tick.
Many EAs do not use it. Instead, they hardcode pip value or calculate it manually:
double pipValue = 10.0; // "works for EURUSD on a USD account"
Or the more ambitious version:
double pipValue = lotSize * Point / MarketInfo(Symbol(), MODE_POINT);
These manual formulas encode assumptions about the account currency, the instrument category, and the broker’s contract size. They work on the developer’s test account — typically a USD-denominated EURUSD setup — and break everywhere else. A EUR-denominated account gets a pip value that is off by the current EUR/USD exchange rate. A cross pair like GBPJPY on a USD account requires a conversion through a third currency that the manual formula does not perform.
The other trap: TickValue returns zero when the market is closed (weekends, holidays) or when the broker’s conversion pair feed is unavailable. An EA that reads TickValue at the wrong moment — inside OnInit() on a Sunday, for example — gets zero, and the lot size formula divides by zero, a critical error that terminates the EA. The EA should read TickValue immediately before OrderSend() during active trading hours, and treat a zero value as a hard stop: do not place the trade.
Failure #3 — Lot Step and Lot Normalization
Brokers enforce three constraints on lot size: minimum lot, maximum lot, and lot step. The lot step is the smallest allowed increment — typically 0.01 for retail accounts, sometimes 0.1 or 1.0 for certain instruments.
If the formula produces 0.087 lots and the lot step is 0.01, what happens depends on the platform. In our tests on demo accounts and in the Strategy Tester, MT4 opened such a volume at 0.08 lots with no error: the terminal cut it to two decimals before sending it, so 0.0867 became 0.08, not 0.09. MT5 rejected the same request with retcode 10014 (invalid volume). The EA controls the result only if it normalizes the value itself before sending the order.
The EA should do this:
double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double lots = MathFloor(rawLots / step) * step;
MathFloor (opens in a new tab) always rounds down, guaranteeing the trader never risks more than intended. Then check the broker’s limits. A lot below the minimum means the risk budget is too small for this stop, so the trade is skipped rather than rounded up:
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
if(lots < minLot)
return; // risk budget below the minimum lot: no trade
lots = MathMin(maxLot, lots);
Without this normalization, an MT4 order still executes, smaller than calculated and with no error. The trader sees a filled position and assumes everything matched the EA’s calculation. It did not. An MT5 order fails instead, and an EA that does not check the result never opens the trade.
The Silent Acceptance Problem
This is what makes lot size bugs uniquely persistent. Other EA errors produce feedback. Place a stop loss too close — the broker returns error 130. Insufficient margin — error 134. Invalid price — error 129. The EA can catch these, log them, retry with corrected values.
In MT4, lot size adjustment produces no error. OrderSend() returns a valid ticket. The position appears in the terminal. Nothing in the EA’s error handling detects the discrepancy, because there is no discrepancy from the broker’s perspective: the terminal sent the smaller volume, and the broker filled a valid order.
The only way to detect the mismatch: compare the lot size the EA calculated with the lot size on the actual order. In MQL4, that means selecting the new ticket with OrderSelect() and comparing OrderLots() with the value the EA intended to send. In MQL5 an off-step request fails instead, so the check there is the trade result: an EA that ignores retcode 10014 never opens the trade. Many EAs make neither check. The bug runs indefinitely.
This is why lot size errors survive every testing stage. They compile. They pass backtest (the Strategy Tester handles lot size exactly as live does). They pass demo testing. They run live without errors. The trader’s risk is wrong on every trade, and the only symptom is a P&L that does not match the expected risk profile — a signal that takes weeks or months to notice.
The Correct Lot Size Calculation
Here is a lot size function that handles all three failures:
double CalculateLotSize(const string symbol, const double riskPercent, const double slPoints)
{
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskMoney = balance * riskPercent / 100.0;
double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
if(tickSize == 0 || tickValue == 0)
return 0;
double ticks = slPoints / tickSize;
double rawLots = riskMoney / (ticks * tickValue);
double step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
double lots = MathFloor(rawLots / step) * step;
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
if(lots < minLot)
return 0;
lots = MathMin(maxLot, lots);
return lots;
}
Three things to note:
slPointsis in price distance, not pips. PassAsk - stopLossPrice(or equivalent) directly. No pip conversion needed — the function handles tick conversion internally usingSYMBOL_TRADE_TICK_SIZE.TickValueis read at call time. This function should be called immediately beforeOrderSend(), not cached at initialization. IfTickValuereturns zero (market closed, feed unavailable), the function returns zero — no trade.MathFloorensures the lot never exceeds intended risk. The trader may risk slightly less than 2% after rounding — never more. When the budget is smaller than the broker’s minimum lot, the function returns zero rather than rounding up to the minimum.
Check Your Own EA
Pull up your last five trades. For each one, compare the lot size your EA logged (or calculated) with the lot size in your account history. If they match exactly, your EA handles normalization correctly. If they differ — even by 0.01 — your EA sends off-step lots and MT4 is cutting them (Failure #3), so your actual risk per trade does not match what you configured. In MT5 an off-step lot does not open at all, so look for orders the EA tried to send that never opened. Matching lots rule out Failure #3 only: a lot from Failures #1 or #2 is wrong before it is sent, so compare it with what a position size calculator gives for the same stop and risk.
The fix is a single function. The cost of not fixing it is every trade at the wrong size, compounding silently, for as long as the EA runs.
Suspect your EA has one of these issues, or want a second pair of eyes on the position sizing logic? We review EA code and can identify lot size bugs in a single session.

Leave a Reply