How trade copiers work on MT4 and MT5

Master and follower accounts, volume and symbol mapping, partial closes, rejections and reconcile loops: how a trade copier works, and how to stop it copying a trade twice.

One master trading window fanning out to three follower windows, each copy carrying the same key tag.

A trade copier watches one trading account and repeats what happens there on one or more other accounts. The idea is simple. Doing it well is not, because the copier has to follow each trade through its whole life, across accounts that may sit with different brokers and differ in size.

Copiers tend to fail in two ways. They copy a trade twice, which doubles the risk on the follower. Or they silently miss one, which leaves the follower out of step with nobody noticing.

This post walks through how a copier works on MetaTrader 4 (MT4) and MetaTrader 5 (MT5), what goes wrong at each step, and how a small reconcile loop with deterministic keys avoids both failures.

Masters, followers and how a copier sees a trade

The account being copied is the master (also called the source or provider). The accounts that receive the copies are followers (or receivers). A copier only ever reads the master. It writes to the followers.

What it copies is a position: an open trade on the account, with a symbol, a side (buy or sell), a volume in lots and, optionally, a stop loss and a take profit. A lot is the standard trade size; on most currency pairs one lot is 100,000 units of the base currency.

The copier first has to notice that something changed on the master. There are two ways.

  • Events. The platform or API pushes a message when a position opens, changes or closes, over a WebSocket stream or a webhook. Events are fast and cheap, but they can be missed: a socket drops, a webhook is delayed, a process restarts at the wrong moment.
  • Polling. The copier reads the master’s open positions every second or two and compares them with what it saw last time. Polling is slower, but each read is a complete picture.

A sound copier uses both: events trigger a sync quickly, and a timer runs the same sync every few seconds to catch anything an event missed. The sync compares full lists rather than trusting a single message. The trade-offs are covered in more depth in webhooks vs polling.

One rule applies to both: if the copier can’t currently see an account, it must say so. A master whose terminal has lost its broker connection may report an empty or stale position list. A copier that believes it will “close” every copy on the followers.

Sizing the copy and mapping symbols

The follower rarely trades the same volume as the master. Common sizing modes:

ModeFollower volumeGood for
FixedAlways the same, e.g. 0.10 lotsSimple, but ignores the master’s sizing
MultiplierMaster volume times a factor, e.g. 0.5Followers that are a fixed fraction of the master
By balanceMaster volume times follower balance divided by master balanceAccounts of different sizes
By equityThe same ratio, using equity (balance plus open profit or loss)Accounts carrying open trades

Whatever the mode, the result has to be a volume the follower’s broker accepts. Every symbol has a minimum volume, a maximum and a step (often 0.01). Round down to the step, never up, so a rounding error can’t make the copy bigger than intended. If the result is below the minimum, skip the copy and record why, rather than quietly trading the minimum. Watch for accounts in different currencies too: a balance ratio between a USD account and a cent account is off by a factor of 100.

Symbol names differ between brokers. One broker calls euro against dollar EURUSD, another EURUSDm or EURUSD.r. Keep a map per follower from the master’s names to the follower’s, built from the follower’s symbol list, and refuse to copy a symbol that has no mapping. Contract sizes can differ as well. Symbol names and specs covers this in detail.

Stop loss and take profit are absolute prices. Two brokers’ prices for the same symbol are close but not identical, so a copied stop sits a fraction off where the master’s does. Some copiers copy the distance from the entry price instead.

After the open: stop changes, partial closes and closes

Opening the copy is the easy part.

  • Stop loss and take profit changes. When the master moves a stop, the copier modifies the copy. Comparing current values on every sync beats replaying each modify event.
  • Partial closes. If the master closes half its position, the follower should close the matching share of the copy. Compute the copy’s target volume from the master’s remaining volume, then close the difference.
  • Closes. When the master position disappears, the copy is closed by its own id.

MT4 adds a twist. A partial close on MT4 leaves the remainder under a new ticket, and the broker usually rewrites the order comment to point back to the original. A copier that tracks MT4 positions by raw ticket loses the link at the first partial close. MT5 keeps one position identifier for the life of a position, so this is easier there.

On MT5 the master’s account type matters as well. A netting account holds one position per symbol, so the master adding to a position shows up as a volume increase, not a new trade. See netting vs hedging accounts for how the two behave.

Latency, slippage and rejections

A copy always happens after the original. The delay is the time to notice the change, plus the time to send the order, plus the follower broker’s execution time. During that gap the price moves, so the follower fills at a different price. That difference is slippage, and on a copier it is built in. For a slow strategy it hardly matters. For a scalping strategy that aims for a few pips (a pip is 0.0001 on most currency pairs), it can turn a winning master into a losing follower. See slippage, requotes and fills.

Followers also reject copies. Typical reasons, with the MT5 return codes you will see:

  • Not enough margin (TRADE_RETCODE_NO_MONEY). The follower is too small for the volume.
  • Market closed (TRADE_RETCODE_MARKET_CLOSED). Different brokers keep different trading hours.
  • Invalid stops or volume. The stop is too close, or the volume breaks the symbol’s rules.
  • Trading disabled. The follower was connected with a read-only (investor) password.

Decide each case in advance. Log and alert a margin rejection; don’t retry it in a loop. A market-closed rejection may deserve one later attempt, but catching up hours later at a very different price is a new trade, not a copy.

Never copy twice: keys and a reconcile loop

The safest copier is a reconcile loop: on every pass, read the master’s positions and each follower’s copies, work out the difference, and send only the orders that close the gap. Combined with an idempotency key, a unique key attached to each order request, it can’t double up. If the same key is sent twice, the server returns the first result instead of placing a second order.

The key must be derived from the master’s position id and the follower’s account id, never random and never the time. A worker that crashes and restarts then sends the same key for the same copy, and the duplicate is refused. A separate clientId on each copy, also derived from the master id, lets the loop find existing copies on the follower. The reasoning is in a retry should never place a second trade.

Here is a short Python sketch using requests and the routes from the copier guide. It copies opens, stop changes, partial closes and closes for a multiplier mode.

import time
import requests

TW = "https://api.tickwire.io"
KEY = "tw_live_..."
MASTER = "<MASTER_ACCOUNT_ID>"
FOLLOWERS = {"<FOLLOWER_ID>": {"multiplier": 0.5, "symbols": {"EURUSD": "EURUSDm"}}}

def call(method, acc, path, body=None, key=None, reason=None):
    h = {"auth-token": KEY}
    if key:
        h.update({"Idempotency-Key": key, "X-Actor": "copier", "X-Reason": reason})
    r = requests.request(method, f"{TW}/users/current/accounts/{acc}{path}",
                         headers=h, json=body, timeout=30)
    r.raise_for_status()
    return r.json()

def target(fid, cfg, src):
    symbol = cfg["symbols"].get(src["symbol"])
    if symbol is None:
        return None, 0.0                       # no mapping: never guess a symbol
    spec = call("GET", fid, f"/symbols/{symbol}/specification")
    step = spec["volumeStep"]
    vol = round(int(src["volume"] * cfg["multiplier"] / step + 1e-9) * step, 8)
    return symbol, (min(vol, spec["maxVolume"]) if vol >= spec["minVolume"] else 0.0)

def sync(fid, cfg, done):
    if any(call("GET", a, "/health")["status"] != "healthy" for a in (MASTER, fid)):
        return                                 # can't see one side: alert, don't act
    master = {p["id"]: p for p in call("GET", MASTER, "/positions")}
    copies = {p["clientId"][3:]: p for p in call("GET", fid, "/positions")
              if p.get("clientId", "").startswith("cp_")}

    for sid, src in master.items():
        symbol, want = target(fid, cfg, src)
        copy = copies.get(sid)
        if copy is None:
            if want and sid not in done:
                res = call("POST", fid, "/trade", key=f"open-{sid}-{fid}", reason=f"copy of {sid}", body={
                    "actionType": "ORDER_TYPE_BUY" if src["type"] == "POSITION_TYPE_BUY" else "ORDER_TYPE_SELL",
                    "symbol": symbol, "volume": want, "clientId": f"cp_{sid}"})
                if res.get("outcome") in ("applied", "rejected"):
                    done.add(sid)              # "unknown": next pass retries with the SAME key
            continue
        sl, tp = src.get("stopLoss"), src.get("takeProfit")
        if (sl or tp) and (sl, tp) != (copy.get("stopLoss"), copy.get("takeProfit")):
            call("POST", fid, "/trade", key=f"mod-{sid}-{fid}-{sl}-{tp}", reason=f"stops of {sid}",
                 body={"actionType": "POSITION_MODIFY", "positionId": copy["id"],
                       "stopLoss": sl, "takeProfit": tp})
        extra = round(copy["volume"] - want, 8)
        if want and extra > 0:
            call("POST", fid, "/trade", key=f"part-{sid}-{fid}-{src['volume']}",
                 reason=f"partial close of {sid}",
                 body={"actionType": "POSITION_PARTIAL", "positionId": copy["id"], "volume": extra})

    for sid, copy in copies.items():
        if sid not in master:
            call("POST", fid, "/trade", key=f"close-{sid}-{fid}", reason=f"master closed {sid}",
                 body={"actionType": "POSITION_CLOSE_ID", "positionId": copy["id"]})

done = set()                                   # persist this in a real copier
while True:
    for fid, cfg in FOLLOWERS.items():
        try:
            sync(fid, cfg, done)
        except requests.RequestException as exc:
            print(f"sync of {fid} failed, next pass retries: {exc}")
    time.sleep(2)

What makes it safe:

  • Every order key comes from the master id, the follower id and, for changes, the new values. A retry or a restarted worker sends the same key.
  • If an open comes back unknown, meaning it may or may not have executed, the next pass either finds the copy by clientId or resends with the same key. It never invents a new one.
  • The done set stops a copy that was closed by its own stop loss from being reopened later. Keep it in a database in real use; idempotency keys expire after a while.
  • If either account isn’t healthy, the loop does nothing that pass. An empty list from a disconnected master never closes every copy.

Things the sketch leaves out on purpose: scaling into a position, removing a stop, sizing by balance or equity, and a map from master id to copy id for when a broker rewrites comments. Add them before real use.

Checklist

  • Only copy into accounts whose owners have agreed to it, and check your prop firm’s rules on copy trading if a funded account is involved.
  • Sync from full position lists, triggered by events and by a timer.
  • Pause when either side’s health is not confirmed.
  • Round volumes down to the step, skip copies below the minimum, and map every symbol explicitly.
  • Derive idempotency keys and client ids from the master position id.
  • Treat unknown as “may have executed”: reconcile, never resend with a new key.
  • Decide in advance what a margin or market-closed rejection means.
  • Practise on a demo account before you risk money, including a forced disconnect mid-copy.

Doing this with Tickwire

Tickwire hosts a real MT4 or MT5 terminal for each account, with the same REST routes for both platforms, so one copier can read an MT4 master and write to MT5 followers. Every order route takes an Idempotency-Key; the same key returns the first result, and an order the terminal couldn’t confirm comes back as unknown rather than a guess. Health reports connected, disconnected or unknown with the evidence, which is what the pause rule above needs. Every copy is written to the audit log with its X-Actor and X-Reason.

The copier guide covers the stream and the batch order route for many followers at once, idempotency has the full key rules, and webhooks lists the position events you can sync on.

Trading carries risk, and you can lose money. This post explains software and how it behaves; it is not investment advice.

Try it on your own account

7-day free trial. See pricing or read the quickstart.