Funded account risk management: a dashboard with MT5 alerts

7 min readMetaKit

Three funded accounts at two firms. Three terminals, each showing equity to the cent. Not one of them shows the number that actually matters: how many dollars you are from today's daily drawdown limit, right now, on this account.

You end up doing the subtraction in your head, per account, during a trade. That's not funded account risk management, that's arithmetic under stress, and it's how people breach a limit they could recite from memory. The alternative takes an afternoon to build: one table across every account, distance to each limit in dollars and percent, and a Telegram message that arrives before the line, not after.

The data you need per account

The API gives you live equity and balance. It does not know your firm's rules, and no API does: the daily limit, the max limit, whether the max is static or trailing, and the balance the account was funded at all live in your contract. So the first step is writing them down somewhere a script can read.

{
  "2": { "name": "Firm A 100k", "initial_balance": 100000, "daily_pct": 5,
         "max_pct": 10, "anchor": "balance" },
  "5": { "name": "Firm B 50k",  "initial_balance": 50000,  "daily_pct": 4,
         "max_pct": 8,  "anchor": "equity" }
}

Keyed by account id. anchor is which figure the firm freezes at its daily reset: some use balance, some use equity, and the difference matters whenever you hold overnight. The drawdown rules post covers why.

Then there's the anchor value itself: the balance or equity at the firm's reset time today. The account object has a daily_profit field, but its day is not your firm's day, so capture the anchor yourself: read the account at the reset time and store the figure. The script below does that on its first run each day.

For a trailing max drawdown you also need peak equity since funding. Either track it in the same file, or let a drawdown monitor track it for you (it measures the fall from the highest equity since it was armed, which is the same shape).

Distance to limit, in dollars and percent

With those inputs, each limit is one line and the distance is one subtraction.

Daily floor: anchor × (1 − daily_pct / 100). Distance to daily: equity − daily floor. Share of today's allowance already used: (anchor − equity) ÷ (anchor × daily_pct / 100), floored at zero when you're up on the day.

Static max floor: initial_balance × (1 − max_pct / 100). Trailing max floor: peak − (peak × max_pct / 100), or peak minus a fixed dollar trail, depending on the firm.

The number to sort the table by is the smaller of the two distances. On a fresh account that's almost always the daily one. After a losing week it can flip, and the flip is exactly the moment people stop paying attention to the max line.

Equity monitor configuration for each limit

A dashboard you have to look at is half a system. The other half is a monitor that runs while you don't. Two monitors per account, one per limit, and each set inside the firm's number, not on it.

Daily drawdown. An equity monitor, comparator: "below", threshold at the daily floor plus a buffer. If the anchor is $100,000 and the daily limit is 5%, the floor is $95,000; alert at $96,000 and you have a full percent of room to close positions. Re-PATCH the threshold each day after you capture the new anchor.

curl -X POST https://api.metakit.cloud/v1/monitors \
  -H "Authorization: Bearer $METAKIT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "account_id": 2,
        "name": "Daily floor",
        "metric": "equity",
        "comparator": "below",
        "threshold": 96000,
        "rearm_mode": "once",
        "channel": "telegram",
        "channel_config": { "bot_token": "123456:ABC...", "chat_id": "-100123456789" }
      }'

Max drawdown from peak. A drawdown monitor. The threshold is a percent fall from the highest equity since arming, and the comparator is ignored (it always fires on the drawdown exceeding the threshold). With a 10% trailing limit, alert at 8. Use rearm_mode: "cooldown" with cooldown_minutes set to something like 30: sitting near the floor is exactly when you want to be nagged, and once goes quiet after the first message.

curl -X POST https://api.metakit.cloud/v1/monitors \
  -H "Authorization: Bearer $METAKIT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "account_id": 2,
        "name": "Trailing max",
        "metric": "drawdown",
        "threshold": 8,
        "rearm_mode": "cooldown",
        "cooldown_minutes": 30,
        "channel": "telegram",
        "channel_config": { "bot_token": "123456:ABC...", "chat_id": "-100123456789" }
      }'

One subtlety with the drawdown metric: the peak starts at arming time. Arm it after the account has already dipped from its high and your peak is lower than the firm's, which means your alert fires later than the firm's breach. Arm it at a fresh high, or set the threshold a couple of points tighter to compensate.

For a static max limit (fixed floor below the initial balance), skip the drawdown metric and use a second equity monitor below that floor.

The limits are per account: one monitor on a read-only account, three on a full account. If the account is read-only, spend the one monitor on the daily floor, since that's the line that moves every day, and leave the max line to the dashboard. Monitors are evaluated about every twenty seconds while the account is connected; while it isn't, evaluation is skipped and the monitor's state is left alone, so it picks up again on reconnect. Exact fields and the delivered payload are in the monitor reference.

Routing the MT5 drawdown alert to Telegram

The telegram channel takes a bot_token from BotFather and a chat_id, which for a group is a negative number. The alerts post walks through creating the bot and finding the chat id. Two things to know: channel_config is write-only, so a leaked API key can't read your bot token back out, and you should send a test the moment the monitor exists. A monitor with status: "error" means the last delivery failed; evaluation continues, but you won't hear about it until you fix the channel.

The dashboard script

This reads every connected account, joins it with your limits file, captures the day's anchor on first run, and prints a table sorted by the account closest to its daily floor. Run it at the firm's reset time (a cron entry in the firm's timezone) and again whenever you like.

import json
import os
from datetime import date
 
import requests
 
BASE = "https://api.metakit.cloud"
HEADERS = {"Authorization": f"Bearer {os.environ['METAKIT_KEY']}"}
 
with open("limits.json") as f:
    LIMITS = json.load(f)
 
ANCHORS_FILE = "anchors.json"
 
 
def all_accounts() -> list[dict]:
    out, page = [], 1
    while True:
        res = requests.get(
            f"{BASE}/v1/accounts",
            params={"page": page, "limit": 100},
            headers=HEADERS,
        ).json()
        out.extend(res["data"])
        if page >= res["total_pages"]:
            return out
        page += 1
 
 
try:
    with open(ANCHORS_FILE) as f:
        anchors = json.load(f)
except FileNotFoundError:
    anchors = {}
 
today = date.today().isoformat()
rows = []
 
for acct in all_accounts():
    lim = LIMITS.get(str(acct["id"]))
    if not lim or acct["status"] != "connected":
        continue
 
    # First run of the day freezes the firm's anchor (balance or equity).
    key = f"{acct['id']}:{today}"
    anchor = anchors.setdefault(key, acct[lim["anchor"]])
 
    equity = acct["equity"]
    daily_allow = anchor * lim["daily_pct"] / 100
    daily_floor = anchor - daily_allow
    max_floor = lim["initial_balance"] * (1 - lim["max_pct"] / 100)
 
    rows.append({
        "name": lim["name"],
        "equity": equity,
        "to_daily": equity - daily_floor,
        "daily_used": max(0.0, (anchor - equity) / daily_allow * 100),
        "to_max": equity - max_floor,
    })
 
with open(ANCHORS_FILE, "w") as f:
    json.dump(anchors, f)
 
print(f"{'account':<16}{'equity':>12}{'to daily':>12}{'used':>8}{'to max':>12}")
for r in sorted(rows, key=lambda r: r["to_daily"]):
    flag = "  <-- close something" if r["daily_used"] > 70 else ""
    print(
        f"{r['name']:<16}{r['equity']:>12,.0f}{r['to_daily']:>12,.0f}"
        f"{r['daily_used']:>7.0f}%{r['to_max']:>12,.0f}{flag}"
    )

Output looks like this on a mixed day:

account               equity    to daily    used      to max
Firm B 50k            48,610       1,110     45%       2,610
Firm A 100k          101,240       6,240      0%      11,240

Firm B has used 45% of today's allowance and is $1,110 from the floor. That row is the one you're managing; the other one can wait.

To turn it into a web page, render the same rows into an HTML table and serve the file. To put it somewhere your phone can see, push the rows to a sheet using the pattern in the Google Sheets post. The multi-account fetch is the same loop as the aggregate P&L post, which adds the per-account profit columns if you want them on the same screen.

What to do when it fires

Flatten. Close the positions on that account, then think.

The alert is set inside the limit for exactly one reason: so that closing still changes the outcome. If the daily alert is at 4% and the limit is 5%, you have one percent of adverse movement left, which on gold is a few minutes. "Wait for it to come back" spends that percent on hope, and a breach isn't a bad day, it's the account.

If the account is a copier follower, set the copier to off before you close, so the master doesn't reopen what you just flattened. Then reopen the limits file, and ask whether the size that got you here was ever right for the smaller account.