Protocol Reference
This document describes the MT5 WebSocket binary protocol as reverse-engineered from the official MetaTrader 5 Web Terminal JavaScript client. All byte orders are little-endian unless stated otherwise.
Overview
The MT5 Web Terminal communicates with the trade server over a single WebSocket
connection (default wss://web.metatrader.app/terminal). Every WebSocket
message is a binary frame consisting of:
An 8-byte outer header.
An AES-CBC encrypted body containing a command frame.
The protocol is strictly request/response with the addition of server-initiated push notifications for real-time data (ticks, order book, trade updates, account changes).
Frame Format
Outer Frame
Every WebSocket message has a fixed 8-byte header followed by the encrypted body:
+-----------+-----------+---------------------------+
| Offset | Size | Description |
+-----------+-----------+---------------------------+
| 0 | 4 bytes | body_length (uint32 LE) |
| 4 | 4 bytes | version (uint32 LE = 1) |
| 8 | N bytes | encrypted body |
+-----------+-----------+---------------------------+
body_lengthis the number of bytes that follow the header (i.e.len(encrypted_body)).versionis always1(OUTER_PROTOCOL_VERSION).
The outer frame is constructed by pymt5.protocol.pack_outer() and parsed
by pymt5.protocol.unpack_outer().
Inner Command Frame
After decryption, the inner body has the following layout:
+-----------+-----------+---------------------------+
| Offset | Size | Description |
+-----------+-----------+---------------------------+
| 0 | 2 bytes | random prefix (uint16) |
| 2 | 2 bytes | command_id (uint16 LE) |
| 4 | N bytes | command payload |
+-----------+-----------+---------------------------+
Request frames (client to server):
Bytes 0-1 are random (generated by
os.urandom(2)), providing minimal replay resistance.Bytes 2-3 are the
command_ididentifying the operation.Bytes 4+ are the serialized command payload (may be empty).
Response frames (server to client):
Bytes 0-1 are echoed from the request (ignored by the parser).
Bytes 2-3 are the
command_id.Byte 4 is the
response_code(0 = success).Bytes 5+ are the response body.
Key Exchange
The connection begins with an unencrypted bootstrap phase. The initial
cipher uses a hard-coded obfuscated key (INITIAL_KEY_OBFUSCATED in
constants.py) decoded via a simple character-shift algorithm.
Bootstrap Flow (cmd=0)
Client connects to the WebSocket endpoint.
Client sends
CMD_BOOTSTRAP(cmd=0) with a 64-byte zero token as payload, encrypted with the initial (hard-coded) AES key.Server responds with:
code=0on success.Response body layout:
+--------+--------+---------------------------------+ | Offset | Size | Description | +--------+--------+---------------------------------+ | 0 | 2 | reserved | | 2 | 64 | session token | | 66 | 16/24 | AES session key (128 or 192bit) | +--------+--------+---------------------------------+
Client replaces the initial cipher with a new
AESCipherinitialized from the session key. All subsequent messages use this session key.
AES-CBC Encryption
All frames (after the outer header) are encrypted using AES-CBC:
Mode: CBC (Cipher Block Chaining).
IV: Always 16 zero bytes (
b"\x00" * 16).Padding: PKCS7 with a 128-bit block size.
Key size: 128-bit (16 bytes) or 192-bit (24 bytes), as provided by the server during bootstrap.
The pymt5.crypto.AESCipher class wraps the cryptography library
primitives.
Session Lifecycle
The following text diagram shows the typical connection state transitions:
DISCONNECTED
|
v
CONNECTING ---- WebSocket open ----> (send bootstrap cmd=0)
|
| (receive bootstrap response, extract session key)
v
READY ---- (optional) cmd=29 init_session ---->
|
| cmd=28 login
v
AUTHENTICATED ---- trading, data subscriptions ---->
|
| cmd=2 logout (or disconnect)
v
DISCONNECTED
Transport State Machine
The pymt5.transport.TransportState enum tracks these states:
DISCONNECTED– No active connection.CONNECTING– WebSocket open in progress and bootstrap handshake.READY– Bootstrap complete, session key exchanged, commands accepted.CLOSING– Graceful shutdown initiated.ERROR– Unexpected disconnection or protocol error.
On unexpected disconnect, if auto_reconnect is enabled, the client enters a
reconnect loop with exponential backoff and jitter:
min(base_delay * 2^(attempt-1) + random(0, base_delay), max_delay).
Command ID Catalog
The following table lists all command IDs defined in pymt5/constants.py.
Commands marked “push” are server-initiated notifications.
ID |
Constant |
Description |
|---|---|---|
0 |
|
Bootstrap handshake: exchange session token and AES key. |
2 |
|
End the authenticated session. |
3 |
|
Retrieve account info (balance, equity, margin, leverage). |
4 |
|
Retrieve open positions and pending orders. |
5 |
|
Retrieve historical deals. |
6 |
|
Retrieve basic symbol list (name, id, digits, path). |
7 |
|
Subscribe to real-time tick data for given symbol IDs. |
8 |
|
Push: Real-time tick data for subscribed symbols. |
9 |
|
Retrieve symbol group names. |
10 |
|
Push: Trade state change (balance update or order transaction). |
11 |
|
Retrieve OHLCV bars (klines) for a symbol and timeframe. |
12 |
|
Submit a trade request (market, pending, modify, close). |
13 |
|
Push: Symbol property change (spread, session, etc.). |
14 |
|
Push: Account balance/margin/equity change. |
15 |
|
Push: Login status change (forced logout, session expiry). |
17 |
|
Push: Extended symbol quote data (greeks, session stats). |
18 |
|
Retrieve full symbol specifications (contract details). |
19 |
|
Push: Asynchronous trade execution result. |
20 |
|
Retrieve spread data for symbols. |
22 |
|
Subscribe to order book (depth-of-market) for given symbol IDs. |
23 |
|
Push: Order book update for subscribed symbols. |
24 |
|
Change the account password. |
27 |
|
Verify an email/phone confirmation code. |
28 |
|
Authenticate with login, password, and optional OTP. |
29 |
|
Initialize session (pre-login handshake for cmd=28). |
30 |
|
Open a demo account. |
34 |
|
Retrieve symbols (gzip-compressed variant). |
39 |
|
Open a real (live) trading account. |
40 |
|
Request email/phone verification codes to be sent. |
41 |
|
Retrieve trader parameter strings. |
42 |
|
Retrieve server notifications. |
43 |
|
Set up or manage OTP (one-time password / 2FA). |
44 |
|
Retrieve corporate links (help, support URLs). |
51 |
|
Heartbeat / keep-alive ping. |
Field Type Encoding
The pymt5.protocol.SeriesCodec serializes and deserializes fields
according to the PROP_* type constants. All multi-byte integers and floats
use little-endian byte order.
Constant |
ID |
Size |
Endian |
Description |
|---|---|---|---|---|
|
1 |
1 byte |
N/A |
Signed 8-bit integer ( |
|
2 |
2 bytes |
LE |
Signed 16-bit integer ( |
|
3 |
4 bytes |
LE |
Signed 32-bit integer ( |
|
4 |
1 byte |
N/A |
Unsigned 8-bit integer ( |
|
5 |
2 bytes |
LE |
Unsigned 16-bit integer ( |
|
6 |
4 bytes |
LE |
Unsigned 32-bit integer ( |
|
7 |
4 bytes |
LE |
IEEE 754 single-precision float ( |
|
8 |
8 bytes |
LE |
IEEE 754 double-precision float ( |
|
9 |
8 bytes |
LE |
Windows FILETIME (100ns ticks since 1601-01-01). Converted to/from Unix milliseconds by the codec. |
|
10 |
N bytes |
N/A |
Fixed-length ASCII string ( |
|
11 |
N bytes |
LE |
Fixed-length UTF-16LE string ( |
|
12 |
N bytes |
N/A |
Raw byte buffer ( |
|
17 |
8 bytes |
LE |
Signed 64-bit integer ( |
|
18 |
8 bytes |
LE |
Unsigned 64-bit integer ( |
Tick Push Notifications (cmd=8)
When symbols are subscribed via CMD_SUBSCRIBE_TICKS (cmd=7), the server
streams tick updates as CMD_TICK_PUSH (cmd=8) push notifications.
Tick Body Format
The response body contains a 4-byte count followed by repeated tick records:
+--------+--------+---------------------------+
| Offset | Size | Description |
+--------+--------+---------------------------+
| 0 | 4 | tick_count (uint32 LE) |
| 4 | 42 * N | tick records |
+--------+--------+---------------------------+
Each tick record (42 bytes) follows the TICK_SCHEMA:
# |
Field |
Type |
Description |
|---|---|---|---|
0 |
symbol_id |
U32 |
Symbol identifier. |
1 |
tick_time |
I32 |
Tick time in Unix seconds. |
2 |
fields |
U32 |
Bitmask indicating which fields are populated. |
3 |
bid |
F64 |
Best bid price. |
4 |
ask |
F64 |
Best ask price. |
5 |
last |
F64 |
Last trade price. |
6 |
tick_volume |
I64 |
Tick volume. |
7 |
time_ms_delta |
U32 |
Millisecond offset from |
8 |
flags |
U16 |
Tick flags (bid changed, ask changed, etc.). |
Book Push Notifications (cmd=23)
When symbols are subscribed via CMD_SUBSCRIBE_BOOK (cmd=22), the server
streams order book updates as CMD_BOOK_PUSH (cmd=23) push notifications.
Book Body Format
The response body contains one or more symbol book entries, each consisting of a header followed by bid and ask level arrays:
+--------+--------+------------------------------------------+
| Offset | Size | Description |
+--------+--------+------------------------------------------+
| 0 | 22 | Book header (BOOK_HEADER_SCHEMA) |
| 22 | 16 * B | Bid levels (B = bid_count from header) |
| ... | 16 * A | Ask levels (A = ask_count from header) |
+--------+--------+------------------------------------------+
Book Header (22 bytes):
# |
Field |
Type |
Description |
|---|---|---|---|
0 |
symbol_id |
U32 |
Symbol identifier. |
1 |
field1 |
I32 |
Reserved field. |
2 |
field2 |
I32 |
Reserved field. |
3 |
bid_count |
U32 |
Number of bid levels following. |
4 |
ask_count |
U32 |
Number of ask levels following. |
5 |
flags |
U16 |
Book update flags. |
Book Level (16 bytes each):
# |
Field |
Type |
Description |
|---|---|---|---|
0 |
price |
F64 |
Price level. |
1 |
volume |
I64 |
Volume at this price level. |
Error Codes
Trade requests (cmd=12) return a retcode in the response indicating success
or failure. The following table lists all TRADE_RETCODE_* constants:
Code |
Constant |
Description |
|---|---|---|
10004 |
|
Requote. |
10006 |
|
Request rejected. |
10007 |
|
Request cancelled by trader. |
10008 |
|
Order placed. |
10009 |
|
Request completed (done). |
10010 |
|
Request partially completed. |
10011 |
|
Request processing error. |
10012 |
|
Request cancelled by timeout. |
10013 |
|
Invalid request. |
10014 |
|
Invalid volume. |
10015 |
|
Invalid price. |
10016 |
|
Invalid stops. |
10017 |
|
Trade disabled. |
10018 |
|
Market closed. |
10019 |
|
Not enough money. |
10020 |
|
Price changed. |
10021 |
|
No quotes for request processing. |
10022 |
|
Invalid order expiration. |
10023 |
|
Order state changed. |
10024 |
|
Too many requests. |
10025 |
|
No changes in request. |
10026 |
|
Auto-trading disabled by server. |
10027 |
|
Auto-trading disabled by client. |
10028 |
|
Request locked for processing. |
10029 |
|
Order/position frozen. |
10030 |
|
Invalid order filling type. |
10031 |
|
No connection with trade server. |
10032 |
|
Operation allowed only for live accounts. |
10033 |
|
Pending orders limit reached. |
10034 |
|
Volume limit for symbol reached. |
10035 |
|
Invalid or prohibited order type. |
10036 |
|
Position already closed. |
Volume Encoding
MT5 uses integer volume encoding: the wire value is volume * 10000. For
example, 0.01 lots is transmitted as 100, and 1.0 lots as 10000. The
pymt5 client handles this conversion transparently in trading helper methods.
Rate Bar Format (cmd=11)
Historical OHLCV bars returned by CMD_GET_RATES (cmd=11) have a 48-byte
per-bar layout:
# |
Field |
Type |
Description |
|---|---|---|---|
0 |
time |
I32 |
Bar open time (Unix seconds). |
1 |
open |
F64 |
Open price. |
2 |
high |
F64 |
High price. |
3 |
low |
F64 |
Low price. |
4 |
close |
F64 |
Close price. |
5 |
tick_volume |
I64 |
Tick volume during the bar period. |
6 |
spread |
I32 |
Spread at bar close. |