Skip to main content

Python SDK for integrating with Tonpo Gateway, enabling secure access to MetaTrader 5 trading services.

Project description

tonpo-py

PyPI version Python License

Official Python SDK for the Tonpo API — connect your application to MetaTrader 5 in minutes.


What is Tonpo?

Tonpo is a cloud API that gives your application a direct connection to MetaTrader 5. Send your MT5 credentials once, and trade through a simple Python interface — no Windows server, no MT5 installation, no infrastructure to manage.

Your Application
      │
      │  HTTPS / WSS
      ▼
Tonpo Gateway
      │
      │
      ▼
MT5 Terminal → Broker
        │
        ▼
  Your Broker
  (ICMarkets, XM, FBS, ...)

You only interact with the top — your app calls the SDK, Tonpo handles everything below.


Installation

pip install tonpo

Install the latest development version:

pip install git+https://github.com/TonpoLabs/tonpo-py.git

Clone and install locally for development:

git clone https://github.com/TonpoLabs/tonpo-py.git
cd tonpo-py
pip install -e ".[dev]"

Requirements: Python 3.10+


Quick Start

import asyncio
from tonpo import TonpoClient, TonpoConfig

config = TonpoConfig(
    host="gateway.tonpo.io",
    port=443,
    use_ssl=True,
)

async def main():
    # Step 1 — Create a Tonpo user account (once per user)
    async with TonpoClient.admin(config) as client:
        user = await client.create_user()
        print(f"api_key: {user.api_key}")          # save this
        print(f"user_id: {user.gateway_user_id}")   # save this

    # Step 2 — Connect an MT5 account (once per MT5 account)
    async with TonpoClient.for_user(config, user.api_key) as client:
        account = await client.create_account(
            mt5_login="105745233",
            mt5_password="YourMT5Password",
            mt5_server="FBS-Demo",
        )
        print(f"account_id: {account.account_id}")  # save this

        # Wait for MT5 to log in — takes 2–4 minutes on first connect
        await client.wait_for_active(account.account_id, timeout=180)
        print("MT5 connected!")

    # Step 3 — Trade (api_key + account_id is all you need from here)
    async with TonpoClient.for_user(config, user.api_key) as client:
        info = await client.get_account_info()
        print(f"Balance: {info.balance} {info.currency}")

        result = await client.place_market_buy("EURUSD", volume=0.1, sl=1.0800, tp=1.1000)
        print(f"Order placed: ticket={result.ticket}")

asyncio.run(main())

How It Works

Tonpo separates setup from trading. You go through setup once per user and once per MT5 account. After that, all you need are three values stored in your database.

Setup (run once)
──────────────────────────────────────────────────────────
create_user()       →  api_key + gateway_user_id   (save to DB)
create_account()    →  account_id                   (save to DB)
wait_for_active()   →  MT5 is logged in, ready to trade

Every request after that
──────────────────────────────────────────────────────────
for_user(api_key)   →  identifies and authenticates the user
account_id          →  tells Tonpo which MT5 account to act on

MT5 login, password, server — never needed again after setup.

Store only these three values per user in your database:

Field Description
tonpo_api_key Authenticates every request
tonpo_user_id User identity on Tonpo
tonpo_account_id Identifies which MT5 account to act on

Configuration

from tonpo import TonpoConfig

config = TonpoConfig(
    host="gateway.tonpo.io",      # Tonpo gateway hostname
    port=443,                      # 443 for SSL (default)
    use_ssl=True,                  # always True in production
    api_key_header="X-API-Key",    # header name (default is correct)
    connect_timeout=10.0,          # seconds to establish connection
    request_timeout=30.0,          # seconds to wait for a response
    ws_reconnect_delay=5.0,        # seconds between WebSocket reconnect attempts
    max_reconnect_attempts=5,      # max reconnects before raising an error
)

Client Modes

Admin client — no authentication required

Used only for health_check() and create_user().

async with TonpoClient.admin(config) as client:
    healthy = await client.health_check()
    user    = await client.create_user()

User client — authenticated

Used for all trading and account operations.

async with TonpoClient.for_user(config, api_key="your-api-key") as client:
    info = await client.get_account_info()

Manual lifecycle

client = TonpoClient(config, api_key="your-api-key")
await client.start()
try:
    await client.get_account_info()
finally:
    await client.stop()

API Reference

Health

healthy = await client.health_check()  # → bool

User Management

# Create a new Tonpo user — no auth required
user = await client.create_user()
# user.gateway_user_id  — store in your DB
# user.api_key          — store in your DB (shown once — save immediately)

Account Lifecycle

# Connect an MT5 account to Tonpo
account = await client.create_account(
    mt5_login="105745233",
    mt5_password="password",
    mt5_server="FBS-Demo",
    region="eu",          # optional — route to a specific region
)
# account.account_id — store in your DB

# Wait for MT5 to become active (logged in to broker)
# Default timeout is 180s — first connect takes 2–4 minutes
await client.wait_for_active(account.account_id, timeout=180)

# Check status manually
status = await client.get_account_status(account.account_id)
# status["status"]     — "active" | "connecting" | "paused" | "login_failed" | "deleted"
# status["last_error"] — error message if status is "login_failed"

# List all accounts for this user
accounts = await client.get_accounts()  # → List[dict]

# Pause / resume (blocks new orders while paused — MT5 stays connected)
await client.pause_account(account.account_id)
await client.resume_account(account.account_id)

# Remove account permanently
await client.delete_account(account.account_id)

Account Information

info = await client.get_account_info()
# info.login        → int
# info.name         → str
# info.server       → str
# info.balance      → float
# info.equity       → float
# info.margin       → float
# info.free_margin  → float
# info.leverage     → int
# info.currency     → str    ("USD", "EUR", ...)
# info.profit       → float
# info.margin_level → float  (equity / margin * 100)

Positions

positions = await client.get_positions()
for p in positions:
    print(p.ticket, p.symbol, p.side, p.volume, p.profit)

# Close a position (full or partial)
result = await client.close_position(ticket=123456)
result = await client.close_position(ticket=123456, volume=0.05)  # partial close

# Modify SL / TP
result = await client.modify_position(ticket=123456, sl=1.0800, tp=1.1000)

Orders

# Market orders
result = await client.place_market_buy("EURUSD",  volume=0.1)
result = await client.place_market_sell("GBPUSD", volume=0.2, sl=1.2500, tp=1.2200)

# Limit orders
result = await client.place_limit_buy("EURUSD",  volume=0.1, price=1.0750)
result = await client.place_limit_sell("EURUSD", volume=0.1, price=1.1050)

# Stop orders
result = await client.place_stop_buy("EURUSD",  volume=0.1, price=1.0950)
result = await client.place_stop_sell("EURUSD", volume=0.1, price=1.0700)

# All order methods accept optional parameters
result = await client.place_market_buy(
    symbol="EURUSD",
    volume=0.1,
    sl=1.0800,      # absolute stop loss price
    tp=1.1000,      # absolute take profit price
    comment="bot",  # visible in MT5 order history
    magic=12345,    # magic number — identifies your bot's orders in MT5
)

# result.ticket  → int         — MT5 ticket number
# result.success → bool
# result.error   → str | None  — broker error message on failure

Market Data (REST)

price = await client.get_symbol_price("EURUSD")
# price.symbol → "EURUSD"
# price.bid    → float
# price.ask    → float
# Falls back to WebSocket price cache automatically if REST returns zeros

Real-Time Data (WebSocket)

# Register callbacks before subscribing
def on_tick(tick):
    print(f"{tick.symbol}  bid={tick.bid}  ask={tick.ask}")

async def on_position_update(position):
    print(f"Position {position.ticket}: profit={position.profit}")

client.ws.on_tick("EURUSD", on_tick)
client.ws.on_position(on_position_update)
client.ws.on_candle("EURUSD", "H1", lambda c: print(f"H1 close={c.close}"))
client.ws.on_order_result(lambda r: print(f"Order {r.ticket} ok={r.success}"))
client.ws.on_account(lambda a: print(f"Balance={a.balance} {a.currency}"))

# Subscribe to symbols and start receiving data
await client.subscribe(["EURUSD", "GBPUSD"])

# Keep the event loop alive
await asyncio.sleep(3600)

# Unsubscribe and check connection
await client.unsubscribe(["GBPUSD"])
alive = await client.ping_ws()  # → bool

Models

Model Key fields
TonpoConfig host, port, use_ssl, api_key_header, connect_timeout, request_timeout, ws_reconnect_delay, max_reconnect_attempts
UserCredentials gateway_user_id, api_key
AccountCredentials account_id, auth_token
AccountInfo login, name, server, balance, equity, margin, free_margin, leverage, currency, profit, margin_level
Position ticket, symbol, side, volume, open_price, current_price, profit, swap, commission, sl, tp, open_time, comment
OrderResult ticket, success, error
SymbolPrice symbol, bid, ask
Tick symbol, bid, ask, last, volume, time
Quote symbol, bid, ask, time, spread, mid
Candle symbol, timeframe, time, open, high, low, close, volume, complete

Exceptions

All exceptions inherit from TonpoError.

from tonpo import (
    TonpoError,              # base — catch-all
    NotStartedError,         # client used before start() or outside async with
    AuthenticationError,     # invalid or revoked API key
    AccountNotFoundError,    # account_id does not exist
    AccountLoginFailedError, # MT5 credentials rejected by broker
    AccountTimeoutError,     # account did not become active within timeout
    OrderError,              # order placement, close, or modify failed
    TonpoConnectionError,    # HTTP or WebSocket connection failed
    SubscriptionError,       # WebSocket market data subscription failed
    TonpoResponseError,      # unexpected HTTP response (.status_code, .raw)
)

TonpoConnectionError is intentionally not named ConnectionError — that would shadow Python's built-in builtins.ConnectionError.

Error handling

from tonpo import (
    TonpoClient,
    AccountLoginFailedError,
    AccountTimeoutError,
    TonpoConnectionError,
    AuthenticationError,
    TonpoError,
)

# Account setup
try:
    await client.wait_for_active(account.account_id, timeout=180)
except AccountLoginFailedError as e:
    print(f"Wrong MT5 credentials: {e}")
    await client.delete_account(account.account_id)
except AccountTimeoutError as e:
    print(f"MT5 took too long to connect: {e}")

# Trading
try:
    result = await client.place_market_buy("EURUSD", volume=0.1)
except AuthenticationError:
    print("API key invalid — re-create the user")
except TonpoConnectionError:
    print("Cannot reach Tonpo — check your internet connection")
except TonpoError as e:
    print(f"Tonpo error: {e}")

Example — Telegram Trading Bot

A common use case: each Telegram user connects their own MT5 account, and your bot places trades on their behalf.

from tonpo import TonpoClient, TonpoConfig, AccountLoginFailedError, AccountTimeoutError

config = TonpoConfig(host="gateway.tonpo.io", port=443, use_ssl=True)

# ── Registration ──────────────────────────────────────────────────────────────
# Called once when a user submits their MT5 credentials.

async def register_user(telegram_id, mt5_login, mt5_password, mt5_server):
    async with TonpoClient.admin(config) as c:
        user = await c.create_user()

    async with TonpoClient.for_user(config, user.api_key) as c:
        account = await c.create_account(mt5_login, mt5_password, mt5_server)
        try:
            await c.wait_for_active(account.account_id, timeout=180)
        except AccountLoginFailedError:
            await c.delete_account(account.account_id)
            raise

    # MT5 credentials are never needed again — store only these three values
    db.save(
        telegram_id      = telegram_id,
        tonpo_api_key    = user.api_key,
        tonpo_user_id    = user.gateway_user_id,
        tonpo_account_id = account.account_id,
    )

# ── Place a trade ─────────────────────────────────────────────────────────────

async def place_buy(telegram_id, symbol, volume):
    row = db.get(telegram_id=telegram_id)
    async with TonpoClient.for_user(config, row.tonpo_api_key) as c:
        result = await c.place_market_buy(symbol, volume=volume)
        return result.ticket

# ── Check balance ─────────────────────────────────────────────────────────────

async def get_balance(telegram_id):
    row = db.get(telegram_id=telegram_id)
    async with TonpoClient.for_user(config, row.tonpo_api_key) as c:
        info = await c.get_account_info()
        return info.balance, info.currency

Project Structure

tonpo-py/
├── pyproject.toml              # packaging metadata
├── setup.py                    # legacy build shim
├── MANIFEST.in                 # source distribution file list
├── LICENSE
├── README.md
├── CHANGELOG.md
├── .gitignore
├── .github/
│   └── workflows/
│       └── publish.yml         # auto-publishes to PyPI on git tag
└── tonpo/
    ├── __init__.py             # public API + __version__
    ├── client.py               # TonpoClient — main entry point
    ├── models.py               # all dataclasses
    ├── exceptions.py           # exception hierarchy
    ├── transport.py            # HTTP layer (httpx)
    ├── websocket.py            # WebSocket layer (auto-reconnection)
    └── py.typed                # PEP 561 marker — enables IDE type hints

Publishing a Release

# 1. Bump version in pyproject.toml and tonpo/__init__.py
# 2. Add entry to CHANGELOG.md
# 3. Commit, tag, and push

git add .
git commit -m "Release v1.0.6"
git tag v1.0.6
git push origin main
git push origin v1.0.6
# GitHub Actions builds and publishes to PyPI automatically

Development

git clone https://github.com/TonpoLabs/tonpo-py.git
cd tonpo-py
pip install -e ".[dev]"

pytest
pytest tests/test_client.py -v

Dev dependencies:

Package Purpose
httpx>=0.24 Async HTTP client
websockets>=11.0 Async WebSocket client
pytest Test runner
pytest-asyncio Async test support
respx httpx request mocking

Changelog

v1.0.6 — 2026-05-04

  • WebSocket resilience improvements — proper CancelledError handling on disconnect
  • Fixed ConnectionClosed logging — removed invalid .rcvd_then attribute access
  • Comprehensive test suite — 97 tests across transport, validation, and WebSocket layers

v1.0.5 — 2026-04-19

  • License updated to Proprietary
  • wait_for_active default timeout raised to 180s

v1.0.0 — 2026-04-10

  • Initial release
  • TonpoClient with admin() and for_user() factory methods
  • Full account lifecycle: create_account, wait_for_active, get_account_status, get_accounts, delete_account, pause_account, resume_account
  • All order types: market, limit, stop (buy and sell)
  • Position management: get_positions, close_position, modify_position
  • Account info: get_account_info
  • Market data: get_symbol_price (REST + WebSocket cache fallback)
  • WebSocket real-time data with auto-reconnection: ticks, quotes, candles, positions, order results, account updates
  • Typed dataclass models for all gateway responses
  • py.typed PEP 561 marker for full IDE type hint support
  • GitHub Actions workflow for automated PyPI publishing on git tag
  • TonpoConnectionError named to avoid shadowing builtins.ConnectionError

License

Proprietary — All rights reserved. © Tonpo. Unauthorised copying, distribution, or use is strictly prohibited.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tonpo-1.0.6.tar.gz (55.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tonpo-1.0.6-py3-none-any.whl (32.8 kB view details)

Uploaded Python 3

File details

Details for the file tonpo-1.0.6.tar.gz.

File metadata

  • Download URL: tonpo-1.0.6.tar.gz
  • Upload date:
  • Size: 55.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tonpo-1.0.6.tar.gz
Algorithm Hash digest
SHA256 15469bbb5ef334a7aa204d7714d6fb14155aea9056d749fc1b1f4c929f2aabfe
MD5 c6af23a39c6d76dc46e94dbff55f85cf
BLAKE2b-256 5be9787fca4d746e2b2efe0b75b4e2d973fd2944970bf509ed3f9a36957e531a

See more details on using hashes here.

Provenance

The following attestation bundles were made for tonpo-1.0.6.tar.gz:

Publisher: publish.yml on TonpoLabs/tonpo-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tonpo-1.0.6-py3-none-any.whl.

File metadata

  • Download URL: tonpo-1.0.6-py3-none-any.whl
  • Upload date:
  • Size: 32.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tonpo-1.0.6-py3-none-any.whl
Algorithm Hash digest
SHA256 746d3d85f053280943551b967e1fbd6682f9d17c7dca70fc99a37a70819191a8
MD5 3fae74f3945b0fa645495e3c8f306371
BLAKE2b-256 414acf57a56f44de9fbb64047b9ef58a2d9096741251a429f339db7bd919ce28

See more details on using hashes here.

Provenance

The following attestation bundles were made for tonpo-1.0.6-py3-none-any.whl:

Publisher: publish.yml on TonpoLabs/tonpo-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page