# 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.

Source: https://tickwire.io/blog/prop-firm-drawdown-rules-in-code/

Tickwire blog, published 2026-10-20. General information, not investment advice.

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](https://tickwire.io/blog/broker-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.

```python
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 like
  `stop_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](https://tickwire.io/docs/guides/prop-risk-tool/). 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](https://tickwire.io/docs/guides/ai-agent/) and
[how to write a pre-trade hook](https://tickwire.io/blog/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](https://tickwire.io/docs/time-zones/).
