Metadata-Version: 2.4
Name: tickerall
Version: 0.6.0
Summary: Official Python client for the TickerAll REST + WebSocket API — place trades, stream live market data, and manage broker sessions without an MT4/MT5 terminal in the path.
Project-URL: Homepage, https://tickerall.com
Project-URL: Documentation, https://tickerall.com/docs
Project-URL: Repository, https://github.com/TickerAll/tickerall
Author: Miguel Santos
License: MIT
License-File: LICENSE
Keywords: algo,api,broker,forex,mt4,mt5,sdk,tickerall,trading,websocket
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
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.9
Requires-Dist: httpx>=0.25
Requires-Dist: websocket-client>=1.6
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# tickerall

Official Python client for the [TickerAll](https://tickerall.com) REST + WebSocket API.

Place trades, stream live market data, and manage broker sessions programmatically — **without an MT4/MT5 terminal in the path**. No Windows VM, no Wine, no `MetaTrader5` terminal to babysit, no thread-safety workarounds.

```bash
pip install tickerall
```

Requires Python 3.9+. Depends only on [`httpx`](https://www.python-httpx.org/) and [`websocket-client`](https://github.com/websocket-client/websocket-client).

## Why

The official `MetaTrader5` Python package only runs on Windows, drives a local terminal over a single-threaded IPC channel, and falls over under concurrency. TickerAll hosts the broker connection for you and exposes it as a clean HTTP + WebSocket API, so your bot can run anywhere — Linux, macOS, a container, a Raspberry Pi — and stream ticks instead of polling.

| | `MetaTrader5` (local terminal) | `tickerall` |
|---|---|---|
| OS | Windows only | anywhere Python runs |
| Live ticks | poll `symbol_info_tick()` per symbol | push over WebSocket |
| Concurrency | single-threaded IPC, not thread-safe | stateless HTTP, thread-safe |
| Deploy | a terminal per account to babysit | `pip install` |

## Quickstart

```python
from tickerall import Tickerall

client = Tickerall(api_key="cf_live_...")

# Connect a broker account → get a TickerAll account_id
session = client.sessions.start(
    broker="mt5",
    server="Exness-MT5Trial7",
    account=12345678,
    password="...",
)

# Place a market order
order = client.orders.place(
    session.account_id,
    type="market",
    symbol="BTCUSDm",
    side="BUY",
    volume=0.10,
    stop_loss=58000.0,
    take_profit=72000.0,
)
print(order.ticket, order.status)

client.sessions.end(session.account_id)
```

The client is a context manager too:

```python
with Tickerall(api_key="cf_live_...") as client:
    ...
```

## Terminal type (MOBILE / WEB / CLIENT)

`terminal_type` picks which client the connection presents AS — `"MOBILE"` (the
default), `"WEB"`, or `"CLIENT"` (a desktop terminal). All expose the full surface
(account, quotes, positions, history). The type sets the broker-assigned order
origin (`ENUM_DEAL_REASON`): `"MOBILE"` → `DEAL_REASON_MOBILE`, `"WEB"` →
`DEAL_REASON_WEB`, `"CLIENT"` → `DEAL_REASON_CLIENT` — useful where a venue
distinguishes desktop-placed orders (e.g. some prop firms).

`"WEB"` **requires the broker's web-terminal URL** (`web_terminal_url`) — web
terminals are per-broker-domain, so the URL must be supplied. `"MOBILE"` and
`"CLIENT"` take neither web field:

```python
session = client.sessions.start(
    broker="mt5",
    server="YourBroker-Server",
    account=12345678,
    password="...",
    terminal_type="WEB",
    web_terminal_url="https://mt5.yourbroker.com",  # required for WEB
    # web_endpoint="wss://host/path",               # optional WS override (rare)
)
```

For a desktop-origin (`DEAL_REASON_CLIENT`) connection — no web URL needed:

```python
session = client.sessions.start(
    broker="mt5",
    server="YourBroker-Server",
    account=12345678,
    password="...",
    terminal_type="CLIENT",
)
```

## Streaming — push, not poll

The stream runs on its own background thread. Register callbacks and go; it
heartbeats, reconnects with backoff, and re-subscribes automatically.

```python
client = Tickerall(api_key="cf_live_...")
session = client.sessions.start(broker="mt5", server="Exness-MT5Trial7",
                                account=12345678, password="...")

stream = client.stream.connect()
stream.on("tick", lambda e: print(e.symbol, e.bid, e.ask, e.timestamp))
stream.on("position", lambda e: print(e.event, e.position.ticket, e.position.profit))
stream.subscribe_ticks(session.account_id, ["BTCUSDm", "ETHUSDm"])
stream.subscribe_positions(session.account_id)

# ... your app runs ...
stream.close()
```

### Keep an in-memory tick cache fresh (zero polling)

A common pattern: let the WebSocket fill a dict so price reads are O(1) with no
network call — strictly better than polling a terminal per symbol.

```python
latest: dict[str, "TickEvent"] = {}
stream = client.stream.connect()
stream.on("tick", lambda e: latest.__setitem__(e.symbol, e))
stream.subscribe_ticks(session.account_id, ["BTCUSDm", "ETHUSDm", "XAUUSDm"])

# Anywhere in your app — instant, no IPC, no thread-safety dance:
tick = latest.get("BTCUSDm")
```

## Market data & history

```python
# Historical OHLC candles (coarser timeframes reach further back)
bars = client.candles.get(session.account_id, symbol="BTCUSDm", hours=24, timeframe="M5")
for c in bars:
    print(c.timestamp, c.open, c.high, c.low, c.close)

# Closed-trade history (recent broker window)
trades = client.history.get(session.account_id, symbol="BTCUSDm", limit=100)

# Tradeable symbols and their volume specs
symbols = client.accounts.symbols(session.account_id)
specs = client.accounts.symbol_specs(session.account_id)  # volume min/max/step + base/quote/margin currency (MT5)

# Remove an account from your roster (disconnects it + drops it from your list
# and billing; broker account and open positions are untouched). Reversible —
# reconnect the same login with sessions.start to re-add it.
client.accounts.remove(session.account_id)
```

## Positions

```python
detail = client.accounts.get(session.account_id)
for p in detail.positions:
    print(p.ticket, p.symbol, p.side, p.volume, p.profit)

client.positions.modify(session.account_id, ticket=p.ticket, stop_loss=60000.0)
client.positions.close(session.account_id, ticket=p.ticket)          # full close
client.positions.close(session.account_id, ticket=p.ticket, volume=0.05)  # partial
```

## Bulk operations

Execute one action across many of your accounts in a single request. Bulk requires an eligible plan — a request without it returns `403 BULK_REQUIRES_PRO`; see [pricing](https://tickerall.com/pricing) for what's included. A bulk call spans N broker sessions and is **not** atomic — partial success is the contract: each account's outcome is in `results`, and a mix of success/failure still returns (nothing is raised).

> Bulk **trading** (place/close/modify/cancel) currently runs on **demo** accounts; live trading is coming soon. The bulk **read** works on all your accounts, demo or live.

```python
# Place the same order across several accounts
placed = client.bulk.place([
    {"account_id": "acc_1", "type": "market", "symbol": "EURUSDm", "side": "BUY", "volume": 0.1},
    {"account_id": "acc_2", "type": "market", "symbol": "EURUSDm", "side": "BUY", "volume": 0.2},
])
# placed.results -> [BulkPlaceResult(account_id, status='filled'|'failed', ticket, price, ...)]
# placed.summary -> BulkPlaceSummary(total, filled, failed)

# Close positions — explicit tickets, or by intent (flatten all / by symbol+side)
client.bulk.close_positions(items=[{"account_id": "acc_1", "ticket": 4072808150}])
client.bulk.close_positions(targets=[{"account_id": "exness_acc", "symbol": "EURUSDm"}, {"account_id": "xm_acc", "symbol": "EURUSD"}])  # each account, its own broker-native symbol
client.bulk.close_positions(targets=[{"account_id": "acc_1"}, {"account_id": "acc_2"}])                                                # no symbol on a target → flatten that account

# Modify SL/TP, cancel + modify pending orders
client.bulk.modify_positions([{"account_id": "acc_1", "ticket": 4072808150, "stop_loss": 1.0850}])
client.bulk.cancel_pending(targets=[{"account_id": "acc_1", "symbol": "EURUSDm"}])
client.bulk.modify_pending([{"account_id": "acc_1", "ticket": 4072808151, "price": 1.0805}])

# Read live state for many accounts at once (balance/equity/margin + open positions)
roster = client.bulk.read_accounts()                                            # your whole roster
some = client.bulk.read_accounts(ids=["acc_1", "acc_2"], include=["account", "positions"])
# roster.accounts -> [BulkAccountState(id, status='online'|'offline', account, positions, ...)]
# roster.summary  -> BulkAccountsSummary(total, online, offline)
```

In close/cancel by intent, **each target carries its own broker-native `symbol`** — so a mixed-broker roster (Exness `EURUSDm` + XM `EURUSD`) is handled in one call. Omit a target's `symbol` to flatten every position on that account; a target whose symbol matches nothing simply reports `no matching open positions`.

Every write method auto-generates an idempotency key (pass `idempotency_key=` to dedupe retries), like the single-account methods.

## Copy trading

Mirror one **master** account's trades to many **follower** accounts — each scaled, symbol-mapped, and risk-clamped to its own size. Create a set, tune each follower, arm it, and the moment the master trades (through TickerAll) the followers follow. All accounts are your own. Copy trading requires an eligible plan — a request without it returns `403 COPY_REQUIRES_PRO`; see [pricing](https://tickerall.com/pricing).

> Copy **trading** currently mirrors to **demo** followers; live is coming soon. Managing sets + reading stats works on all accounts.

```python
# Create a set: one master, followers each with their own sizing + risk
s = client.copy.create_set(
    "My desk", "acc_master",
    followers=[
        {"follower_account_id": "acc_1", "sizing_method": "proportional"},                     # scale by equity ratio
        {"follower_account_id": "acc_2", "sizing_method": "multiplier", "sizing_value": 0.5},   # half the master's size
        {"follower_account_id": "acc_3", "sizing_method": "fixed", "sizing_value": 0.01, "symbol_block": ["XAUUSD"]},
    ],
)

client.copy.arm(s.id)   # start mirroring  (pause with client.copy.pause(s.id))

# Manage followers
client.copy.add_follower(s.id, "acc_4", config={"reverse": True, "max_slippage_pips": 3})
client.copy.update_follower(s.id, "follower_id", {"max_lot": 1, "min_master_lot": 0.05})

# Stats + the copy log
stats = client.copy.get_stats(s.id)          # totals, replication rate, per-follower rollups
page = client.copy.get_log(s.id, limit=50)   # every mirrored action; page with before=
```

Config keys are accepted in snake_case or camelCase. **Sizing** per follower: `proportional` (by equity ratio — a small account gets proportionally small trades), `multiplier`, `fixed`, or `risk_percent`. **Per-follower controls** include lot clamps, exposure caps, symbol allow/block lists, a daily-loss stop, `reverse` (inverse) copy, a slippage guard, a min-master-lot filter, and per-broker `symbol_overrides`. A follower's symbol auto-resolves from the master's (suffix-normalized); set `symbol_overrides` for anything cross-broker that doesn't.

## Always-hot sessions & transparent re-arm

For connections that must stay up across restarts, use `keep_alive`. The
credentials live in this process's memory only (never persisted); if the
account goes cold (e.g. TickerAll restarted), the next call transparently
re-supplies them and retries once.

```python
session = client.sessions.keep_alive(broker="mt5", server="Exness-MT5Trial7",
                                     account=12345678, password="...")
# ... later, after an outage, this just works — the client re-arms under the hood:
client.accounts.get(session.account_id)

# Stop keeping it alive (drops the cached credentials):
client.sessions.stop_keep_alive(session.account_id)
```

## Reliability — idempotency & queue-and-replay

State-changing calls (`sessions.start`, `orders.place`, `positions.close`,
`positions.modify`) carry a stable **Idempotency-Key**, so a retried call can't
double-execute. By default a transient connectivity failure
(`TickerallServiceUnavailableError`, `.transient == True`) **fails fast** so you
can re-decide with fresh prices:

```python
from tickerall import TickerallServiceUnavailableError

try:
    client.orders.place(account_id, type="market", symbol="BTCUSDm", side="BUY", volume=0.1)
except TickerallServiceUnavailableError:
    ...  # momentary blip — safe to retry
```

For price-insensitive orders (pending orders, SL/TP edits) you can instead
**queue-and-replay** until connectivity returns:

```python
client.orders.place(
    account_id, type="limit", symbol="BTCUSDm", side="BUY", volume=0.1, price=60000.0,
    queue_if_reconnecting=True, queue_max_s=60.0,
)
```

## Errors

All errors derive from `TickerallApiError` and carry `.status`, `.code`,
`.request_id`, `.details`, and `.transient`:

| Class | When |
|---|---|
| `TickerallAuthError` | 401 — bad/missing API key |
| `TickerallForbiddenError` | 403 — plan limit / reserved resource |
| `TickerallValidationError` | 400 / 422 — malformed request |
| `TickerallNotFoundError` | 404 — account / position not found |
| `TickerallBrokerError` | broker rejected or could not satisfy the request |
| `TickerallServiceUnavailableError` | transient — TickerAll momentarily unreachable (safe to retry) |

## Using it from an async app

REST methods are synchronous and thread-safe, so call them from an event loop
via `asyncio.to_thread`:

```python
detail = await asyncio.to_thread(client.accounts.get, account_id)
```

The stream is already non-blocking (its own thread) — callbacks fire as events
arrive.

## License

MIT © Miguel Santos
