Docs

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 needEndpointScope
Everything the broker recorded: trades, commissions, swaps, depositsGET /v1/accounts/{id}/dealshistory:read
Open positions with floating profit, and how fresh they areGET /v1/accounts/{id}/positionstrading:read
Orders placed through fxapis, including closes you madeGET /v1/orders?accountId=…trading:read
Pull the latest deals and positions from the broker nowPOST /v1/accounts/{id}/reconciletrading:read
Bring accounts online so they can be readPOST /v1/accounts/prepareaccounts:write
Close positions on a breached accountPOST /v1/accounts/{id}/positions/{ticket}/closetrading: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 reconcile pulls the broker's deals from the last 24 hours on demand.
  • While offline, you read the last snapshot. Each position's observedAt says 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.

KeyScopesCanCannot
Dashboard / reportingaccounts:read, trading:read, history:readRead accounts, positions, orders, dealsChange anything
Monitorthe above, plus accounts:writeBring accounts online and reconcile themPlace, close or change any order
Enforcertrading:reduce, trading:readClose positions, move stops, cancel pending ordersOpen 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 dealType is balance, credit or bonus (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 + swap over 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
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_UNRESOLVED on 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 with GET /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.

On this page