FxSocket Python SDK
Typed Python client for the FxSocket API. Connect your MetaTrader 4 / 5 accounts, then place trades, read market data, and stream live updates over REST and WebSocket — with mirrored synchronous and async interfaces.
Features
- Account management — link, list, fetch, and disconnect MT4/MT5 accounts.
- Private servers — list your dedicated hosting servers and manage the accounts on them.
- Read-only keys — mint, scope, rotate and revoke
fxs_ro_…keys for dashboards and monitors. - Trading — market & pending orders, modify, close, close-all, plus margin/profit calculators.
- Multi-account trading — one order, or one close, fanned out to several accounts in a single request, with idempotency keys for safe retries.
- Wallet — read your prepaid balance, pending top-ups and upcoming charges.
- Market data — quotes, symbol specifications (incl. commission rules & trading sessions), OHLC history, account state & info.
- Live streaming — ticks, bars, account, positions, trades, and terminal status over WebSocket, with automatic reconnect + subscription replay.
- Sync and async —
Client/AsyncClient, method-for-method mirrors. - Typed — Pydantic v2 models throughout; ships
py.typed. - One interface for MT4 and MT5 — platform differences handled for you.
Install
pip install fxsocket
Requires Python 3.10+.
Quickstart
from fxsocket import Client
with Client(api_key="fxs_live_…") as fx: # or set FXSOCKET_API_KEY
account = fx.accounts.list()[0]
term = fx.terminal(account)
print("equity:", term.account_summary().equity)
print("EURUSD:", term.quote("EURUSD").ask)
Authentication
Every call uses your FxSocket API key (fxs_live_…), from the dashboard.
Pass it explicitly, or set the FXSOCKET_API_KEY environment variable and call
Client() with no arguments.
from fxsocket import Client
with Client(api_key="fxs_live_…") as fx:
for account in fx.accounts.list():
print(account.platform, account.nickname, account.status)
Managing accounts
from fxsocket import Client
with Client(api_key="fxs_live_…") as fx:
# Link a new account (platform defaults to MT5).
account = fx.accounts.create(
platform="mt5", server="ICMarkets-Demo", login=1150125, password="…",
)
# Poll until it's connected.
account = fx.accounts.get(account.id)
print(account.status) # connecting → connected
# Where this account's terminal API lives (empty until provisioned).
print(account.rest_url, account.ws_url)
# Move the trade expert to a specific chart symbol (the terminal
# restarts on it — poll until connected again). "" reverts to automatic.
account = fx.accounts.update(account, trade_ea_symbol="EURUSDm")
fx.accounts.delete(account.id) # unlink
An account's terminal can be routed through an outbound proxy at link time.
The proxy is verified first — an unreachable one raises ConnectFailedError
with code == "proxy_unreachable" and nothing is created. The address, type
and local port are readable on the Account; the credentials never come
back.
account = fx.accounts.create(
server="ICMarkets-Demo", login=1150125, password="…",
trade_ea_symbol="EURUSDm", # optional, broker-exact
proxy_address="10.0.0.5:1080", proxy_type="socks5",
proxy_auth="user:secret", proxy_local_port=1080,
)
Everything is also available on AsyncClient:
from fxsocket import AsyncClient
async with AsyncClient(api_key="fxs_live_…") as fx:
accounts = await fx.accounts.list()
Read-only keys
A read-only key (fxs_ro_…) can call every GET endpoint but nothing that
mutates state. fx.readonly_keys manages named ones; each has a scope —
all sees every account, selected only the accounts attached to it
(everything else is absent from lists, 404 by id and 401 at the terminal).
Passing accounts implies selected.
from fxsocket import Client, KeyScope
with Client(api_key="fxs_live_…") as fx:
key = fx.readonly_keys.create(name="dashboard", accounts=[account])
print(key.key) # plaintext, returned on every read
key = fx.readonly_keys.update(key, name="ops-dashboard")
key = fx.readonly_keys.rotate(key) # new secret, same name & scope
fx.readonly_keys.delete(key) # revoke — immediate, irreversible
for k in fx.readonly_keys.list():
print(k.name, k.scope == KeyScope.ALL, k.last_used_at)
Terminals are started with the exact set of read-only keys they accept, so
creating, re-scoping, rotating or revoking a key restarts the terminals of
every account in its scope — each goes briefly offline, typically a few
minutes. Renaming is free. Key management itself always needs the full
fxs_live_… key, even for reads, because the replies contain plaintext
key values.
Trading & market data
fx.terminal(account) returns a REST client bound to that account's terminal
(resolved from account.rest_url, whether it's a shared pod or a private droplet):
from fxsocket import Client
with Client(api_key="fxs_live_…") as fx:
account = fx.accounts.get("…") # must be connected (has a terminal)
term = fx.terminal(account)
summary = term.account_summary() # balance, equity, margin, …
quote = term.quote("EURUSD") # latest tick
bars = term.price_history("EURUSD", "M5") # recent OHLC bars
result = term.order_send( # market buy
symbol="EURUSD", operation="Buy", volume=0.10,
stop_loss=1.07, take_profit=1.10,
)
if result.success:
term.order_modify(result.order, take_profit=1.12) # None keeps the SL
term.order_close(result.order)
Every order call returns an OrderResult. A 200 only means the terminal
answered — check the body: success is true for retcode 10009 (done) or
10008 (placed), and outcome classifies the result as applied /
no_change / partial / rejected (compare against OrderOutcome).
no_change (retcode 10025) is a benign, idempotent no-op — the requested
SL/TP/price already match — so it's safe to treat as applied even though
success is False. For idempotent SL/TP management (e.g. re-sending after a
lost confirmation), send absolute values and gate on result.is_effective
(true for both applied and no_change):
res = term.order_modify(ticket, stop_loss=1.0850)
if res.is_effective: # applied now, or already in effect
...
There's also a panic button. close_all() closes every open position in one
trade-EA pass — optionally filtered by symbol and/or magic (magic=0
matches manually-opened orders), and delete_pending=True also deletes
matching pending orders. It returns a CloseAllSummary with per-ticket
results. On a 504 the pass continues inside the terminal — check
opened_orders() before acting again rather than re-sending:
summary = term.close_all(symbol="EURUSD", delete_pending=True)
if summary.failed:
for r in summary.results:
if not r.success:
print(r.ticket, r.retcode, r.retcode_description)
Inputs are validated client-side before they're sent. One guard worth knowing:
in order_modify, a literal stop_loss=0.0 would remove your stop-loss, so
it's rejected — pass clear_stop_loss=True to remove one deliberately, while
None (the default) keeps the current value.
MT4 and MT5 share one interface. MT5-only timeframes (M2, M3, H2, H6,
H8, H12) raise UnsupportedOnPlatformError on MT4 before any request.
MT4 history note: on MT4,
price_historywithfrom_/tobounds (or theD1timeframe) can fail server-side withCopyRates failedwhen the terminal hasn't loaded that history. Callingprice_history(symbol, timeframe)without date bounds returns the most recent bars reliably.
Multi-account trading
fx.orders sends one order — or one close — to several accounts in a single
request, through the management API rather than each terminal. Every leg
inherits defaults and may override any of it, so "same trade, three
accounts, three lot sizes" stays short. A leg can be an OrderLeg, a plain
dict, or just the account (object or id) when defaults say everything else.
from fxsocket import Client, OrderDefaults, OrderLeg
with Client(api_key="fxs_live_…") as fx:
accounts = [a for a in fx.accounts.list() if a.has_terminal]
result = fx.orders.send(
[OrderLeg(account_id=a, volume=0.1 * (i + 1)) for i, a in enumerate(accounts)],
defaults=OrderDefaults(symbol="EURUSD", operation="buy", stop_loss=1.07),
idempotency_key="signal-4711",
)
for account, leg in zip(accounts, result.results):
print(account.nickname, leg.status, leg.order, leg.message)
Validation is all-or-nothing, execution is not. A malformed leg raises
ValidationError before anything is sent (the SDK checks what the API
checks — symbol / operation / volume present, a price for pending orders,
…). Once dispatched the legs are independent: you get a BatchOrderResult
with a positional results list — zip it with what you sent rather than
matching on account_id, which repeats when several legs target one
account. Each OrderLegResult.status is only trustworthy as filled; a
timeout leg may have executed (is_unknown), so never blind-retry it.
result.all_filled, filled_legs, failed_legs and unknown_legs roll
this up.
Symbols are per broker (EURUSD, EURUSD.sd, EURUSDm), so a single
defaults symbol across mixed brokers will partly fail by design — that is
what a leg-level symbol is for.
Idempotency. Pass an idempotency_key (any opaque string, ≤ 128 chars)
whenever a retry is possible: replaying the identical batch with the same
key returns the stored reply (idempotent_replay=True) and sends nothing.
Keys are remembered for 15 minutes. A key that is still in flight, was
already used for a different body, or can't currently be guaranteed raises
IdempotencyError — nothing is sent in any of those cases.
Closing works by selector, not by ticket: each account is matched
against symbol (plus optional side, kind, magic), the backend
resolves the tickets from that account's open orders and closes them.
symbol is required — type "*" to mean every symbol; it is never implied.
symbol_match="base" lets one selector reach EURUSD, EURUSD.sd and
EURUSDm across brokers. kind defaults to position, so a routine close
does not also delete resting pending orders.
from fxsocket import CloseDefaults, CloseLeg
closed = fx.orders.close(
[
CloseLeg(account_id=accounts[0], side="long", volume=0.05), # partial
CloseLeg(account_id=accounts[1], tickets=[123456, 123457]), # explicit
accounts[2], # defaults only
],
defaults=CloseDefaults(symbol="EURUSD", symbol_match="base"),
idempotency_key="flatten-4711",
)
for leg in closed.results:
print(leg.account_id, leg.status, f"{leg.closed}/{leg.matched}")
for ticket in leg.results:
print(" ", ticket.ticket, ticket.status, ticket.retcode_description)
nothing_matched is a normal answer, not an error. Per-account status
compares against CloseLegStatus and per-ticket against
ClosedTicketStatus; skipped tickets were never sent, timeout ones may
well have closed. An idempotency_key matters most for partial closes,
where a blind retry genuinely over-closes.
Both calls need the full fxs_live_… key — a read-only key raises
ForbiddenError.
Streaming (WebSocket)
Subscribe to live ticks, bars, account, positions, trades, and terminal status.
Streaming is async-first; a synchronous wrapper is provided too. A dropped
connection auto-reconnects and replays active subscriptions
(auto_reconnect=True by default).
import asyncio
from fxsocket import AsyncClient, Tick, Bar, AccountUpdate
async def main():
async with AsyncClient(api_key="fxs_live_…") as fx:
account = await fx.accounts.get("…")
async with fx.stream(account) as s:
await s.subscribe_prices("EURUSD")
await s.subscribe_bars("EURUSD", "M5")
await s.subscribe_account()
async for event in s:
match event:
case Tick():
print(event.symbol, event.data.bid, event.data.ask)
case Bar():
print(event.symbol, event.timeframe, event.data.close)
case AccountUpdate():
print("equity", event.data.equity)
asyncio.run(main())
Synchronous equivalent:
from fxsocket import Client, Tick
with Client(api_key="fxs_live_…") as fx:
with fx.stream(fx.accounts.get("…")) as s:
s.subscribe_prices("EURUSD")
for event in s:
if isinstance(event, Tick):
print(event.data.bid, event.data.ask)
Trade events
A TradeUpdate carries the full deal: commission, swap, magic and a
real comment alongside profit (bridges MT5 0.12+ / MT4 0.11+; zero on
older pods). Event-only P&L accounting is data.net_profit
(profit + commission + swap).
Correlate the In and Out events of one round-trip through
data.position — on MT5, Out deals carry magic=0 / comment="" unless
the closing request set them (platform behavior, not a bridge gap), so
position id is the reliable join key. On MT4, deal is always 0 and
position equals the order ticket. The same id appears as position in
order_history() rows (bridges MT5 0.14+ / MT4 0.13+) and as
position_id in position_history().
If the bridge can't fully enrich an event in time it sets
data.degraded=True: identifiers, symbol, type, volume and price
are still trustworthy, but entry is "Unknown" and the cost fields are
zeroed — reconcile that deal via order_history().
async for event in s:
match event:
case TradeUpdate() as t if t.data.degraded:
reconcile_later(t.data.position) # costs/entry unreliable
case TradeUpdate() as t if t.data.entry == DealEntry.OUT:
print(t.data.position, "closed, net", t.data.net_profit)
Errors
Every failure raises a subclass of fxsocket.FxSocketError:
| Exception | When |
|---|---|
AuthError |
missing/invalid API key |
ForbiddenError |
key not allowed to do this (read-only key on a mutating call) |
RateLimitError |
rate limited (.retry_after) |
ValidationError |
malformed request (.code: invalid_batch, unknown_account, …) |
IdempotencyError |
batch refused because of its Idempotency-Key (.code) |
NotFoundError |
account/resource not found |
PaymentRequiredError |
base for every 402 below (plan / balance doesn't allow it) |
AccountCapError |
plan account limit reached (.cap, .current) |
NoSubscriptionError |
no plan permits linking accounts |
InsufficientBalanceError |
prepaid balance too low (.shortfall_eur_cents, .shortfall_eur) |
SeatLapsedError |
seats lapsed, existing accounts unseated — renew first |
DuplicateAccountError |
account already linked |
ConnectFailedError |
broker rejected the login |
TerminalNotReadyError |
terminal not provisioned / not ready |
UnsupportedOnPlatformError |
feature not available on this platform |
from fxsocket import Client, AccountCapError, InsufficientBalanceError
try:
fx.accounts.create(server="Demo", login=1, password="…")
except AccountCapError as e:
print(f"Plan limit reached: {e.current}/{e.cap}")
except InsufficientBalanceError as e:
print(f"Top up {e.shortfall_eur} EUR first") # None if the API gave no figure
Private hosting
Dedicated private servers are managed through client.private_servers:
import time
from fxsocket import Client, PrivateAccountStatus, SlotsFullError
with Client(api_key="fxs_live_...", verify_terminal_tls=False) as fx:
[server] = fx.private_servers.list()
print(server.name, server.status, f"{server.used_slots}/{server.purchased_slots}")
try:
account = fx.private_servers.add_account(
server, server="ICMarkets-Demo", login=1150125, password="..."
)
except SlotsFullError as err:
print(f"Server full ({err.used}/{err.cap}) — raise the limit in the dashboard.")
# Poll until the on-server agent has the terminal up, then trade as usual.
while True:
server = fx.private_servers.get(server)
account = next(a for a in server.accounts if a.id == account.id)
if account.status == PrivateAccountStatus.READY:
break
time.sleep(5)
print(fx.terminal(account).account_summary())
Accounts on a private server are traded and streamed exactly like
shared-cluster accounts — their rest_url / ws_url simply point at the
server's dedicated IP. The server presents a self-signed certificate, so reach
it with Client(..., verify_terminal_tls=False) (or supply a pinned CA).
Purchasing a server, canceling, and slot changes happen in the dashboard;
the API deliberately exposes no billing operations.
Wallet
fx.wallet.get() is a read-only view of your prepaid balance: what is in
it, top-ups that haven't landed yet, and what the balance will pay for over
the next 30 days (account seats and balance-funded private servers
together). Amounts are integer EUR cents; the *_eur properties give
Decimal euros.
wallet = fx.wallet.get()
print(f"balance {wallet.balance_eur} EUR, covers next 30 days: {wallet.covers_upcoming}")
for charge in wallet.upcoming:
print(f" {charge.when:%Y-%m-%d} {charge.kind:6} {charge.label} {charge.amount_eur} EUR")
if not wallet.covers_upcoming:
print(f"top up at least {wallet.shortfall_eur} EUR")
Affordability is cumulative — with 24 EUR and three 12 EUR renewals the
first two are covered and the third is not — so shortfall_eur is the
total gap, not the size of any single charge. Topping up happens in the
dashboard; the SDK deliberately exposes no payment operations.
Timestamps
Terminal timestamps (quote.time, candle time, order times) are returned as
strings in broker server time — not Python datetime. The trailing Z is
stylistic and does not mean UTC. Use terminal.server_timezone() to get the
broker's UTC offset if you need to convert.
Requirements
- Python 3.10+
httpx,pydantic≥ 2,websockets≥ 13
Links
- API reference: https://api.fxsocket.com/v1/docs
- Examples:
examples/
Development
pip install -e ".[dev]"
ruff check . && mypy && pytest
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fxsocket-0.6.0.tar.gz.
File metadata
- Download URL: fxsocket-0.6.0.tar.gz
- Upload date:
- Size: 63.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6bae261626ac3795356291f05938374fb4daa2dc21b7bcbbcdc75bbb2af5c65d
|
|
| MD5 |
2f8d2114b6593be1fe0c420dc9693342
|
|
| BLAKE2b-256 |
df46b18efeffa13fb102aa05d1ff8e307aa1abfd7d64104e78aae637c169705e
|
Provenance
The following attestation bundles were made for fxsocket-0.6.0.tar.gz:
Publisher:
publish.yml on fxsocket-com/FxSocket-Python-SDK
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fxsocket-0.6.0.tar.gz -
Subject digest:
6bae261626ac3795356291f05938374fb4daa2dc21b7bcbbcdc75bbb2af5c65d - Sigstore transparency entry: 2782919896
- Sigstore integration time:
-
Permalink:
fxsocket-com/FxSocket-Python-SDK@0dd208b559c5a258801d5252a7181f896cfd06a2 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/fxsocket-com
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0dd208b559c5a258801d5252a7181f896cfd06a2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file fxsocket-0.6.0-py3-none-any.whl.
File metadata
- Download URL: fxsocket-0.6.0-py3-none-any.whl
- Upload date:
- Size: 50.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2803c0b6c1324bdcfe24a692aab8901120ecb56d0d74e9d0772c78adeb7271a1
|
|
| MD5 |
3278133e62769bf84e692e44f50c5031
|
|
| BLAKE2b-256 |
74c1561104c22b53c32ae0870e7a08ea89e02b12dd18b202e42188341f2ea360
|
Provenance
The following attestation bundles were made for fxsocket-0.6.0-py3-none-any.whl:
Publisher:
publish.yml on fxsocket-com/FxSocket-Python-SDK
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fxsocket-0.6.0-py3-none-any.whl -
Subject digest:
2803c0b6c1324bdcfe24a692aab8901120ecb56d0d74e9d0772c78adeb7271a1 - Sigstore transparency entry: 2782919937
- Sigstore integration time:
-
Permalink:
fxsocket-com/FxSocket-Python-SDK@0dd208b559c5a258801d5252a7181f896cfd06a2 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/fxsocket-com
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0dd208b559c5a258801d5252a7181f896cfd06a2 -
Trigger Event:
push
-
Statement type: