Monitor MT5 trader accounts for prop-firm rules
Use deals, positions and order history from the fxapis MT5 API to check prop-firm rules such as daily loss and maximum drawdown on your side, with read-only and reduce-only API keys, and close every position on an account that breaches.
Prop firms, funded-trader programs and risk desks need to watch many MT5 accounts that other people trade, check each against rules — a daily loss limit, a maximum drawdown, a minimum number of trading days — and act when an account breaches. This guide shows how to do that with fxapis: what to read, how to compute the rules on your side, how to keep the keys narrow, and how to close out a breached account.
It also says plainly what fxapis does not do, because a prop firm's risk system has to know where its tools end.
What fxapis gives you
| You need | Endpoint | Scope |
|---|---|---|
| Everything the broker recorded: trades, commissions, swaps, deposits | GET /v1/accounts/{id}/deals | history:read |
| Open positions with floating profit, and how fresh they are | GET /v1/accounts/{id}/positions | trading:read |
| Orders placed through fxapis, including closes you made | GET /v1/orders?accountId=… | trading:read |
| Pull the latest deals and positions from the broker now | POST /v1/accounts/{id}/reconcile | trading:read |
| Bring accounts online so they can be read | POST /v1/accounts/prepare | accounts:write |
| Close positions on a breached account | POST /v1/accounts/{id}/positions/{ticket}/close | trading:reduce |
The trader keeps trading from their own MetaTrader terminal. Their deals appear
in fxapis's deal history like any other — a deal with no fxapis order behind it
has orderId: null — so the history is complete whoever placed the trade.
Keep the accounts readable
fxapis reads an account through a MetaTrader 5 terminal we run for it, logged in with the account's trading password. What you can read depends on whether that account is online:
- While online, positions refresh about every 15 seconds on their own, and
reconcilepulls the broker's deals from the last 24 hours on demand. - While offline, you read the last snapshot. Each position's
observedAtsays how old it is.
For accounts under active rule enforcement, connect them with
"mode": "always_on" (a plan feature) so the view is never more than seconds
old. For end-of-day checks, warm_on_demand is enough: prepare the accounts
(up to 200 per call), wait for ready, reconcile, read, and let them go
offline by themselves after 15 idle minutes.
Call reconcile at least every few hours for each account you track: each
call pulls the broker's deals from the last 24 hours, so a longer gap can leave
deals out of your copy of the history.
Keys: read-only, reduce-only, and the one in between
Scopes are per key; issue each part of your system only what it needs. See Authentication → Scopes.
| Key | Scopes | Can | Cannot |
|---|---|---|---|
| Dashboard / reporting | accounts:read, trading:read, history:read | Read accounts, positions, orders, deals | Change anything |
| Monitor | the above, plus accounts:write | Bring accounts online and reconcile them | Place, close or change any order |
| Enforcer | trading:reduce, trading:read | Close positions, move stops, cancel pending orders | Open any exposure |
reconcile needs only trading:read: it asks the broker what happened and
updates our records, and places nothing. So the monitor key can bring accounts
online and read them, and can never trade. The enforcer key is the one to hand
to anything that acts on a breach: even if it were stolen, it can only reduce
risk.
Computing the rules on your side
fxapis reports deals and positions, not balance or equity, and it does not know your rules. You compute them — which is also what lets you define "day", "drawdown" and "trading day" exactly as your terms of business do.
From the deals:
- Trading result of a deal =
profit + swap + commission + fee, for every deal except funding. - Funding = deals whose
dealTypeisbalance,creditorbonus(deposits, withdrawals, credits). They move the balance but are not trading results. - Balance = your recorded starting balance + funding + trading results since the start.
From the positions:
- Floating result = Σ
profit + swapover open positions. - Equity = balance + floating result.
Then, for example:
- Daily loss = balance at the start of the day − current equity. Breached if above the daily limit.
- Maximum drawdown (static) = starting balance − current equity. Breached if above the limit.
- Trailing drawdown = highest equity you have recorded − current equity. You keep the peak.
Two cautions. A position row's observedAt is part of the answer — do not
enforce a rule on a snapshot that is minutes old; bring the account online
first. And your day boundary must match your terms: broker server midnight and
UTC midnight are usually different.
A monitor you can run
This Python script checks a list of accounts against a daily loss limit and a
static maximum drawdown, and — only when ENFORCE=1 — closes every open
position on an account that breaches, using the reduce-only key.
pip install requests
export FXAPIS_MONITOR_KEY="fx_live_…" # accounts:read/write, trading:read, history:read
export FXAPIS_ENFORCER_KEY="fx_live_…" # trading:reduce, trading:read
# account id = starting balance, ISO date the challenge started
export ACCOUNTS="0d7c1f5e-4b6a-4c1e-9f0a-2d9e8c7b6a51=100000@2026-09-01"
export DAILY_LOSS_LIMIT="0.05" MAX_DRAWDOWN_LIMIT="0.10"
python3 monitor.py"""Check MT5 accounts against a daily loss limit and a maximum drawdown.
Rules are computed here, on your side. fxapis supplies the broker's deals and positions.
With ENFORCE=1, a breached account has every open position closed (reduce-only key).
"""
import os
import time
from datetime import datetime, timezone
from decimal import Decimal
import requests
API = os.environ.get("FXAPIS_API", "https://api.fxapis.com")
FUNDING = {"balance", "credit", "bonus"}
DAILY_LOSS_LIMIT = Decimal(os.environ.get("DAILY_LOSS_LIMIT", "0.05"))
MAX_DRAWDOWN_LIMIT = Decimal(os.environ.get("MAX_DRAWDOWN_LIMIT", "0.10"))
ENFORCE = os.environ.get("ENFORCE") == "1"
def client(key_env):
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ[key_env]}"
return s
monitor = client("FXAPIS_MONITOR_KEY")
enforcer = client("FXAPIS_ENFORCER_KEY") if ENFORCE else None
def data(response):
if response.status_code >= 300:
error = response.json().get("error", {})
raise RuntimeError(f"{response.status_code} {error.get('code')}: {error.get('message')}")
return response.json()
def all_deals(account_id, since):
deals, cursor = [], None
while True:
params = {"since": since, "limit": 200}
if cursor:
params["cursor"] = cursor
body = data(monitor.get(f"{API}/v1/accounts/{account_id}/deals", params=params, timeout=60))
deals.extend(body["data"])
if not body["page"]["hasMore"]:
return deals
cursor = body["page"]["nextCursor"]
def amount(value):
return Decimal(value) if value not in (None, "") else Decimal(0)
def trading_result(deal):
return amount(deal["profit"]) + amount(deal["swap"]) + amount(deal["commission"]) + amount(deal["fee"])
def evaluate(starting_balance, deals, positions, day_start):
"""Pure: the numbers a rule needs, from deals and positions."""
balance = start_of_day = starting_balance
for deal in deals:
change = amount(deal["profit"]) if deal["dealType"] in FUNDING else trading_result(deal)
balance += change
if deal["dealtAt"] < day_start:
start_of_day += change
floating = sum((amount(p["profit"]) + amount(p["swap"]) for p in positions), Decimal(0))
equity = balance + floating
return {
"balance": balance,
"equity": equity,
"daily_loss": start_of_day - equity,
"drawdown": starting_balance - equity,
"oldest_position": min((p["observedAt"] for p in positions), default=None),
}
def ensure_online(account_ids, timeout=120):
data(monitor.post(f"{API}/v1/accounts/prepare", json={"accountIds": account_ids}, timeout=60))
deadline = time.monotonic() + timeout
waiting = set(account_ids)
while waiting and time.monotonic() < deadline:
for account_id in list(waiting):
state = data(monitor.get(f"{API}/v1/accounts/{account_id}/status", timeout=30))["data"]["state"]
if state in ("ready", "executing"):
waiting.discard(account_id)
elif state in ("invalid_credentials", "trading_disabled", "needs_2fa", "needs_certificate"):
print(f"{account_id}: needs attention ({state}); not checked")
waiting.discard(account_id)
time.sleep(2)
return waiting # accounts that did not come online in time
def close_everything(account_id, positions):
for p in positions:
ticket = p["brokerPositionId"]
r = enforcer.post(f"{API}/v1/accounts/{account_id}/positions/{ticket}/close", json={},
headers={"Idempotency-Key": f"breach:{account_id}:{ticket}"}, timeout=90)
if r.status_code == 201:
print(f" closed #{ticket}")
continue
code = r.json().get("error", {}).get("code")
if code == "POSITION_NOT_FOUND":
print(f" #{ticket} already closed")
elif code == "ORDER_UNRESOLVED":
print(f" #{ticket}: close unresolved — do not resend; it is being confirmed with the broker")
else:
print(f" #{ticket}: close failed with {code}; retry with the same key")
def main():
accounts = {}
for item in os.environ["ACCOUNTS"].split(","):
account_id, rest = item.split("=")
balance, started = rest.split("@")
accounts[account_id] = (Decimal(balance), started + "T00:00:00Z")
not_online = ensure_online(list(accounts))
day_start = datetime.now(timezone.utc).strftime("%Y-%m-%dT00:00:00Z") # your terms may differ
for account_id, (starting_balance, started) in accounts.items():
if account_id in not_online:
print(f"{account_id}: not online, skipped")
continue
data(monitor.post(f"{API}/v1/accounts/{account_id}/reconcile", timeout=60))
deals = all_deals(account_id, started)
positions = data(monitor.get(f"{API}/v1/accounts/{account_id}/positions", timeout=30))["data"]
m = evaluate(starting_balance, deals, positions, day_start)
breaches = []
if m["daily_loss"] > starting_balance * DAILY_LOSS_LIMIT:
breaches.append(f"daily loss {m['daily_loss']:.2f}")
if m["drawdown"] > starting_balance * MAX_DRAWDOWN_LIMIT:
breaches.append(f"drawdown {m['drawdown']:.2f}")
print(f"{account_id}: balance {m['balance']:.2f} equity {m['equity']:.2f} "
f"positions {len(positions)} (oldest {m['oldest_position']})"
+ (f" BREACH: {', '.join(breaches)}" if breaches else ""))
if breaches and ENFORCE:
close_everything(account_id, positions)
if __name__ == "__main__":
main()The starting balance is the account's balance at the start date you give it, after any initial deposit — every deal from that moment on is added to it. Choose a start date no earlier than when you connected the account to fxapis: deals from before then may not be in its history.
Run it on a schedule — every minute for always_on accounts, or once at the
end of your trading day for the others — and store what it computes, so you
have a record of every check alongside fxapis's own history.
Closing out a breached account
close_everything closes each open position with the enforcer key and an
idempotency key per position (breach:{account}:{ticket}), so running the
monitor twice never closes twice. On a hedging account a repeated close would
otherwise open an opposite position. Closing is never refused for plan or quota
reasons.
ORDER_UNRESOLVEDon a close means we do not yet know if it executed: do not resend it. Poll the order it names, or run the monitor again once it settles.- Pending orders placed through fxapis can be cancelled with
POST /v1/orders/{id}/cancel; list them withGET /v1/orders?accountId=…&state=working. Pending orders the trader placed from their own terminal are not fxapis orders and cannot be cancelled this way.
What fxapis does not do
- No MT5 Manager API. fxapis works with ordinary trading accounts through their login and password. It cannot create accounts, change leverage, disable trading, reset passwords, or read accounts in bulk at the server level.
- No account creation at the broker. You or the broker create the MT5 account; you connect it to fxapis afterwards.
- It cannot stop the trader. Closing positions does not prevent the trader from opening new ones from their terminal. Disabling the account is done at the broker or MT5 server.
- No rule engine. Limits are computed by your code, as above. fxapis supplies the facts.
- No balance or equity field. Derive them from deals and positions, from a starting balance you know.
- No push alerts. There are no event webhooks yet; poll on a schedule.
- MT5 only. MT4 accounts cannot be connected.
Related
- Connect MT5 accounts — connecting accounts at scale, and their states.
- Idempotent MT5 orders — why each close carries a key.
- Trading → History — orders, deals and positions compared.
- Security — how account passwords are stored.
MCP server: let Claude, Cursor, VS Code and other AI agents trade MT5
Connect an AI assistant or agent to fxapis over the Model Context Protocol. Setup for Claude, Claude Code, Claude Desktop, Cursor, VS Code and the OpenAI and Anthropic APIs, the full tool reference, and the safety model for letting an agent near a real MetaTrader 5 account.
Migrating from MetaApi to fxapis
A concept-by-concept guide for moving an MT5 integration from MetaApi to fxapis: provisioning and deploy versus connect and modes, RPC and streaming versus REST and polling, a table of equivalent calls, and what has no equivalent yet — streaming, MT4 and CopyFactory features.