Your copier worked perfectly on the demo. You point it at a live account on a
different broker and every trade fails, because the master trades EURUSD and
the follower's broker only knows EURUSD.pro. You fix the name, and the next
gold trade comes out ten times too big, because that broker's contract is 100
ounces and your code assumed 1,000.
That is the whole problem in two sentences. The MT5 symbol suffix is the visible half. The invisible half is everything else in the symbol spec the broker changed while they were renaming it.
Why brokers rename the same instrument
An MT5 broker doesn't offer "EURUSD". It offers a symbol it configured on its own server, with a name it chose, and the name usually encodes which account group or liquidity feed the symbol belongs to.
| What you see | What it usually means |
|---|---|
EURUSD.pro, EURUSD.raw, EURUSD.ecn | Raw-spread or commission-based account tier |
EURUSDm, EURUSDc, EURUSD_i | Micro, cent, or instant-execution groups |
EURUSD-STD, EURUSD.std | Standard account: wider spread, no commission |
#EURUSD, .EURUSD | A prefix instead, often a different liquidity provider |
XAUUSD, GOLD, XAUUSDm, GOLD.pro | Gold. Every one is a separate symbol |
The suffix is not cosmetic. The same broker often runs EURUSD and
EURUSD.raw side by side with different spreads, different commission, and
occasionally different trading hours. A trade on one is not a trade on the
other. Indices are the worst: US30, DJ30, DOW, US30.cash, and WS30
are all the same thing on five different brokers.
There is no registry. Nobody mandates that gold is called XAUUSD. The name is
whatever the broker typed into their server.
Naming is the easy part
If naming were the only difference, a lookup table would fix it in an afternoon. The reason MetaTrader symbol names on different brokers cause real bugs is that the rest of the spec moves with them.
Digits. Most brokers now quote five decimals on majors (1.08453) and
three on yen pairs (157.203). A few still quote four and two. Gold is two
digits on some brokers (2031.45) and three on others (2031.450). Digits
decide what a point is, and therefore what a pip is.
Contract size. Forex is 100,000 units per lot almost everywhere. Gold is
usually 100 troy ounces per lot, but 10 and 1,000 both exist. Silver is
commonly 5,000 ounces. Indices and share CFDs are often 1 unit per lot,
sometimes 10. Crypto CFDs are anyone's guess. One lot of XAUUSD and one lot
of GOLD can be different amounts of gold.
Min lot and lot step. 0.01 minimum in 0.01 steps is standard on
forex. Some index CFDs start at 0.1 or 1.0. Some brokers set the step to
0.1 on exotics. A volume of 0.15 is valid on one broker and rejected on the
other.
Trading hours. Gold pauses for around an hour a day on most brokers, but not the same hour. Index CFDs have session breaks that vary broker to broker. A market order sent into a closed session is just a rejection.
Stop level. The minimum distance a stop or limit must sit from the current price. Zero on many ECN feeds, dozens of points elsewhere. A 3-pip stop is fine on one broker and "invalid stops" on the next.
How each difference breaks code
Same bug, four costumes.
String equality on the symbol. if deal["symbol"] == "EURUSD" silently
matches nothing on a .pro broker. Your dashboard shows zero EURUSD trades,
your filter drops every signal, your copier copies nothing. The fix is not
startswith, either: EURUSD starts-with matches EURUSDm but not
.EURUSD, and it also matches EURUSDT if the broker lists crypto. You need a
normalisation step, not a cleverer comparison.
Pip maths on the wrong digits. Code that hardcodes pip = 0.0001
computes a 25-pip stop as 0.0025 on EURUSD. Correct. The same code on
USDJPY puts the stop 25 yen away instead of 25 pips. Also "correct", just
catastrophically wrong. And gold at two digits: is one pip 0.01 or 0.10?
People disagree, which is a good reason to compute in points and ticks rather
than pips at all. The lot size and pip value post
does exactly that.
Lot sizing on the wrong contract size. Risk-based sizing divides your risk amount by stop distance times value per point per lot. Value per point depends on contract size. Assume 100 ounces when the broker uses 1,000 and every gold position is ten times what you intended. This is the one that ends accounts.
Rounding to the wrong lot step. You compute 0.375 lots. With a 0.01
step that rounds down to 0.37. With a 0.1 step it has to become 0.3.
Round to the wrong grid and every order comes back "invalid volume", and
because the error is on the broker side, your logs look fine.
Read the spec from the broker, every time
The fix for all of it is the same rule: never assume anything about a symbol. Ask the account's own broker for the spec and compute from that.
Over MetaKit, every connected account exposes its broker's symbol list at
GET /v1/accounts/{id}/symbols and the full spec for one symbol at
GET /v1/accounts/{id}/symbols/{symbol}. The spec carries digits,
contract_size, tick_size, tick_value, min_lot, max_lot, lot_step,
stop_level, base_currency, profit_currency, and live bid, ask, and
spread.
import os
import requests
BASE = "https://api.metakit.cloud"
HEADERS = {"Authorization": f"Bearer {os.environ['METAKIT_KEY']}"}
def symbol_spec(account_id: int, symbol: str) -> dict:
res = requests.get(
f"{BASE}/v1/accounts/{account_id}/symbols/{symbol}", headers=HEADERS
)
res.raise_for_status()
return res.json()
for account_id, name in [(2, "XAUUSD"), (5, "GOLD.pro")]:
s = symbol_spec(account_id, name)
print(
f"{name:10} digits={s['digits']} contract={s['contract_size']} "
f"min={s['min_lot']} step={s['lot_step']} tick={s['tick_size']}"
)Run that against two brokers and the numbers will disagree in at least one column. That is the moment the hardcoded constants leave your codebase.
Note the account is in the path. A symbol spec belongs to a broker, and a broker is reached through an account, so there is no global "what is XAUUSD" endpoint. There can't be one; the answer depends on who you ask.
Normalise with a mapping table
Once specs are read per account, the naming problem reduces to a table: a canonical name on your side, the broker's name on the other.
# canonical -> broker symbol, per account
SYMBOL_MAP = {
2: {"EURUSD": "EURUSD", "XAUUSD": "XAUUSD", "US30": "US30"},
5: {"EURUSD": "EURUSD.pro", "XAUUSD": "GOLD.pro", "US30": "DJ30.pro"},
}
def broker_symbol(account_id: int, canonical: str) -> str:
return SYMBOL_MAP[account_id][canonical]
def canonical_symbol(account_id: int, broker: str) -> str:
reverse = {v: k for k, v in SYMBOL_MAP[account_id].items()}
return reverse.get(broker, broker) # unknown symbols pass throughBuild the table by pulling GET /v1/accounts/{id}/symbols once per account
and matching. You can semi-automate it: strip a known list of suffixes and
prefixes, then confirm using base_currency and profit_currency (a symbol
with base XAU and profit USD is gold whatever it's called). Do not trust
the automatic match for indices. Check those by eye.
Then store deals and positions under the canonical name and translate only at
the edges. Everything in the middle of your system says XAUUSD and nothing
else.
How a copier's symbol map does this for you
A trade copier is this problem in its purest form: a master on broker A, a follower on broker B, and every trade has to cross the naming gap in under a second.
A MetaKit copier carries a symbol_map for exactly this. The master trades
EURUSD, the follower's broker wants EURUSD.pro, the map says so, and the
copier translates on every open, modify, and close. You can set it when you
create the copier or update it in bulk:
curl -X PUT https://api.metakit.cloud/v1/copiers/4/symbol-map \
-H "Authorization: Bearer $METAKIT_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "merge",
"map": { "EURUSD": "EURUSD.pro", "XAUUSD": "GOLD.pro", "US30": "DJ30.pro" }
}'mode: "merge" upserts the entries you send (an empty string removes one);
mode: "replace" swaps the whole map. Symbols with no entry pass through under
the master's name. If the follower's broker doesn't have that name, the open
fails and you get a copier.error event, which is the right outcome. Guessing
is how you end up long ten lots of the wrong index.
Sizing runs against the follower's own spec. The multiplier and
proportional lot modes scale the master's volume, the result is snapped to
the follower's lot step before it's sent, and max_lot_per_trade gives you a
hard ceiling on top. The contract-size mismatch is still yours to think about
though: a multiplier of 1.0 copies one lot of 100-ounce gold as one lot of
1,000-ounce gold. If the two brokers disagree on contract size, the multiplier
has to carry the ratio.
The copier setup post walks through building one end to end, and every copier field is listed in the API reference.
The one-line rule
If a number describing a symbol is typed into your source code, it is wrong on some broker you haven't met yet. Read it from the account instead.