Trade Many MetaTrader 5 Accounts at Once with the Bulk API
Place, close, modify, cancel, and read across many MetaTrader 5 accounts in a single request. Here's how the TickerAll Bulk API fans one call out to your whole roster — with TypeScript and Python examples.
If you run more than one MetaTrader 5 account — a desk of funded accounts, a
handful of demos under test, the same strategy spread across brokers — doing
everything one account at a time gets old fast. Placing the same order on ten
accounts is ten round trips, ten awaits, and ten places for something to go
half-wrong. Reading them all for a dashboard is another ten.
The Bulk API collapses that into one call. You hand it a list, it fans the request out to every account you named, and it returns a per-account result plus a summary. Six verbs cover the surface: place, close positions, modify positions, cancel pending, modify pending, and read.
Bulk is a Pro / Enterprise feature. Every
/v1/bulk/*call requires a Pro or Enterprise plan; on Free and Trader it returns403. Read state for your whole roster or fan a trade out to all of it — same key, one request.
The one contract to internalize: partial success
A bulk op runs across many independent broker sessions, so it is not atomic.
There is no rollback of a real fill or a real close — if account C's broker
rejects the order after A and B already filled, you can't un-fill A and B. So the
Bulk API doesn't pretend: every method returns a results array with one
entry per account plus a summary, and a per-account failure is data
(status: 'failed' with a code / reason), never a thrown exception.
The rule that follows: inspect every result. A call that returns without throwing has not necessarily succeeded everywhere — it has told you, per account, what happened.
Install
npm install @tickerall/sdk # TypeScript / Node 18+
pip install tickerall # Python 3.9+
Both wrap the same REST endpoints, so if you'd rather hit the API directly the shapes below map one-to-one onto the JSON.
Place an order on many accounts
Each element is an ordinary place-order body plus the accountId it targets. An
account may appear at most once. type defaults to market; a limit / stop
order needs a price.
import { Tickerall } from '@tickerall/sdk'
const client = new Tickerall({ apiKey: process.env.TICKERALL_API_KEY! })
const res = await client.bulk.place([
{ accountId: 'acc_8Kd3f2', symbol: 'BTCUSD', side: 'BUY', volume: 0.10, stopLoss: 58000, takeProfit: 72000 },
{ accountId: 'acc_9Lm4Xs', symbol: 'BTCUSD', side: 'BUY', volume: 0.05 },
{ accountId: 'acc_2Rt7Bv', type: 'limit', symbol: 'BTCUSD', side: 'BUY', volume: 0.10, price: 60000 },
])
console.log(res.summary) // { total: 3, filled: 2, failed: 1 }
for (const r of res.results) {
if (r.status === 'filled') console.log(r.accountId, 'ticket', r.ticket, '@', r.price)
else console.log(r.accountId, 'FAILED', r.code, r.reason)
}
The same in Python — dict keys are accepted in snake_case or camelCase:
from tickerall import Tickerall
client = Tickerall(api_key="cf_live_...")
res = client.bulk.place([
{"account_id": "acc_8Kd3f2", "symbol": "BTCUSD", "side": "BUY", "volume": 0.10, "stop_loss": 58000, "take_profit": 72000},
{"account_id": "acc_9Lm4Xs", "symbol": "BTCUSD", "side": "BUY", "volume": 0.05},
{"account_id": "acc_2Rt7Bv", "type": "limit", "symbol": "BTCUSD", "side": "BUY", "volume": 0.10, "price": 60000},
])
print(res.summary.total, res.summary.filled, res.summary.failed) # 3 2 1
for r in res.results:
if r.status == "filled":
print(r.account_id, "ticket", r.ticket, "@", r.price)
else:
print(r.account_id, "FAILED", r.code, r.reason)
Each results entry carries accountId, status ('filled' or 'failed'),
and on a fill a ticket, price, and symbol; on a failure a code and
reason. The summary is { total, filled, failed }.
Close positions — explicit list, or per-account intent
Closing takes one of two shapes. Pass exactly one:
items— an explicit per-account list,{ accountId, ticket, volume? }. Omitvolumefor a full close; pass a smallervolumeto scale out.targets— a per-account intent,{ accountId, symbol?, side? }. Each target flattens every open position on that account matching its ownsymbol/side(all open positions when neither is given).
// Explicit: close these exact positions (the second is a partial scale-out)
await client.bulk.closePositions({
items: [
{ accountId: 'acc_8Kd3f2', ticket: 4072808150 },
{ accountId: 'acc_9Lm4Xs', ticket: 4072809002, volume: 0.05 },
],
})
// Intent: flatten BTC longs on A, everything on B
const res = await client.bulk.closePositions({
targets: [
{ accountId: 'acc_8Kd3f2', symbol: 'BTCUSD', side: 'BUY' },
{ accountId: 'acc_9Lm4Xs' },
],
})
// res.results[] -> { accountId, ticket, status: 'ok' | 'failed', closed, symbol, side, volume, ... }
# Explicit
client.bulk.close_positions(items=[
{"account_id": "acc_8Kd3f2", "ticket": 4072808150},
{"account_id": "acc_9Lm4Xs", "ticket": 4072809002, "volume": 0.05},
])
# Intent
res = client.bulk.close_positions(targets=[
{"account_id": "acc_8Kd3f2", "symbol": "BTCUSD", "side": "BUY"},
{"account_id": "acc_9Lm4Xs"},
])
The reason targets is per-account and not one global filter is mixed brokers.
Symbols aren't portable — the same instrument is BTCUSDm on one broker and
BTCUSD on another, EURUSDm vs EURUSD. Because each target carries its own
broker-native symbol, you can flatten a roster that spans brokers in one call:
await client.bulk.closePositions({
targets: [
{ accountId: 'acc_exness', symbol: 'EURUSDm' }, // Exness naming
{ accountId: 'acc_xm', symbol: 'EURUSD' }, // XM naming
],
})
Modify positions, cancel pending, modify pending
Modifies and pending cancels follow the same per-account-list pattern.
// Set / change SL / TP across accounts (at least one of stopLoss / takeProfit)
await client.bulk.modifyPositions([
{ accountId: 'acc_8Kd3f2', ticket: 4072808150, stopLoss: 60000, takeProfit: 85000 },
{ accountId: 'acc_9Lm4Xs', ticket: 4072809002, takeProfit: 84000 },
])
// Cancel resting pending orders — explicit tickets, or per-account intent
await client.bulk.cancelPending({
items: [
{ accountId: 'acc_8Kd3f2', ticket: 4072809988 },
{ accountId: 'acc_9Lm4Xs', ticket: 4072809991 },
],
})
await client.bulk.cancelPending({
targets: [{ accountId: 'acc_8Kd3f2', symbol: 'BTCUSD' }, { accountId: 'acc_9Lm4Xs' }],
})
// Change a pending order's trigger price / SL / TP
await client.bulk.modifyPending([
{ accountId: 'acc_8Kd3f2', ticket: 4072809988, price: 60000, stopLoss: 58000 },
])
client.bulk.modify_positions([
{"account_id": "acc_8Kd3f2", "ticket": 4072808150, "stop_loss": 60000, "take_profit": 85000},
{"account_id": "acc_9Lm4Xs", "ticket": 4072809002, "take_profit": 84000},
])
client.bulk.cancel_pending(items=[
{"account_id": "acc_8Kd3f2", "ticket": 4072809988},
{"account_id": "acc_9Lm4Xs", "ticket": 4072809991},
])
client.bulk.cancel_pending(targets=[
{"account_id": "acc_8Kd3f2", "symbol": "BTCUSD"},
{"account_id": "acc_9Lm4Xs"},
])
client.bulk.modify_pending([
{"account_id": "acc_8Kd3f2", "ticket": 4072809988, "price": 60000, "stop_loss": 58000},
])
Every write verb returns the same BulkWriteResponse: a results array of
{ accountId, ticket, status, ... } (with closed / cancelled / modified
set as the verb dictates) and a summary of { total, ok, failed }.
Read your whole roster in one call
readAccounts is the dashboard verb. Omit ids to read every account on your
key, or pass a subset; include trims the payload ('account' financials and/or
'positions'). It's a plain read — Pro-gated, but never demo-gated, and no
idempotency key.
const state = await client.bulk.readAccounts({
include: ['account', 'positions'], // omit `ids` for the whole roster
})
for (const a of state.accounts) {
console.log(a.id, a.status, a.hot, a.account ? (a.account as any).balance : '—')
}
console.log(state.summary) // { total, online, offline }
state = client.bulk.read_accounts(include=["account", "positions"])
for a in state.accounts:
bal = a.account.get("balance") if a.account else None
print(a.id, a.status, a.hot, bal)
print(state.summary.total, state.summary.online, state.summary.offline)
Each account comes back as { id, broker, server, accountNumber, isDemo, hot, status, ... }, plus account and positions when you asked for them and the
account is online. If you passed ids, any that aren't yours land in
not_found.
Retries are safe — one key for the whole fan-out
The write verbs carry a single Idempotency-Key that covers the entire batch, so
a retried bulk call is recognized as the same call and won't double-execute. If a
transient connectivity blip surfaces as a TickerallServiceUnavailableError, you
can retry the same request without fear of placing a second round of orders. (Pass
your own idempotencyKey in the options if you want to control the value.)
Before you fan a write out to real money
Bulk multiplies both convenience and blast radius — one call can touch every
account you have. Validate your fan-out logic on demo accounts first: confirm
the right accounts, the right symbols per broker, and the right sizes, and read
the results array back to see exactly what each account did. Once it's boring
and predictable, point it wherever you like.
Where to go next
Bulk covers "do the same thing to many accounts." If what you actually want is "whatever the master does, mirror it to the followers," that's Copy Trading — a master account and a set of scaled, risk-clamped followers. And if you're new to the SDK, the TypeScript quickstart walks a single account end to end.
Full API reference: tickerall.com/docs