# 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.

Source: https://tickwire.io/blog/how-trade-copiers-work/

Tickwire blog, published 2026-10-20. General information, not investment advice.

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](https://tickwire.io/blog/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:

| Mode | Follower volume | Good for |
|---|---|---|
| Fixed | Always the same, e.g. 0.10 lots | Simple, but ignores the master's sizing |
| Multiplier | Master volume times a factor, e.g. 0.5 | Followers that are a fixed fraction of the master |
| By balance | Master volume times follower balance divided by master balance | Accounts of different sizes |
| By equity | The 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](https://tickwire.io/blog/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](https://tickwire.io/blog/netting-vs-hedging-mt5/) 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](https://tickwire.io/blog/slippage-requotes-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](https://tickwire.io/blog/retries-never-trade-twice/).

Here is a short Python sketch using `requests` and the routes from the
[copier guide](https://tickwire.io/docs/guides/copier/). It copies opens, stop changes, partial closes and closes for a
multiplier mode.

```python
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](https://tickwire.io/docs/guides/copier/) covers the stream and the batch order route for many
followers at once, [idempotency](https://tickwire.io/docs/idempotency/) has the full key rules, and
[webhooks](https://tickwire.io/docs/webhooks/) lists the position events you can sync on.
