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: 1. An 8-byte outer header. 2. 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_length`` is the number of bytes that follow the header (i.e. ``len(encrypted_body)``). - ``version`` is always ``1`` (``OUTER_PROTOCOL_VERSION``). The outer frame is constructed by :func:`pymt5.protocol.pack_outer` and parsed by :func:`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_id`` identifying 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) ~~~~~~~~~~~~~~~~~~~~~~ 1. Client connects to the WebSocket endpoint. 2. Client sends ``CMD_BOOTSTRAP`` (cmd=0) with a 64-byte zero token as payload, encrypted with the initial (hard-coded) AES key. 3. Server responds with: - ``code=0`` on success. - Response body layout:: +--------+--------+---------------------------------+ | Offset | Size | Description | +--------+--------+---------------------------------+ | 0 | 2 | reserved | | 2 | 64 | session token | | 66 | 16/24 | AES session key (128 or 192bit) | +--------+--------+---------------------------------+ 4. Client replaces the initial cipher with a new ``AESCipher`` initialized 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 :class:`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 :class:`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. .. list-table:: Command IDs :header-rows: 1 :widths: 10 30 60 * - ID - Constant - Description * - 0 - ``CMD_BOOTSTRAP`` - Bootstrap handshake: exchange session token and AES key. * - 2 - ``CMD_LOGOUT`` - End the authenticated session. * - 3 - ``CMD_GET_ACCOUNT`` - Retrieve account info (balance, equity, margin, leverage). * - 4 - ``CMD_GET_POSITIONS_ORDERS`` - Retrieve open positions and pending orders. * - 5 - ``CMD_GET_TRADE_HISTORY`` - Retrieve historical deals. * - 6 - ``CMD_GET_SYMBOLS`` - Retrieve basic symbol list (name, id, digits, path). * - 7 - ``CMD_SUBSCRIBE_TICKS`` - Subscribe to real-time tick data for given symbol IDs. * - 8 - ``CMD_TICK_PUSH`` - **Push**: Real-time tick data for subscribed symbols. * - 9 - ``CMD_GET_SYMBOL_GROUPS`` - Retrieve symbol group names. * - 10 - ``CMD_TRADE_UPDATE_PUSH`` - **Push**: Trade state change (balance update or order transaction). * - 11 - ``CMD_GET_RATES`` - Retrieve OHLCV bars (klines) for a symbol and timeframe. * - 12 - ``CMD_TRADE_REQUEST`` - Submit a trade request (market, pending, modify, close). * - 13 - ``CMD_SYMBOL_UPDATE_PUSH`` - **Push**: Symbol property change (spread, session, etc.). * - 14 - ``CMD_ACCOUNT_UPDATE_PUSH`` - **Push**: Account balance/margin/equity change. * - 15 - ``CMD_LOGIN_STATUS_PUSH`` - **Push**: Login status change (forced logout, session expiry). * - 17 - ``CMD_SYMBOL_DETAILS_PUSH`` - **Push**: Extended symbol quote data (greeks, session stats). * - 18 - ``CMD_GET_FULL_SYMBOLS`` - Retrieve full symbol specifications (contract details). * - 19 - ``CMD_TRADE_RESULT_PUSH`` - **Push**: Asynchronous trade execution result. * - 20 - ``CMD_GET_SPREADS`` - Retrieve spread data for symbols. * - 22 - ``CMD_SUBSCRIBE_BOOK`` - Subscribe to order book (depth-of-market) for given symbol IDs. * - 23 - ``CMD_BOOK_PUSH`` - **Push**: Order book update for subscribed symbols. * - 24 - ``CMD_CHANGE_PASSWORD`` - Change the account password. * - 27 - ``CMD_VERIFY_CODE`` - Verify an email/phone confirmation code. * - 28 - ``CMD_LOGIN`` - Authenticate with login, password, and optional OTP. * - 29 - ``CMD_INIT`` - Initialize session (pre-login handshake for cmd=28). * - 30 - ``CMD_OPEN_DEMO`` - Open a demo account. * - 34 - ``CMD_GET_SYMBOLS_GZIP`` - Retrieve symbols (gzip-compressed variant). * - 39 - ``CMD_OPEN_REAL`` - Open a real (live) trading account. * - 40 - ``CMD_SEND_VERIFY_CODES`` - Request email/phone verification codes to be sent. * - 41 - ``CMD_TRADER_PARAMS`` - Retrieve trader parameter strings. * - 42 - ``CMD_NOTIFY`` - Retrieve server notifications. * - 43 - ``CMD_OTP_SETUP`` - Set up or manage OTP (one-time password / 2FA). * - 44 - ``CMD_GET_CORPORATE_LINKS`` - Retrieve corporate links (help, support URLs). * - 51 - ``CMD_PING`` - Heartbeat / keep-alive ping. Field Type Encoding ------------------- The :class:`pymt5.protocol.SeriesCodec` serializes and deserializes fields according to the ``PROP_*`` type constants. All multi-byte integers and floats use **little-endian** byte order. .. list-table:: Field Types :header-rows: 1 :widths: 15 10 10 10 55 * - Constant - ID - Size - Endian - Description * - ``PROP_I8`` - 1 - 1 byte - N/A - Signed 8-bit integer (``struct "