Skip to content

Order Management & Trade Execution

CTrade class, order types, stop-loss, take-profit, trailing stops, and partial close operations.

💡
Before you start

A C++ compiler and a terminal. No MetaTrader, no broker, no account and no money, and nothing below can place, modify or close a real trade. Most Linux machines already have g++; on macOS run xcode-select --install once; on Windows use the Windows Subsystem for Linux or MSYS2.

If you have done an earlier MQL5 page you already have the two setup files — reuse them and start at step 3.

Every bug on this page is invisible in normal operation. They appear when there are several positions to handle at once, when another EA shares the account, or when the broker refuses something — which is to say, at the worst possible moment. That is why they are worth reproducing deliberately.

Order Types in MT5

MT5 supports six order types, each serving a different purpose:

  • Market Orders — Execute immediately at the current price. ORDER_TYPE_BUY and ORDER_TYPE_SELL.
  • Limit Orders — Execute at a specified price or better. ORDER_TYPE_BUY_LIMIT (buy below current price) and ORDER_TYPE_SELL_LIMIT (sell above current price).
  • Stop Orders — Execute when price reaches a specified level. ORDER_TYPE_BUY_STOP (buy above current price) and ORDER_TYPE_SELL_STOP (sell below current price).
  • Stop-Limit Orders — A combination: when price reaches the stop level, a limit order is placed. ORDER_TYPE_BUY_STOP_LIMIT and ORDER_TYPE_SELL_STOP_LIMIT.

The CTrade Class

The standard library's CTrade class simplifies order execution:

#include <Trade/Trade.mqh>

CTrade trade;

void OnInit()
{
    trade.SetExpertMagicNumber(12345);
    trade.SetDeviationInPoints(10);  // max allowed slippage
    trade.SetTypeFilling(ORDER_FILLING_FOK);  // fill or kill
}

// Market orders
trade.Buy(0.1, _Symbol, ask, sl, tp, "Buy signal");
trade.Sell(0.1, _Symbol, bid, sl, tp, "Sell signal");

// Pending orders
trade.BuyLimit(0.1, limitPrice, _Symbol, sl, tp);
trade.SellStop(0.1, stopPrice, _Symbol, sl, tp);

// Close position
trade.PositionClose(_Symbol);

// Modify position SL/TP
trade.PositionModify(ticket, newSL, newTP);

Stop-Loss and Take-Profit

Always set stop-loss on every trade. It is your maximum acceptable loss per trade.

double CalculateSL(ENUM_ORDER_TYPE orderType, double entryPrice,
                   int slPoints)
{
    if(orderType == ORDER_TYPE_BUY)
        return NormalizeDouble(entryPrice - slPoints * _Point, _Digits);
    else
        return NormalizeDouble(entryPrice + slPoints * _Point, _Digits);
}

double CalculateTP(ENUM_ORDER_TYPE orderType, double entryPrice,
                   int tpPoints)
{
    if(orderType == ORDER_TYPE_BUY)
        return NormalizeDouble(entryPrice + tpPoints * _Point, _Digits);
    else
        return NormalizeDouble(entryPrice - tpPoints * _Point, _Digits);
}

Trailing Stop Implementation

A trailing stop moves the stop-loss in the direction of profit, locking in gains:

input int TrailingStop = 50;     // Trailing Stop (points)
input int TrailingStep = 10;    // Trailing Step (points)

void ManageTrailingStop()
{
    if(!PositionSelect(_Symbol)) return;

    double currentSL = PositionGetDouble(POSITION_SL);
    long posType = PositionGetInteger(POSITION_TYPE);
    ulong ticket = PositionGetInteger(POSITION_TICKET);

    if(posType == POSITION_TYPE_BUY)
    {
        double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
        double newSL = bid - TrailingStop * _Point;

        // Only move SL up, never down
        if(newSL > currentSL + TrailingStep * _Point)
        {
            double tp = PositionGetDouble(POSITION_TP);
            trade.PositionModify(ticket, NormalizeDouble(newSL, _Digits), tp);
        }
    }
    else if(posType == POSITION_TYPE_SELL)
    {
        double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double newSL = ask + TrailingStop * _Point;

        // Only move SL down, never up
        if(newSL < currentSL - TrailingStep * _Point || currentSL == 0)
        {
            double tp = PositionGetDouble(POSITION_TP);
            trade.PositionModify(ticket, NormalizeDouble(newSL, _Digits), tp);
        }
    }
}

Partial Close

Close part of a position to lock in partial profits:

void PartialClose(double closePercent)
{
    if(!PositionSelect(_Symbol)) return;

    double volume = PositionGetDouble(POSITION_VOLUME);
    double closeVolume = NormalizeDouble(volume * closePercent / 100.0, 2);

    // Ensure minimum lot size
    double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
    if(closeVolume < minLot) closeVolume = minLot;

    // Cannot close more than we have
    if(closeVolume >= volume) closeVolume = volume;

    ulong ticket = PositionGetInteger(POSITION_TICKET);
    trade.PositionClosePartial(ticket, closeVolume);
}

Error Handling

Always check the result of trade operations:

bool OpenBuyPosition(double lots, double sl, double tp)
{
    double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

    if(!trade.Buy(lots, _Symbol, ask, sl, tp, "Buy"))
    {
        Print("Buy failed. Error: ", GetLastError(),
              " Retcode: ", trade.ResultRetcode(),
              " Comment: ", trade.ResultComment());
        return false;
    }

    Print("Buy opened. Ticket: ", trade.ResultOrder(),
          " Price: ", trade.ResultPrice());
    return true;
}

Manage Positions Without Touching Somebody Else's, in Five Steps

Opening a position is the easy half. The awkward half is everything afterwards: recognising which of the positions on an account are yours, closing them without skipping half of them, and moving a stop without generating hundreds of rejected requests a minute. All three have specific, mechanical failure modes that look like working code. In the next half hour you will reproduce each one and fix it. Every line of output below came from running these files.

1
Save the header that supplies MetaTrader's built-ins

Go: open a terminal in a folder you can write to — cd ~/Desktop on macOS or Linux, cd %USERPROFILE%\Desktop on Windows.

Do: save this as mql5.h. If you already have it from an earlier page, reuse that copy and skip to step 3.

// mql5.h - a small shim that lets a useful SUBSET of MQL5 compile and run
// with an ordinary C++ compiler, so you can practise the language without
// MetaTrader. It is NOT MetaTrader and does not trade anything.
#pragma once
#include <cstdio>
#include <cmath>
#include <string>
#include <vector>
#include <iostream>
#include <sstream>

typedef std::string string;
typedef long long datetime;
#define input static

template <typename T> int ArraySize(const std::vector<T> &a) { return (int)a.size(); }
template <typename T> void ArrayResize(std::vector<T> &a, int n) { a.resize(n); }

inline void Print() { std::cout << std::endl; }
template <typename T, typename... Rest>
void Print(const T &first, const Rest &...rest) {
    std::cout << first;
    if constexpr (sizeof...(rest) > 0) Print(rest...);
    else std::cout << std::endl;
}

inline string DoubleToString(double v, int digits = 8) {
    char buf[64]; snprintf(buf, sizeof buf, "%.*f", digits, v); return string(buf);
}
inline string IntegerToString(long long v) { return std::to_string(v); }
inline double MathAbs(double v)  { return std::fabs(v); }
inline double MathMax(double a, double b) { return a > b ? a : b; }
inline double MathMin(double a, double b) { return a < b ? a : b; }
inline int    MathRound(double v) { return (int)std::lround(v); }

You should see: nothing — this is a header file. Confirm with ls mql5.h (Windows: dir mql5.h).

If not: no compiler? On Ubuntu or Debian sudo apt install g++; on macOS xcode-select --install; on Windows use the Windows Subsystem for Linux or MSYS2.

2
Save the build script

Go: the same folder.

Do: save this as build.sh. It compiles a .mq5 file and executes the result. Make it executable with chmod +x build.sh, or invoke it as sh build.sh.

#!/bin/sh
# build.sh NAME  ->  compiles NAME.mq5 and runs it.
# MQL5 is close enough to C++ that a C++ compiler can run the subset used here,
# with mql5.h supplying the handful of built-ins MetaTrader would provide.
# Uses whichever compiler you have.
set -e
NAME="$1"
if command -v g++ >/dev/null 2>&1 && g++ -x c++ -std=c++17 -o "$NAME" "$NAME.mq5" 2>/dev/null; then
    :
elif command -v clang++ >/dev/null 2>&1; then
    clang++ -x c++ -std=c++17 -o "$NAME" "$NAME.mq5"
else
    echo "No C++ compiler found. Install g++ (Linux: apt install g++, macOS: xcode-select --install)." >&2
    exit 1
fi
"./$NAME"

You should see: nothing — this is the toolchain. ls should show both mql5.h and build.sh.

If not: Permission denied means the chmod was skipped; use sh build.sh NAME, which needs no execute bit.

3
Find only your own positions

Go: the same folder.

Do: save this as selectown.mq5 and run sh build.sh selectown.

#include "mql5.h"

//+------------------------------------------------------------------+
//| An account holds other people's trades too. Find only yours.      |
//+------------------------------------------------------------------+
struct Position { long ticket; string symbol; long magic; string type; double volume; };

const long MY_MAGIC = 20260822;

std::vector<Position> g_positions = {
    {5551001, "EURUSD", 20260822, "BUY",  0.10},   // mine
    {5551002, "EURUSD",        0, "BUY",  1.00},   // opened by hand
    {5551003, "GBPUSD", 20260822, "SELL", 0.20},   // mine, other symbol
    {5551004, "EURUSD", 99999999, "SELL", 0.50},   // another EA
    {5551005, "EURUSD", 20260822, "BUY",  0.30},   // mine
};

int PositionsTotal() { return ArraySize(g_positions); }

void OnStart()
{
    Print("positions on the account : ", PositionsTotal());
    Print("");
    Print("ticket    symbol   magic       type  volume   mine?");
    for(int i = 0; i < PositionsTotal(); i++)
    {
        Position p = g_positions[i];
        bool mine = (p.magic == MY_MAGIC);
        Print(p.ticket, "  ", p.symbol, "  ", p.magic, "   ", p.type,
              "   ", DoubleToString(p.volume, 2), "    ", mine ? "YES" : "no");
    }

    int all_mine = 0, mine_here = 0;
    double vol_here = 0.0;
    for(int i = 0; i < PositionsTotal(); i++)
    {
        Position p = g_positions[i];
        if(p.magic != MY_MAGIC) continue;
        all_mine++;
        if(p.symbol != "EURUSD") continue;
        mine_here++;
        vol_here += p.volume;
    }

    Print("");
    Print("mine, any symbol        : ", all_mine);
    Print("mine, on EURUSD         : ", mine_here);
    Print("my EURUSD volume        : ", DoubleToString(vol_here, 2));
    Print("");
    Print("An EA that closes 'all positions' closes five. An EA that filters on");
    Print("magic closes three. One that filters on magic AND symbol closes two --");
    Print("and only that last one is doing what a chart-attached EA should.");
    Print("");
    Print("Iterate DOWNWARD when you intend to close, because closing shortens");
    Print("the list and an upward loop then skips the element that shifted into");
    Print("the index you just handled.");
}

int main() { OnStart(); return 0; }

You should see: five positions on the account and two that belong to this EA on this chart:

positions on the account : 5

ticket    symbol   magic       type  volume   mine?
5551001  EURUSD  20260822   BUY   0.10    YES
5551002  EURUSD  0   BUY   1.00    no
5551003  GBPUSD  20260822   SELL   0.20    YES
5551004  EURUSD  99999999   SELL   0.50    no
5551005  EURUSD  20260822   BUY   0.30    YES

mine, any symbol        : 3
mine, on EURUSD         : 2
my EURUSD volume        : 0.40

An EA that closes 'all positions' closes five. An EA that filters on
magic closes three. One that filters on magic AND symbol closes two --
and only that last one is doing what a chart-attached EA should.

Iterate DOWNWARD when you intend to close, because closing shortens
the list and an upward loop then skips the element that shifted into
the index you just handled.

An account is shared territory. It can hold trades you opened by hand, trades from another EA, and trades from the same EA running on a different chart. PositionsTotal() counts all of them.

So every loop over positions needs two filters, and the magic number is what makes the first possible: it is a number you choose, attached to every order you send, and it is the only thing distinguishing your positions from anyone else's. Filter on the symbol as well, or an EA on the EURUSD chart will happily close the position its twin opened on GBPUSD.

Choose a magic number per strategy, not per chart, and write it down. Changing it later orphans every position the old value opened — they stay on the account, and your EA no longer recognises them.

If not: if mine, on EURUSD reports 3, the symbol filter is missing — the GBPUSD position carries the same magic and is correctly counted by the first filter and correctly excluded by the second.

4
Close them all without skipping half

Go: the same folder.

Do: save this as loopdir.mq5 and run sh build.sh loopdir.

#include "mql5.h"

//+------------------------------------------------------------------+
//| Closing while iterating. One direction works; the other skips.    |
//+------------------------------------------------------------------+
std::vector<long> MakeList()
{
    return {5551001, 5551002, 5551003, 5551004, 5551005};
}

int CloseUpward()
{
    std::vector<long> list = MakeList();
    int closed = 0;
    for(int i = 0; i < ArraySize(list); i++)
    {
        Print("   examining index ", i, " ticket ", list[i], " -> close");
        list.erase(list.begin() + i);       // the list shrinks under us
        closed++;
    }
    Print("   closed ", closed, ", still open ", ArraySize(list));
    return ArraySize(list);
}

int CloseDownward()
{
    std::vector<long> list = MakeList();
    int closed = 0;
    for(int i = ArraySize(list) - 1; i >= 0; i--)
    {
        Print("   examining index ", i, " ticket ", list[i], " -> close");
        list.erase(list.begin() + i);
        closed++;
    }
    Print("   closed ", closed, ", still open ", ArraySize(list));
    return ArraySize(list);
}

void OnStart()
{
    Print("five positions, closing every one.");
    Print("");
    Print("counting UP:");
    int left_up = CloseUpward();
    Print("");
    Print("counting DOWN:");
    int left_down = CloseDownward();

    Print("");
    Print("left open after the upward loop   : ", left_up);
    Print("left open after the downward loop : ", left_down);
    Print("");
    Print("The upward loop skips every other one. Removing index 0 slides the");
    Print("old index 1 down into index 0 -- and the loop then moves on to");
    Print("index 1, which is now the old index 2.");
    Print("");
    Print("This is why 'close all' code that looks obviously correct leaves");
    Print("half the positions open, and why it is worse under stress: with");
    Print("more positions, more survive.");
}

int main() { OnStart(); return 0; }

You should see: the upward loop leaving two positions open:

five positions, closing every one.

counting UP:
   examining index 0 ticket 5551001 -> close
   examining index 1 ticket 5551003 -> close
   examining index 2 ticket 5551005 -> close
   closed 3, still open 2

counting DOWN:
   examining index 4 ticket 5551005 -> close
   examining index 3 ticket 5551004 -> close
   examining index 2 ticket 5551003 -> close
   examining index 1 ticket 5551002 -> close
   examining index 0 ticket 5551001 -> close
   closed 5, still open 0

left open after the upward loop   : 2
left open after the downward loop : 0

The upward loop skips every other one. Removing index 0 slides the
old index 1 down into index 0 -- and the loop then moves on to
index 1, which is now the old index 2.

This is why 'close all' code that looks obviously correct leaves
half the positions open, and why it is worse under stress: with
more positions, more survive.

Closing a position removes it from the list, so everything after it shifts down one index — and a loop counting upward then moves past the element that slid into the slot it just handled. Two of five survive here; with twenty positions, ten survive.

Count downward whenever the loop can remove things. Removing index 4 does not disturb indexes 0 to 3, so nothing shifts underneath you.

This bug is unusually nasty because it is invisible in normal operation — an EA that usually holds one position never triggers it. It appears the first time something goes wrong and there are several to close at once, which is precisely the moment you need the code to work.

If not: if both loops close everything, the erase call was removed from one of them — the shrinking list is the mechanism being demonstrated.

5
Move a stop without spamming the broker

Go: the same folder.

Do: save this as trail.mq5 and run sh build.sh trail.

#include "mql5.h"

//+------------------------------------------------------------------+
//| A trailing stop, and the two ways it goes wrong.                  |
//+------------------------------------------------------------------+

const double POINT = 0.00001;
int          TRAIL_POINTS = 200;        // 20 pips on a 5-digit quote
const int    MIN_STOP_LEVEL = 30;       // broker minimum, in points
const double ENTRY = 1.10000;

int modifications = 0, rejected_no_change = 0, rejected_too_close = 0;

// Returns the new stop, or 0.0 for "do not send a modification".
double NewStop(double current_price, double current_stop)
{
    double wanted = current_price - TRAIL_POINTS * POINT;

    // A trailing stop only ever moves UP for a buy.
    if(wanted <= current_stop) { rejected_no_change++; return 0.0; }

    // The broker refuses a stop closer than MIN_STOP_LEVEL to the price.
    if(current_price - wanted < MIN_STOP_LEVEL * POINT)
    { rejected_too_close++; return 0.0; }

    return wanted;
}

void OnStart()
{
    double prices[8] = {1.10050, 1.10120, 1.10090, 1.10250,
                        1.10300, 1.10180, 1.10400, 1.10390};
    double stop = ENTRY - 300 * POINT;

    Print("entry ", DoubleToString(ENTRY, 5), "  initial stop ",
          DoubleToString(stop, 5));
    Print("");
    Print("price      wanted stop   action");
    for(int i = 0; i < 8; i++)
    {
        double n = NewStop(prices[i], stop);
        if(n > 0.0)
        {
            modifications++;
            Print(DoubleToString(prices[i], 5), "    ", DoubleToString(n, 5),
                  "     MODIFY (stop moves up)");
            stop = n;
        }
        else
            Print(DoubleToString(prices[i], 5), "    ", DoubleToString(
                  prices[i] - TRAIL_POINTS * POINT, 5), "     no change");
    }

    Print("");
    Print("final stop            : ", DoubleToString(stop, 5));
    Print("modifications sent    : ", modifications);
    Print("skipped, would not move: ", rejected_no_change);
    Print("skipped, too close     : ", rejected_too_close);
    // Now prove the SECOND guard fires too, by trailing tighter than the
    // broker's minimum stop level allows.
    Print("");
    Print("the same prices with a 20-point trail (broker minimum is 30):");
    TRAIL_POINTS = 20;
    rejected_too_close = 0;
    int accepted_tight = 0;
    stop = ENTRY - 300 * POINT;
    for(int i = 0; i < 8; i++)
    {
        double n = NewStop(prices[i], stop);
        if(n > 0.0) { accepted_tight++; stop = n; }
    }
    Print("   modifications accepted : ", accepted_tight);
    Print("   rejected as too close  : ", rejected_too_close);
    Print("   -> a trail tighter than the minimum stop level can never work");

    Print("");
    Print("Both guards matter. Without the first, every tick where the price");
    Print("falls sends a modification moving the stop DOWN -- which widens your");
    Print("risk, and is the opposite of trailing.");
    Print("");
    Print("Without the second, the broker rejects the request with retcode");
    Print("10016 (invalid stops), and an EA that retries every tick generates");
    Print("hundreds of failed requests a minute. Some brokers throttle or");
    Print("disconnect an account doing that.");
}

int main() { OnStart(); return 0; }

You should see: five modifications from eight prices, then a tighter trail that can never work:

entry 1.10000  initial stop 1.09700

price      wanted stop   action
1.10050    1.09850     MODIFY (stop moves up)
1.10120    1.09920     MODIFY (stop moves up)
1.10090    1.09890     no change
1.10250    1.10050     MODIFY (stop moves up)
1.10300    1.10100     MODIFY (stop moves up)
1.10180    1.09980     no change
1.10400    1.10200     MODIFY (stop moves up)
1.10390    1.10190     no change

final stop            : 1.10200
modifications sent    : 5
skipped, would not move: 3
skipped, too close     : 0

the same prices with a 20-point trail (broker minimum is 30):
   modifications accepted : 0
   rejected as too close  : 8
   -> a trail tighter than the minimum stop level can never work

Both guards matter. Without the first, every tick where the price
falls sends a modification moving the stop DOWN -- which widens your
risk, and is the opposite of trailing.

Without the second, the broker rejects the request with retcode
10016 (invalid stops), and an EA that retries every tick generates
hundreds of failed requests a minute. Some brokers throttle or
disconnect an account doing that.

The first guard is what makes it a trailing stop rather than a moving one. Without it, every tick where the price falls sends a modification putting the stop lower — which widens your loss and is the exact opposite of the intent. A trailing stop for a buy moves up or not at all.

The second guard reflects a real broker constraint: a stop closer to the current price than the minimum stop level is rejected with retcode 10016. The final block proves it by trailing at 20 points against a 30-point minimum — eight attempts, zero accepted, and in a live EA that is eight rejected requests on eight consecutive ticks, repeated for as long as the position is open. Some brokers throttle an account behaving that way; some disconnect it.

Read the minimum with SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) rather than assuming a value, because it varies by symbol and some brokers widen it around news.

If not: if the tighter-trail block reports accepted modifications, TRAIL_POINTS was left as a const — it has to be reassignable for the second experiment, which is why it is a plain int.

🎉
Check yourself before moving on

Without scrolling up: an EA has an emergency routine that closes everything when the account drawdown exceeds a limit. It was triggered for the first time last week, and afterwards three of the seven open positions were still there. The routine ran without errors. What happened, and what else about that routine would you want to check? Answer: it iterated upward while closing. Step 4 showed that closing shortens the list, so everything after the closed position shifts down an index and the loop skips it — five positions left two behind, and seven leaving three is exactly the same pattern. The fix is to count downward from PositionsTotal() - 1 to zero. Two other things about that routine are worth checking while it is open. First, whether it filters on magic and symbol — an emergency close that catches everything will also close positions another EA or a person opened, which in a drawdown emergency may be the last thing you want. Second, whether it checks the retcode of each close: a close can be refused for reasons unrelated to your loop, such as the market being closed or the position already being closed by a stop, and a routine that assumes success leaves you believing you are flat when you are not.

Now do it without the page: write a CloseAllMine() function combining all three lessons: count downward, filter on magic and symbol, and check the retcode of every close, retrying the failures once. Then test it against a list where one close deliberately fails, and decide what your function should do — retry forever, retry once, or report and stop. There is no universally right answer, and choosing it deliberately is the difference between a routine you can rely on in an emergency and one you find out about during it.

Iterating Through Positions

void ListAllPositions()
{
    int total = PositionsTotal();
    for(int i = 0; i < total; i++)
    {
        ulong ticket = PositionGetTicket(i);
        if(ticket > 0)
        {
            string symbol = PositionGetString(POSITION_SYMBOL);
            double profit = PositionGetDouble(POSITION_PROFIT);
            long magic = PositionGetInteger(POSITION_MAGIC);

            Print("Ticket: ", ticket,
                  " Symbol: ", symbol,
                  " Profit: ", DoubleToString(profit, 2),
                  " Magic: ", magic);
        }
    }
}