Why Your EA Needs Partial Close Capability
Most beginner Expert Advisors treat a trade as binary: open at price X, close everything at price Y. That works for simple strategies, but it leaves serious money on the table. Markets rarely move in straight lines. A trend that runs 200 pips often retraces 50 pips before continuing. If you close your entire position at the first target, you miss the bigger move. If you hold everything for the bigger move, you give back gains on the retracement.
Partial close solves this. You take profit on a portion of your position at a conservative target, then let the remainder ride with a trailing stop or a wider target. The result is a smoother equity curve, reduced psychological pressure, and better risk-adjusted returns.
In MQL4, implementing partial close is not as straightforward as calling OrderClose(). You need to understand how MetaTrader handles ticket numbers, lot sizes, and position modifications. This post walks through a battle-tested approach that I have used in live EAs for years.
How MQL4 Handles Positions and Tickets
Every open order in MetaTrader 4 gets a unique ticket number. When you call OrderClose(), you close that exact ticket. To partially close a position, you must reduce the lot size of the existing ticket. MQL4 does not have a built-in "partial close" function, but you can achieve it by closing a portion of the lot size and leaving the rest open under the same ticket.
The key function is OrderClose() with a volume parameter smaller than the original lot size. For example, if you opened 1.0 lot and want to close 0.3 lot, you call OrderClose(ticket, 0.3, close_price, slippage). The remaining 0.7 lot stays open under the same ticket number.
There is a catch: MetaTrader does not allow you to close a portion if the remaining lot size would fall below the broker's minimum lot size. You must check MarketInfo(Symbol(), MODE_MINLOT) before executing the partial close.
Understanding Lot Size Normalization
Another critical detail: lot sizes must be normalized to the broker's lot step. Use MarketInfo(Symbol(), MODE_LOTSTEP) to get the step value, then round your close volume accordingly. For example, if MODE_LOTSTEP is 0.01, a close volume of 0.23 is valid, but 0.234 is not. Use the NormalizeDouble() function with the correct number of decimal places derived from the lot step.
double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
int lotDigits = (lotStep == 0.01) ? 2 : (lotStep == 0.1) ? 1 : 0;
double normalizedCloseVolume = NormalizeDouble(closeVolume, lotDigits);
Failing to normalize can cause OrderClose() to fail with error 4108 (invalid trade parameters) or error 138 (requote).
The Core MQL4 Partial Close Function
Below is a robust function I use in production EAs. It handles validation, slippage, and error checking.
//+------------------------------------------------------------------+
//| Partial close a specified volume from a given ticket |
//+------------------------------------------------------------------+
bool PartialCloseOrder(int ticket, double closeVolume, int slippage = 3)
{
if(!OrderSelect(ticket, SELECT_BY_TICKET))
{
Print("OrderSelect failed for ticket ", ticket, " error: ", GetLastError());
return false;
}
double currentLots = OrderLots();
if(closeVolume >= currentLots)
{
Print("Close volume must be less than current lots");
return false;
}
double minLot = MarketInfo(OrderSymbol(), MODE_MINLOT);
double remainingLots = currentLots - closeVolume;
if(remainingLots < minLot)
{
Print("Remaining lots (", remainingLots, ") below minimum (", minLot, ")");
return false;
}
double closePrice = (OrderType() == OP_BUY) ? MarketInfo(OrderSymbol(), MODE_BID) : MarketInfo(OrderSymbol(), MODE_ASK);
bool result = OrderClose(ticket, closeVolume, closePrice, slippage, clrNONE);
if(!result)
{
Print("OrderClose partial failed for ticket ", ticket, " error: ", GetLastError());
}
return result;
}
This function selects the order by ticket, validates that the requested close volume leaves at least the minimum lot, and executes the close at the current market price. It returns true on success and logs errors on failure.
Handling Multiple Partial Closes
If your strategy requires taking profit at multiple levels, you need to track how much has already been closed from each position. I recommend storing the original lot size in a global array or a file, then calculating the remaining volume before each partial close.
// Example: track original lots for each ticket
double originalLots[100];
int ticketArray[100];
int totalTickets = 0;
// When opening a trade
void OnTick()
{
// ... entry logic ...
int ticket = OrderSend(...);
if(ticket > 0)
{
ticketArray[totalTickets] = ticket;
originalLots[totalTickets] = lotSize;
totalTickets++;
}
}
Then, when you want to close 30% of a position, you calculate closeVolume = originalLots[index] * 0.3 and call PartialCloseOrder(ticketArray[index], closeVolume).
Edge Case: Broker Restrictions on Partial Closes
Some brokers, particularly those using hedging models or FIFO rules, may restrict partial closes on certain account types. For example, US brokers under NFA regulations require FIFO (First In, First Out) for retail forex accounts. In such cases, you cannot partially close a specific ticket; you must close the oldest position first. Always test your EA on a demo account with your target broker before going live.
Another edge case: if the market is closed or during a rollover period, OrderClose() may fail with error 145 (market closed). Your EA should handle this gracefully by retrying on the next tick.
Practical Strategy: Scaling Out with ATR Targets
Let me share a concrete strategy I have used in my own EAs. The idea is to enter a trend trade and scale out in three tranches based on Average True Range (ATR) multiples.
| Tranche | Percentage of Position | Target (ATR Multiple) | Stop Loss |
|---|---|---|---|
| First | 40% | 1.0 x ATR | Entry - 1.5 x ATR |
| Second | 30% | 2.0 x ATR | Breakeven |
| Third | 30% | 3.0 x ATR | Trailing stop at 1.0 x ATR |
The logic in your EA's OnTick() checks the current profit against each target. When price hits the first target, you call PartialCloseOrder(ticket, originalLots * 0.4, slippage). When price reaches the second target, you close another 30%. The remainder runs with a trailing stop.
Why ATR-Based Targets Work Better Than Fixed Pip Targets
Fixed pip targets ignore market volatility. On a quiet day, 20 pips might be a major move. On a volatile news day, 50 pips can happen in minutes. ATR adapts to current volatility. In the table above, if ATR is 40 pips, your first target is 40 pips, second is 80 pips, third is 120 pips. On a low-volatility day with ATR of 20 pips, those targets shrink proportionally. This keeps your risk-reward ratio consistent across market conditions.
Pros, Cons, and Risks of Partial Close
Advantages
- Improved risk-adjusted returns: Locking in profits early reduces the impact of reversals on your overall P&L.
- Reduced emotional stress: Knowing you have already banked some profit makes it easier to let the rest run.
- Better alignment with market structure: Partial closes mimic how professional traders scale out of positions.
- Flexibility in volatile markets: You can adjust the percentage closed per tranche based on current volatility without changing the core logic.
Disadvantages
- Increased complexity: Tracking multiple partial closes across multiple positions requires careful state management in your EA.
- Broker limitations: Some brokers impose minimum lot sizes that prevent very small partial closes.
- Potential for over-trading: If you set too many targets, you might close too much too early and miss the trend.
- Backtesting challenges: Strategy Tester does not always model partial closes accurately, especially with pending orders or multiple positions.
Key Risks
- Slippage during volatile markets: Partial closes executed during news events can suffer from significant slippage.
- Error handling failures: If your EA does not properly check return values, a failed partial close could leave your position management in an inconsistent state.
- Over-optimization temptation: It is easy to overfit the target levels and percentages to historical data. Forward test your parameters.
- Account type conflicts: On hedging accounts, partial closes may behave differently than on netting accounts. Know your account type.
Walkthrough: Building a Simple Partial Close EA
Let me walk through building a minimal Expert Advisor that enters a long trade on a moving average crossover and scales out using the ATR targets described above.
Step 1: Define Input Parameters
input double LotSize = 0.1;
input int FastMAPeriod = 10;
input int SlowMAPeriod = 30;
input int ATRPeriod = 14;
input double Target1ATR = 1.0;
input double Target2ATR = 2.0;
input double Target3ATR = 3.0;
input double StopLossATR = 1.5;
input int Slippage = 3;
These inputs allow you to tune the strategy without recompiling. The LotSize is the full position size before scaling out. The ATR periods and targets control when each tranche closes.
Step 2: Entry Logic
In OnTick(), check if the fast MA crosses above the slow MA. If yes and no position exists, send a buy order. Store the ticket and original lot size.
// Inside OnTick()
if(CountOpenPositions() == 0)
{
double fastMA = iMA(NULL, 0, FastMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 0);
double slowMA = iMA(NULL, 0, SlowMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 0);
double prevFastMA = iMA(NULL, 0, FastMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 1);
double prevSlowMA = iMA(NULL, 0, SlowMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 1);
if(prevFastMA <= prevSlowMA && fastMA > slowMA)
{
int ticket = OrderSend(Symbol(), OP_BUY, LotSize, Ask, Slippage, 0, 0, "Partial Close EA", MagicNumber, 0, clrGreen);
if(ticket > 0)
{
ticketArray[totalTickets] = ticket;
originalLots[totalTickets] = LotSize;
totalTickets++;
}
}
}
Step 3: Partial Close Management
On each tick, loop through open positions. For each position, calculate the current profit in pips. Compare against the ATR-based targets. When a target is hit, call PartialCloseOrder() with the appropriate volume. Use a flag array to prevent closing the same tranche twice.
bool tranche1Closed[100];
bool tranche2Closed[100];
// Inside OnTick()
for(int i = 0; i < totalTickets; i++)
{
if(!OrderSelect(ticketArray[i], SELECT_BY_TICKET)) continue;
double currentATR = iATR(OrderSymbol(), PERIOD_CURRENT, ATRPeriod, 0);
double profitPips = (OrderType() == OP_BUY) ? (Bid - OrderOpenPrice()) / Point : (OrderOpenPrice() - Ask) / Point;
Frequently Asked Questions
Can I partially close a position in MQL4 without using OrderClose?
No, MQL4 does not have a dedicated partial close function. You must use OrderClose() with a volume smaller than the original lot size. The remaining lot stays open under the same ticket number.
What happens if I try to close more lots than the minimum lot size allows?
The broker will reject the OrderClose() call and return error 130 (invalid stops) or 138 (requote). Always check MarketInfo(Symbol(), MODE_MINLOT) before executing a partial close to ensure the remaining volume meets the minimum requirement.
How do I track multiple partial closes on the same position?
Store the original lot size when the position opens, then use boolean flags or an integer counter to track which tranches have been closed. Update the flags only after a successful OrderClose() call.
Can I use partial close in MQL5 the same way?
MQL5 uses a different position model. In MQL5, you use PositionSelect() and then call PositionClosePartial() with the desired volume. The logic is similar but the function names and position handling differ significantly from MQL4.






