MT5 symbol suffixes: why EURUSD.pro breaks your code

8 min readMetaKit

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 seeWhat it usually means
EURUSD.pro, EURUSD.raw, EURUSD.ecnRaw-spread or commission-based account tier
EURUSDm, EURUSDc, EURUSD_iMicro, cent, or instant-execution groups
EURUSD-STD, EURUSD.stdStandard account: wider spread, no commission
#EURUSD, .EURUSDA prefix instead, often a different liquidity provider
XAUUSD, GOLD, XAUUSDm, GOLD.proGold. 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 through

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