Coding prop firm drawdown rules correctly
Daily loss and maximum drawdown rules in code: balance or equity, static or trailing, when the trading day starts, buffers, and a Python check that knows when it doesn't know.
A prop firm is a company that gives traders an account to trade under a set of rules, and ends the account when a rule is broken. The rules that end most accounts are about losses: how much you may lose in one day, and how far the account may fall overall. If a bot trades the account, those rules have to live in code, and code gets them wrong in quiet ways.
The usual mistakes are small. The day starts at the wrong hour. The limit is measured on balance when the rule says equity. Open losses are ignored until a trade closes. The check runs on stale data and says “fine”. Each one can break a rule while the bot reports that everything is in order.
This post covers how these rules are usually built, where code goes wrong, and a Python check you can adapt. Rules differ between firms and change over time, so always read your own firm’s rules and treat everything here as a pattern, not a description of any one firm.
Two limits: daily loss and maximum drawdown
Most rule sets have two loss limits.
- A daily loss limit caps how much the account may lose within one trading day, for example 5% of the day’s starting level.
- A maximum drawdown caps how far the account may fall overall, for example 10% below a reference level.
Two questions decide what each limit means in practice.
Balance or equity? Balance is the account’s cash after closed trades. Equity is balance plus the profit or loss of open trades, also called floating profit or loss. A rule measured on equity can be broken by a trade that is still open. Some rules take the starting level as the higher of balance and equity at the start of the day, which matters when a trade is held overnight in profit.
Static or trailing? A static drawdown limit sits at a fixed level, usually a percentage below the starting balance, and never moves. A trailing limit follows the account’s highest point, its high-water mark, and stays a fixed amount below it. Some trailing limits stop moving once they reach the starting balance. A trailing limit on equity is the strictest: an open profit that comes back still lifts the limit.
| Rule type | Reference level | Moves? |
|---|---|---|
| Static, balance | Starting balance | Never |
| Trailing, balance | Highest closed balance | Up only |
| Trailing, equity | Highest equity seen | Up only, including open profit |
Write down which one applies before you write any code. Most bugs come from guessing.
When does the trading day start?
A daily limit resets once a day. The question is when. Most firms use the broker’s midnight: the moment the trading server’s clock reads 00:00. That server clock is usually not UTC. Many servers run two or three hours ahead of UTC, and the gap changes with daylight saving time, on dates that may not match your own country’s.
So “today’s” starting balance is the balance at broker midnight, which is a different UTC instant in summer and in winter. Code that resets at UTC midnight, or at the local midnight of the machine running the bot, compares against the wrong starting level for part of every day. Broker server time is not UTC explains how to find the offset.
Two rules follow:
- Never assume the offset. Measure it, and if it isn’t known, treat the limit as unknown.
- Store times in UTC and convert to broker time only to decide which day something belongs to.
Floating profit and loss, and a buffer
An equity rule is checked against open trades, which move with every price. A position that is down 4.8% against a 5% daily limit can cross the line between two checks, or during a fast move or a weekend gap. A stop loss (an order that closes a position at a set price) limits this but does not remove it: in a gap, the stop fills at the next available price.
So don’t act at the limit. Act before it:
- Set a buffer, for example one percentage point, and stop opening new trades once equity is inside it.
- Count the worst case of open positions, not just their current value. A position with a stop loss can lose roughly the distance to its stop, plus slippage. A position without one has no ceiling.
- Count costs. Commission and swap (the overnight financing charge) reduce equity too.
Computing today’s limit in Python
The example reads the account snapshot, which returns account information and the broker clock in
one call, each with its own status. It works out broker midnight in UTC, rebuilds the balance at
that moment from today’s deals, and compares current equity with the limit. It uses requests and
the paged history route, where times are UTC and limit is at most 1000.
import requests
from datetime import datetime, timedelta, timezone
TW = "https://api.tickwire.io"
KEY = "tw_live_..."
ACC = "<ACCOUNT_ID>"
DAILY_LOSS_PCT = 5.0 # your rule: percent of the start-of-day balance
BUFFER_PCT = 1.0 # stop opening this far before the limit
def get(path, **params):
r = requests.get(f"{TW}/users/current/accounts/{ACC}{path}",
headers={"auth-token": KEY}, params=params, timeout=30)
r.raise_for_status()
return r.json()
def iso(t):
return t.strftime("%Y-%m-%dT%H:%M:%SZ")
def broker_midnight_utc(clock):
offset = clock.get("brokerUtcOffsetMinutes")
if offset is None:
return None # not measured yet: never guess
broker_now = datetime.fromisoformat(clock["brokerTime"][:19])
midnight = broker_now.replace(hour=0, minute=0, second=0, microsecond=0)
return (midnight - timedelta(minutes=offset)).replace(tzinfo=timezone.utc)
def balance_change_since(start, end):
total, offset = 0.0, 0
while True:
page = get(f"/history-deals/time/{iso(start)}/{iso(end)}", offset=offset, limit=1000)
for d in page: # every deal moves the balance
total += sum(d.get(k) or 0.0 for k in ("profit", "swap", "commission", "fee"))
if len(page) < 1000:
return total
offset += len(page)
def daily_limit_state():
snap = get("/snapshot")
if not snap["complete"]:
return "unknown", None # a section is stale or unavailable
clock = snap["server_time"]["data"]
info = snap["account_information"]["data"]
start = broker_midnight_utc(clock)
if start is None:
return "unknown", None
now = datetime.fromisoformat(clock["time"].replace("Z", "+00:00"))
start_balance = info["balance"] - balance_change_since(start, now + timedelta(minutes=1))
limit = start_balance * (1 - DAILY_LOSS_PCT / 100)
buffer = start_balance * BUFFER_PCT / 100
if info["equity"] <= limit:
return "breached", limit
if info["equity"] <= limit + buffer:
return "stop_opening", limit
return "ok", limit
try:
state, limit = daily_limit_state()
except requests.RequestException:
state, limit = "unknown", None # can't read the account: assume the worst
print(state, limit)
Notes on what it does and doesn’t do:
- Unknown is a real answer. If any part of the snapshot is stale, the clock offset isn’t measured
yet, or a request fails, the result is
unknown, and a bot should treat that likestop_opening. - Deposits and withdrawals made today also change the balance, so this rebuilds the true balance at midnight. If your rule ignores money movements, subtract only trade deals.
- An equity-based start can’t be rebuilt from history, because history doesn’t record open profit at midnight. Record equity yourself when the broker date changes, and keep it.
- Maximum drawdown works the same way with a different reference level: the starting balance for a static rule, or a high-water mark you store and only ever raise for a trailing one.
What to do near the limit, and how to test it
Decide each action in advance:
- Inside the buffer: refuse new trades. Allow trades that only reduce risk, such as closing a position or tightening a stop.
- At or past the limit: the account may already have failed the rule. Close what your rules say to close, stop the bot, and alert a person.
- Unknown: don’t open anything until the check works again.
Test the rule before it guards real money:
- Feed the check fixed snapshots and deal lists: a day starting in profit, a deposit at noon, a position open across midnight, and a clock with no measured offset.
- Test the hours around broker midnight in both summer and winter offsets.
- Practise on a demo account before you risk money, and force a breach on purpose to see what the bot does.
Checklist
- Write down: daily or overall, balance or equity, static or trailing, and the reference level.
- Reset the day at broker midnight, using a measured offset.
- Check equity, including open losses and costs, with a buffer before the limit.
- Treat stale data and an unknown clock as “don’t open”.
- Allow closing and risk-reducing changes even when blocking new trades.
- Store the start-of-day equity and the high-water mark yourself if your rule needs them.
Doing this with Tickwire
Tickwire never blocks an order unless you set a limit. You choose where the rule lives. It can be in your own code, as above. It can be a Tickwire risk tracker, which watches daily and maximum drawdown per account by broker-time day and sends a webhook on a breach; see the prop risk tool guide. Or, when an AI agent trades the account, it can be your pre-trade hook: your own HTTPS endpoint that answers allow, deny or hold for every agent order before it reaches the broker. Requests are signed with HMAC, and if the hook can’t be reached the order is not sent. A hook can run the check above and deny new openings while still allowing closes. See the AI agent guide and how to write a pre-trade hook.
Broker time is labelled as broker time and never assumed to be UTC. The snapshot reports the
measured offset, or null until it is known; see time zones.
Trading carries risk, and you can lose money. This post explains software and how it behaves; it is not investment advice.