Metadata-Version: 2.4
Name: tonpo
Version: 1.0.4
Summary: Python SDK for the Tonpo Gateway — self-hosted MT5 trading infrastructure
Author-email: TonpoLabs <carbon-crumb-churn@duck.com>
License: Proprietary
Project-URL: Homepage, https://cipherbridge.cloud
Project-URL: Repository, https://github.com/TonpoLabs/tonpo-py
Project-URL: Bug Tracker, https://github.com/TonpoLabs/tonpo-py/issues
Keywords: mt5,metatrader,trading,forex,gateway,tonpo
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.24.0
Requires-Dist: websockets>=11.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: respx>=0.20; extra == "dev"
Dynamic: license-file

# tonpo-py

[![PyPI version](https://img.shields.io/pypi/v/tonpo.svg)](https://pypi.org/project/tonpo/)
[![Python](https://img.shields.io/pypi/pyversions/tonpo.svg)](https://pypi.org/project/tonpo/)
[![License](https://img.shields.io/badge/License-Proprietary-red.svg)](LICENSE)

Official Python SDK for the **Tonpo Gateway** — a proprietary MT5 trading infrastructure platform.

---

## What is Tonpo?

The Tonpo Gateway is a Rust server that manages MetaTrader 5 terminal connections through the Tonpo Bridge (C++ DLL + MQL5 EA). Instead of routing through third-party cloud APIs, the gateway runs on your own infrastructure — giving you full control over latency, cost, and data.

```
Your App (Python)
      │
      │  HTTPS / WSS
      ▼
Tonpo Gateway  (Rust — your server)
      │
      │  WebSocket
      ▼
Tonpo Bridge   (C++ DLL + MQL5 EA)
      │
      ▼
MT5 Terminal → Broker
```

This SDK handles all communication with the gateway so you never write raw HTTP calls.

---

## Installation

```bash
pip install tonpo
```

Install the latest development version from GitHub:

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

Clone and install locally for development:

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

**Requirements:** Python 3.10+

---

## Quick Start

```python
import asyncio
from tonpo import TonpoClient, TonpoConfig

config = TonpoConfig(
    host="gateway.cipherbridge.cloud",
    port=443,
    use_ssl=True,
)

async def main():
    # 1. Create a gateway user — once per user, store the credentials
    async with TonpoClient.admin(config) as client:
        user = await client.create_user()
        print(f"api_key: {user.api_key}")
        print(f"user_id: {user.gateway_user_id}")

    # 2. Provision an MT5 account — once per 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}")

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

    # 3. Trade — api_key + account_id is all you need from now on
    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())
```

---

## Core Concept — Credential Flow

The gateway owns your MT5 credentials. The SDK reflects this:

```
Step 1 — Register (once per user)
  create_user()       → api_key + gateway_user_id   (store both)
  create_account()    → account_id                   (store this)
  wait_for_active()   → confirms MT5 is logged in

Step 2 — Every subsequent request
  for_user(api_key)   → authenticates the user
  account_id          → tells gateway which MT5 account to act on

MT5 login, password, server — never needed again after Step 1.
```

Store only these values per user in your database:

| Field | Description |
|---|---|
| `tonpo_api_key` | Authenticates every request |
| `tonpo_user_id` | User identity on the gateway |
| `tonpo_account_id` | Identifies which MT5 account to act on |

---

## Configuration

```python
from tonpo import TonpoConfig

config = TonpoConfig(
    host="gateway.cipherbridge.cloud",  # gateway hostname or IP
    port=443,                            # 443 for SSL, 8080 for plain HTTP
    use_ssl=True,                        # must match your Nginx setup
    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 response
    ws_reconnect_delay=5.0,              # seconds between WebSocket reconnect attempts
    max_reconnect_attempts=5,            # max reconnects before raising error
)
```

---

## Client Modes

### Admin client — no authentication

Used only for `health_check()` and `create_user()`.

```python
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.

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

### Manual lifecycle

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

---

## API Reference

### Health

```python
healthy = await client.health_check()  # → bool
```

### User Management

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

### Account Lifecycle

```python
# Provision a new MT5 account
account = await client.create_account(
    mt5_login="105745233",
    mt5_password="password",
    mt5_server="FBS-Demo",
    region="eu",          # optional — route to specific node region
)
# account.account_id — store in DB

# Wait for MT5 to become active (logged in to broker)
# Default timeout is 180s — Windows VPS cold start 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 (keeps MT5 connected, blocks new orders while paused)
await client.pause_account(account.account_id)
await client.resume_account(account.account_id)

# Remove account permanently — deprovisions the MT5 instance
await client.delete_account(account.account_id)
```

### Account Information

```python
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  (computed: margin / equity * 100)
```

### Positions

```python
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

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

### Orders

```python
# 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)

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

### Real-Time Data (WebSocket)

```python
# 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}"))

# Start WebSocket connection and subscribe to symbols
await client.subscribe(["EURUSD", "GBPUSD"])

# Keep event loop alive to receive data
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`.

```python
from tonpo import (
    TonpoError,              # base — catch-all
    NotStartedError,         # client used before start() / outside async with
    AuthenticationError,     # invalid or revoked API key
    AccountNotFoundError,    # account_id does not exist on gateway
    AccountLoginFailedError, # MT5 credentials rejected by broker
    AccountTimeoutError,     # account did not become active in time
    OrderError,              # order placement/close/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

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

# Account provisioning
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-register the user")
except TonpoConnectionError:
    print("Cannot reach gateway — check server")
except TonpoError as e:
    print(f"Gateway error: {e}")
```

---

## Usage in a Telegram Bot

```python
from tonpo import TonpoClient, TonpoConfig, AccountLoginFailedError, AccountTimeoutError

config = TonpoConfig(host="gateway.cipherbridge.cloud", port=443, use_ssl=True)

# ── Registration handler ───────────────────────────────────────────────────────
# Called once when 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

    # Store only these — credentials never needed again
    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,
    )

# ── Trade handler ─────────────────────────────────────────────────────────────

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

# ── Balance check ─────────────────────────────────────────────────────────────

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

```bash
# 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.5"
git tag v1.0.5
git push origin main
git push origin v1.0.5
# GitHub Actions builds and uploads to PyPI automatically
```

---

## Development

```bash
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.4 — 2026-04-19

- License updated to Proprietary
- Gateway URL updated to `gateway.cipherbridge.cloud`
- `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
- `create_account` sends camelCase keys matching gateway's Rust struct (`mt5Login`, `mt5Password`, `mt5Server`)
- `TonpoConnectionError` named to avoid shadowing `builtins.ConnectionError`

---

## License

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