Metadata-Version: 2.4
Name: mt5-web-client
Version: 1.0.4
Summary: Python client for MT5 Web Terminal via reverse-engineered WebSocket binary protocol
Author: cloudQuant
License-Expression: MIT
Project-URL: Homepage, https://github.com/cloudQuant/pymt5
Project-URL: Documentation, https://cloudquant.github.io/pymt5/
Project-URL: Repository, https://github.com/cloudQuant/pymt5
Project-URL: Issues, https://github.com/cloudQuant/pymt5/issues
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: websockets<16,>=13.0
Requires-Dist: cryptography<45,>=42.0.0
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: hypothesis>=6.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: ruff>=0.1; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Provides-Extra: docs
Requires-Dist: sphinx>=7.0; extra == "docs"
Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
Requires-Dist: sphinx-autodoc-typehints>=2.0; extra == "docs"
Provides-Extra: structlog
Requires-Dist: structlog>=24.0; extra == "structlog"
Provides-Extra: pandas
Requires-Dist: pandas>=2.0; extra == "pandas"
Dynamic: license-file

# pymt5

[![CI](https://github.com/cloudQuant/pymt5/actions/workflows/ci.yml/badge.svg)](https://github.com/cloudQuant/pymt5/actions/workflows/ci.yml)
[![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)](https://www.python.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
[![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://cloudquant.github.io/pymt5/)
[![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey)](https://github.com/cloudQuant/pymt5)

Python client for the MT5 Web Terminal via reverse-engineered WebSocket binary protocol.

See the [changelog](docs/changelog.rst) for releases and subsequent changes.

## What's new in 1.0.4

- Trade requests retain the legacy wire format by default. An explicit
  `build_6090` profile adds the reported reserved bytes and market-deal action.
- Ambiguous or mismatched historical rate response lengths raise `ProtocolError`;
  choose `standard` or `extended` explicitly when the server's format is known.
- Login rejects nonzero server response codes, and logout clears stored reconnect
  credentials after success. Switching accounts requires an explicit logout.
- `close_position()` rejects invalid IDs and refuses automatic direction detection
  when the position or its matching symbol cannot be verified.

These changes have offline test coverage. The new build 6090 profile has not been
independently verified against a broker in this release.

## Requirements

- **Python 3.11+** (3.11, 3.12, 3.13)
- **Platforms**: Linux, macOS, Windows
- **Dependencies**: `websockets`, `cryptography`

## Features

### Core Protocol
- WebSocket transport to `wss://web.metatrader.app/terminal`
- AES-CBC encryption with zero IV and PKCS7 padding
- Bootstrap handshake (cmd=0) with automatic key exchange
- Login (cmd=28) with UTF-16LE encoded fields
- `ping`, `logout`, `change_password`, `trader_params`
- `verify_code` (cmd=27) — two-factor authentication
- `request_opening_verification` / `submit_opening_verification` (cmd=27/40)
- `open_demo_account` (cmd=30) and `open_real_account` (cmd=39)
- `enable_otp` / `disable_otp` (cmd=43)
- `open_demo` (cmd=30) — legacy init-style wrapper kept for compatibility
- `send_notification` (cmd=42) — server notifications

### Account Data
- **`get_account` (cmd=3)** — account metadata and balance state: balance, credit, currency, leverage, account/server/company names, timezone, rights flags, trade settings, leverage rules, commission tables
- `get_account_summary()` — `AccountInfo` dataclass (uses cmd=3 + positions)
- `get_positions_and_orders` (cmd=4) — open positions and pending orders
- `get_positions()` — convenience: open positions only
- `get_orders()` — convenience: pending orders only
- `get_trade_history` (cmd=5) — closed deals and historical orders
- `get_deals()` — convenience: closed deals only

### Market Data
- `get_symbols` (cmd=34, zlib compressed) — full symbol list with metadata
- **`get_symbol_groups` (cmd=9)** — category tree (Forex, Crypto, Indexes, etc.)
- `get_full_symbol_info` (cmd=18) — detailed contract specs (tick size, margins, volumes, bond face value/accrued interest, trade schedule, subscription flags)
- **`get_spreads` (cmd=20)** — spread data with proper schema parsing
- `get_rates` (cmd=11) — historical OHLCV bars (M1 to MN1), with standard and extended formats
- `subscribe_ticks` (cmd=7) — real-time tick push (cmd=8) with callback
- `subscribe_symbols` — name-based tick subscription (resolves names via symbol cache)
- **`subscribe_book` (cmd=22)** — order book / depth-of-market subscription
- **`subscribe_book_by_name`** — name-based order book subscription

By default, rate responses are decoded automatically only when their length uniquely
identifies the 48-byte standard or 56-byte extended format. Lengths divisible by both,
including any nonzero multiple of 336 bytes, or by neither, raise `ProtocolError` rather
than guessing. This is a behavior
change for ambiguous or mismatched responses. Select the format confirmed for your
server with `MT5WebClient(rate_bar_format="standard")` or `"extended"`; alternatively,
pass `bar_format="standard"` or `"extended"` to `get_rates()` or the `copy_rates_*()`
wrappers. The extended format includes `real_volume`. `get_rates_raw()` can help inspect
an unknown response, but length alone does not identify an ambiguous format. Both
`get_rates()` and `get_rates_raw()` raise `ProtocolError` for nonzero server response codes.
Length validation cannot detect truncation or an incorrect format selection when the
resulting size happens to match the selected record size.

### Official MetaTrader5-style Helpers
- Session/account: `initialize`, `shutdown`, `account_info`, `terminal_info`, `version`
- Compatibility state: `last_error`
- Symbols/history: `symbols_total`, `symbols_get`, `symbol_info`, `positions_total`, `positions_get`, `orders_total`, `orders_get`, `history_orders_*`, `history_deals_*`
- Bars/trading: `copy_rates_range`, `copy_rates_from`, `copy_rates_from_pos`, `copy_ticks_from`, `copy_ticks_range`, `order_send`, `order_check`
- Local formula helpers: `order_calc_profit`, `order_calc_margin` (best-effort for forex/CFD/futures/stock/bond modes that the current Web Terminal computes client-side)
- DOM/cache helpers: `market_book_add`, `market_book_get`, `market_book_release`, `symbol_select`, `symbol_info_tick`
- `terminal_info` and `version` are best-effort Web Terminal views derived from `cmd=3` plus observed public build metadata
- `copy_ticks_*` are cached views over observed `cmd=8` pushes, and `order_check` is a local compatibility-layer pre-flight rather than a server RPC

### Trading
- **Raw**: `trade_request` — full control over all trade fields, returns `TradeResult`
- **`TradeResult`** includes: `deal`, `order`, `volume`, `price`, `bid`, `ask`, `comment`, `request_id`
- **High-level helpers** (auto-resolve digits from symbol cache):
  - `buy_market` / `sell_market` — market orders with optional SL/TP
  - `buy_limit` / `sell_limit` — limit pending orders
  - `buy_stop` / `sell_stop` — stop pending orders
  - `buy_stop_limit` / `sell_stop_limit` — stop-limit pending orders
  - `close_position` — close by opposite market order (auto-detects BUY/SELL direction)
  - `close_position_by` — close by opposite position (hedge netting)
  - `modify_position_sltp` — modify SL/TP of open position
  - `modify_pending_order` — modify pending order price/SL/TP
  - `cancel_pending_order` — remove a pending order

### Symbol Cache & Info
- `load_symbols()` — fetch and cache all symbols for fast name→id/digits lookup
- `get_symbol_info(name)` — lookup `SymbolInfo` by name
- `get_symbol_id(name)` — lookup symbol ID by name
- `symbol_names` — list all cached symbol names

### Miscellaneous Commands
- **`get_corporate_links` (cmd=44)** — broker links (support, education, social)
- `send_raw_command` — send any command with raw payload
- `send_bootstrap_command_52` — experimental wrapper for reserved bootstrap-only `cmd=52`

### Push Notifications
- `on_tick(callback)` — real-time tick pushes (cmd=8)
- `on_position_update(callback)` — position change pushes
- `on_order_update(callback)` — order change pushes
- `on_trade_update(callback)` — combined position+order push in single callback
- **`on_trade_transaction(callback)`** — order add/update/delete + balance updates (cmd=10)
- `on_symbol_update(callback)` — symbol change pushes (cmd=13)
- **`on_account_update(callback)`** — account balance/equity/margin pushes (cmd=14)
- `on_login_status(callback)` — login status pushes (cmd=15)
- **`on_symbol_details(callback)`** — extended quote data with options greeks (cmd=17)
- **`on_trade_result(callback)`** — async trade execution results (cmd=19)
- **`on_book_update(callback)`** — order book / DOM pushes (cmd=23)

### Reliability
- **Async context manager** (`async with MT5WebClient() as client:`)
- **Auto heartbeat** — periodic ping after login (configurable interval)
- **Auto reconnect** — optional reconnect on disconnect with exponential backoff
- **Disconnect callback** — `on_disconnect()` for custom handling
- **Python logging** — structured logging via `pymt5.client` and `pymt5.transport` loggers

### Constants & Enums
- `TradeResult` dataclass with `retcode`, `description`, `success`, `deal`, `order`, `volume`, `price` fields
- 33 MT5 return codes with human-readable descriptions
- Complete trade constants: `TRADE_ACTION_*` (including `CLOSE_BY`), `ORDER_TYPE_*` (including `STOP_LIMIT`), `ORDER_FILLING_*`, `ORDER_TIME_*`
- Position/deal enums: `POSITION_TYPE_*`, `DEAL_TYPE_*` (all 15 types), `DEAL_ENTRY_*`
- Order states: `ORDER_STATE_*` (all 7 states)
- Symbol trade modes: `SYMBOL_TRADE_MODE_*`
- Command IDs exported: `CMD_GET_ACCOUNT`, `CMD_GET_SYMBOL_GROUPS`, `CMD_TRADE_UPDATE_PUSH`, `CMD_ACCOUNT_UPDATE_PUSH`, `CMD_SYMBOL_DETAILS_PUSH`, `CMD_TRADE_RESULT_PUSH`, `CMD_SUBSCRIBE_BOOK`, `CMD_BOOK_PUSH`, `CMD_GET_CORPORATE_LINKS`

### Tests
- Offline unit tests cover protocol encoding, trade and rate parsing, session handling,
  reconnect logic, compatibility helpers, and push handlers.

Earlier functionality was tested against MetaQuotes-Demo on 2026-03-16 with Web Terminal
build 5687. Those historical results do not verify the changes in 1.0.4.

## Install

```bash
pip install mt5-web-client
```

Or install from source:

```bash
git clone https://github.com/cloudQuant/pymt5.git
cd pymt5
pip install -e .
```

For development (tests + type checking):

```bash
pip install -e ".[dev]"
make check
```

## Quick Start

```python
import asyncio
from pymt5 import MT5WebClient


async def main():
    async with MT5WebClient(auto_reconnect=True) as client:
        await client.login(login=12345678, password="your-password")

        # Load symbol cache
        await client.load_symbols()
        print(f"Loaded {len(client.symbol_names)} symbols")

        # Full account info (balance, equity, margin, leverage)
        acct = await client.get_account()
        print(f"Balance: {acct['balance']}, Currency: {acct['currency']}, Leverage: 1:{acct['leverage']}")

        # Symbol groups
        groups = await client.get_symbol_groups()
        print(f"Groups: {groups}")

        # Subscribe to ticks
        def on_ticks(ticks):
            for t in ticks:
                print(f"TICK {t.get('symbol', t['symbol_id'])}: bid={t['bid']} ask={t['ask']}")

        client.on_tick(on_ticks)
        await client.subscribe_symbols(["EURUSD", "GBPUSD"])

        # Register for all push notifications
        client.on_trade_update(lambda d: print(f"Positions: {len(d['positions'])}"))
        client.on_account_update(lambda d: print(f"Balance: {d.get('balance')}"))
        client.on_trade_transaction(lambda d: print(f"Transaction: type={d.get('update_type')}"))
        client.on_trade_result(lambda d: print(f"Trade result: {d.get('result', {})}"))

        # Place a market buy
        result = await client.buy_market("EURUSD", 0.01, sl=1.0800, tp=1.1200)
        print(f"Trade: {result}")

        await asyncio.sleep(10)


asyncio.run(main())
```

Calling `login()` while already logged in raises `SessionError`; call `logout()`
before switching accounts. A successful `logout()` clears stored reconnect
credentials. A rejected logout raises `SessionError` and retains the current
session state and credentials.

## Trading Examples

### Trade request wire format

`MT5WebClient()` uses the legacy trade request format by default. For the build 6090
market-execution environment reported in [PR #1](https://github.com/cloudQuant/pymt5/pull/1),
opt in explicitly:

```python
client = MT5WebClient(trade_protocol="build_6090")
```

This profile adds the observed reserved bytes and uses action `TRADE_ACTION_MARKET_DEAL` (3)
for `buy_market`, `sell_market`, and `close_position`. The report covers selected symbols
on one demo server; compatibility with other brokers, execution modes, and builds has not
been established. The client does not select a profile from the handshake build number.

`trade_request()` and `order_send()` keep the action and fields supplied by the caller.
For example, callers using `order_send()` for a market-execution deal with this profile
must explicitly set `action` to `TRADE_ACTION_MARKET_DEAL`. The profile does not set
`action_id` or `type_flags` automatically; their requirements may depend on the server.
The PR's observed official-client requests used an `action_id` starting at 1000 and
`type_flags=2`; whether these values are required has not been established.
The `buy_market()`, `sell_market()`, and `close_position()` helpers accept optional
`action_id` and `type_flags` keywords when a server requires them; both default to zero.
Trade requests and acknowledgements do not establish that an order was filled; inspect
subsequent trade results and positions.

`close_position()` rejects non-positive position IDs. If `order_type` is omitted, it
looks up the position and raises `ValidationError` without placing a trade if the
position, its direction, or the position query is unavailable, or if the position's
symbol is missing or differs from the requested symbol. Pass an explicit
`order_type` only when the intended closing side is known.

```python
from pymt5 import (
    MT5WebClient, ORDER_TYPE_BUY, ORDER_TYPE_SELL,
    TRADE_RETCODE_DONE, ORDER_FILLING_IOC,
)

# Market orders
result = await client.buy_market("XAUUSD", 0.1, sl=2300.0, tp=2400.0)
result = await client.sell_market("EURUSD", 0.05, deviation=30)

# Pending orders
result = await client.buy_limit("EURUSD", 0.1, price=1.0800, sl=1.0750, tp=1.0900)
result = await client.sell_stop("GBPUSD", 0.1, price=1.2500)

# Stop-limit orders (trigger at price, then place limit at stop_limit_price)
result = await client.buy_stop_limit("EURUSD", 0.1, price=1.1000,
                                      stop_limit_price=1.0950, sl=1.0900, tp=1.1100)
result = await client.sell_stop_limit("GBPUSD", 0.1, price=1.2400,
                                       stop_limit_price=1.2450)

# Close position (auto-detects BUY/SELL direction)
result = await client.close_position("EURUSD", position_id=123456, volume=0.1)

# Close by opposite position (hedge netting)
result = await client.close_position_by("EURUSD", position_id=123456,
                                         position_by=789012)

# Modify SL/TP
result = await client.modify_position_sltp("EURUSD", position_id=123456,
                                            sl=1.0850, tp=1.0950)

# Cancel pending order
result = await client.cancel_pending_order(order=789012)
```

## Push Notifications

```python
# Combined position+order updates in one callback
def on_trade_change(data):
    print(f"Positions: {len(data['positions'])}, Orders: {len(data['orders'])}")
client.on_trade_update(on_trade_change)

# Trade transaction (order add/update/delete, balance updates)
def on_transaction(data):
    if data.get('update_type') == 2:
        print(f"Balance update: {data['balance_info']}")
    else:
        print(f"Order transaction: type={data.get('transaction_type')}")
client.on_trade_transaction(on_transaction)

# Account balance/equity changes
client.on_account_update(lambda d: print(f"Balance: {d.get('balance')}"))

# Extended symbol data with options greeks
client.on_symbol_details(lambda d: print(f"Delta: {d[0].get('delta')}"))

# Async trade execution results
client.on_trade_result(lambda d: print(f"Retcode: {d.get('result', {}).get('retcode')}"))

# Order book (DOM) updates
client.on_book_update(lambda entries: print(f"Book: {len(entries)} symbols"))

# Login status changes (e.g. forced logout)
client.on_login_status(lambda r: print(f"Login status: code={r.code}"))
```

## Protocol Flow

1. `connect()` — opens WebSocket, sends bootstrap (cmd=0), receives new AES key
2. `login()` — sends credentials (cmd=28), receives session token, starts heartbeat
3. Data commands (get_account, get_symbols, get_rates, get_positions_and_orders, etc.)
4. `close()` — stops heartbeat, sends logout (cmd=2), closes WebSocket

**Important**: Do NOT call `init_session()` (cmd=29) after login — that command is for demo/real account creation, not session initialization.

## Logging

```python
import logging
logging.basicConfig(level=logging.DEBUG)
# Loggers: pymt5.client, pymt5.transport
```

## Examples

Run any example from the project root:

```bash
pip install -e .
python examples/01_connect_and_account.py
```

| Example | Description |
|---------|-------------|
| `01_connect_and_account.py` | Login, full account info (cmd=3), positions/orders/deals, ping |
| `02_market_data.py` | Symbol groups (cmd=9), symbol cache, spreads (cmd=20), OHLCV rates |
| `03_realtime_ticks.py` | Tick subscription, account/symbol details/login status push handlers |
| `04_trading.py` | Market/limit/stop orders, modify SL/TP, position close |
| `05_order_book.py` | Order book (DOM) subscription (cmd=22/23) |
| `06_all_push_notifications.py` | All 9 push notification types in one example |
| `07_all_order_types.py` | All 9 order types + modify + cancel + close + raw trade_request |
| `08_misc_features.py` | Corporate links, notifications, trader params, verify code, reconnect |

## Command Coverage

| CMD | Name | Status |
|-----|------|--------|
| 0 | Bootstrap | Implemented |
| 2 | Logout | Implemented |
| 3 | Get Account | **NEW** — balance, credit, currency, leverage, name |
| 4 | Get Positions/Orders | Implemented |
| 5 | Get Trade History | Implemented |
| 6 | Get Symbols | Implemented |
| 7 | Subscribe Ticks | Implemented |
| 8 | Tick Push | Implemented |
| 9 | Get Symbol Groups | **NEW** — Forex, Metals, Indexes, etc. |
| 10 | Trade Update Push | **NEW** — order add/update/delete + balance updates |
| 11 | Get Rates | Implemented |
| 12 | Trade Request | Implemented |
| 13 | Symbol Update Push | Implemented |
| 14 | Account Update Push | **NEW** — real-time balance/equity changes |
| 15 | Login Status Push | Implemented |
| 17 | Symbol Details Push | **NEW** — extended quotes with options greeks |
| 18 | Get Full Symbols | Implemented |
| 19 | Trade Result Push | **NEW** — async trade execution results |
| 20 | Get Spreads | **IMPROVED** — proper schema parsing |
| 22 | Subscribe Book | **NEW** — order book / DOM subscription |
| 23 | Book Push | **NEW** — order book push with bid/ask levels |
| 24 | Change Password | Implemented |
| 27 | Verify Code | Implemented |
| 28 | Login | Implemented |
| 29 | Init Session | Implemented |
| 30 | Open Demo | Implemented |
| 39 | Open Real Account | Implemented |
| 40 | Send Verification Codes | Implemented |
| 34 | Get Symbols (gzip) | Implemented |
| 41 | Trader Params | Implemented |
| 42 | Notify | Implemented |
| 43 | OTP Setup / Disable | Implemented |
| 44 | Get Corporate Links | **NEW** — broker links |
| 51 | Ping | Implemented |
| 52 | Reserved Bootstrap Command | **EXPERIMENTAL** — bootstrap-only helper with unknown business meaning |

Commands 21, 25, 33, 37, 50, 52, 100-112 are still accepted by the transport allowlist but have no direct usage in the current public Web Terminal JavaScript source. Live probing on 2026-03-16 showed that 21, 25, 33, 37, 50, 100-112 time out with an empty payload, while 52 only returns an empty ACK on a pristine bootstrap-only socket and causes the server to drop the connection after `cmd=29` or `cmd=28`. Commands 39, 40, and 43 are now implemented in `pymt5`.

## Documentation

Full API documentation is available on [GitHub Pages](https://cloudquant.github.io/pymt5/).

To build docs locally:

```bash
pip install -e ".[docs]"
cd docs && make html
```

## CI/CD

This project uses GitHub Actions for continuous integration, testing across:

| | Python 3.11 | Python 3.12 | Python 3.13 |
|---|---|---|---|
| **Linux** (ubuntu-latest) | ✅ | ✅ | ✅ |
| **macOS** (macos-latest) | ✅ | ✅ | ✅ |
| **Windows** (windows-latest) | ✅ | ✅ | ✅ |

The CI pipeline also runs mypy type checking and builds the Sphinx documentation.
For a local pre-push pass, run:

```bash
make check
```

## Notes

- This package is experimental and tracks the MT5 Web Terminal protocol as observed in the public Web Terminal build 5687 (built on 2026-03-15)
- MetaQuotes may change the protocol at any time
- Volume encoding: MT5 uses integer volumes where `volume = lots × 10^precision`. The MetaQuotes demo server uses precision=8, so 1.0 lot = 100,000,000 and 0.01 lot = 1,000,000
- `get_account()` (cmd=3) returns balance, credit, currency, leverage, and name. Equity, margin, and profit are computed from positions
- The Symbol Details push (cmd=17) schema may produce approximate values — the exact byte layout for options greeks varies by server
- Order book (cmd=22/23) may not produce pushes on all servers — depends on broker configuration
- Some commands like `trader_params` (cmd=41) may cause the server to close the connection on rapid successive calls

## License

MIT License — see [LICENSE](LICENSE) for details.
