StructureX

Home › Guides › Trading bots & code

Trading bot risk controls: the 7 guards to code before the strategy

A bot follows its rules perfectly — including the bad ones, at 3 a.m., a hundred times in a row. Risk controls are the rules that decide how much can go wrong before it stops.

Beginner guide · 12-minute read · Updated 2026-10-21

Why the guards come first

Strategy code decides when to trade. Risk code decides how much can go wrong.

A person who has a bad day usually notices and stops. A bot doesn't notice anything. It needs every "stop now" written down in code.

Here are 7 guards. Each one is short. Together they're often longer than the strategy itself — and that's normal.

Example code for learning — test it on paper first; it is not a ready-made trading system.

Guard 1: Size from the stop

Every trade risks a fixed slice of the balance. The distance to the Stop loss sets the size. A hard cap stops a tiny stop from creating a huge order.

Guard 2: A daily loss limit

After −2R in a day, the bot is done until tomorrow. R means one unit of risk, the amount lost if a stop is hit. That's our 2-loss rule: two full losses and you step away. More in daily loss limit.

Guard 3: A cap on trades per day

A bug or a choppy market can fire signal after signal. A cap of, say, 5 trades a day keeps a loop from running wild.

Guard 4: A kill switch

One way to stop everything at once, without touching the code. Here it's a file: create a file called STOP next to the bot and it halts until a human resets it.

Here are guards 1 to 4 in one small class:

import os
class RiskGuard:
    def __init__(self, balance, risk_pct=1.0, max_day_loss_r=2.0,
                 max_trades_day=5, max_units=10_000):
        self.balance, self.risk_pct = balance, risk_pct
        self.max_day_loss_r, self.max_trades_day = max_day_loss_r, max_trades_day
        self.max_units = max_units
        self.day, self.day_r, self.trades_today = None, 0.0, 0
        self.halted = False                      # only a human turns this back off
    def can_trade(self, today):
        if today != self.day:                    # new day: reset the daily tallies
            self.day, self.day_r, self.trades_today = today, 0.0, 0
        if os.path.exists("STOP"):
            self.halted = True
        if self.halted:
            return False, "kill switch is on"
        if self.day_r <= -self.max_day_loss_r:
            return False, "daily loss limit reached"
        if self.trades_today >= self.max_trades_day:
            return False, "trade cap for today reached"
        return True, "ok"
    def size(self, entry, stop):
        per_unit = abs(entry - stop)
        if per_unit <= 0:
            return 0
        units = int(self.balance * self.risk_pct / 100 // per_unit)
        return min(units, self.max_units)       # hard cap, whatever the math says
    def record(self, r):
        self.day_r += r
        self.trades_today += 1

Try it:

from datetime import date
guard = RiskGuard(balance=10_000)
print(guard.size(entry=50.0, stop=49.0))        # 1% of 10,000 = 100, 1 per unit: 100
today = date(2026, 10, 21)
print(guard.can_trade(today))                    # start of the day: (True, 'ok')
for r in (-1.0, -1.0):                           # two full losses
    guard.record(r)
print(guard.can_trade(today))                    # (False, 'daily loss limit reached')
print(guard.can_trade(date(2026, 10, 22)))       # new day: (True, 'ok')

Two losses of −1R each make −2R, so the bot stops for the day. The next morning the tally resets. A math example, not a promise. To check the size math by hand, use the position size calculator.

Guard 5: Check the data is fresh

If the data feed freezes, a bot can keep "seeing" an old price and trade on it. Before every decision, check how old the last finished candle is.

from datetime import datetime, timedelta, timezone
def data_is_fresh(last_close_time, candle_minutes=60, now=None):
    """False if the newest finished candle is older than 2 candles."""
    now = now or datetime.now(timezone.utc)
    return now - last_close_time <= timedelta(minutes=2 * candle_minutes)
t = datetime(2026, 10, 21, 14, 0, tzinfo=timezone.utc)
print(data_is_fresh(t, now=t + timedelta(minutes=30)))   # True
print(data_is_fresh(t, now=t + timedelta(hours=5)))      # False: feed looks stuck

Use one time zone everywhere. UTC is the usual choice. Mixing local time and exchange time is a classic bot bug.

Guard 6: Retry safely — and never send an order twice

Networks drop. The nasty case: you send an order, it goes through, but the reply gets lost. A simple "try again" would buy twice.

The fix is your own order ID. Many brokers and exchanges let you attach one (often called a client order ID). Make it once, reuse it on every retry. If the broker already has that ID, it's the same order, not a new one.

Here's a fake broker that loses the first reply, so we can test the fix:

import logging, time, uuid
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger("bot")
class FakeBroker:
    """Practice stand-in. Accepts the order, then 'loses' the first reply."""
    def __init__(self):
        self.orders, self.calls = {}, 0
    def place_order(self, client_id, side, qty):
        self.calls += 1
        if client_id in self.orders:              # seen this ID: same order, not a new one
            return self.orders[client_id]
        self.orders[client_id] = {"id": client_id, "side": side, "qty": qty}
        if self.calls == 1:
            raise ConnectionError("reply lost")   # the order went through anyway
        return self.orders[client_id]
def place_with_retries(broker, side, qty, tries=3, wait=0.1):
    client_id = str(uuid.uuid4())                 # made ONCE, reused on every retry
    for attempt in range(1, tries + 1):
        try:
            order = broker.place_order(client_id, side, qty)
            log.info(f"order ok {side} {qty} id={client_id[:8]}")
            return order
        except (ConnectionError, TimeoutError) as e:
            log.warning(f"attempt {attempt} failed: {e}")
            time.sleep(wait * attempt)            # wait a bit longer each time
    raise RuntimeError("gave up: check the account by hand")
broker = FakeBroker()
place_with_retries(broker, "buy", 100)
print("orders at broker:", len(broker.orders))    # 1, not 2

The first call "fails", the retry succeeds, and the broker still holds just one order. If every retry fails, the bot stops and asks for a human. It never guesses.

Guard 7: Sanity-check every order, and log it

Last line of defence. Before an order leaves, check it isn't absurd.

def order_looks_sane(qty, price, last_price, max_units=10_000, max_gap_pct=1.0):
    if qty <= 0 or qty > max_units:
        return False, "size out of range"
    if abs(price - last_price) / last_price * 100 > max_gap_pct:
        return False, "price too far from the market"
    return True, "ok"
print(order_looks_sane(100, 50.10, 50.00))    # (True, 'ok')
print(order_looks_sane(100, 55.00, 50.00))    # (False, 'price too far from the market')

And log everything: every signal, every skipped trade and why, every order, every fill, every error. When something odd happens, the log is the only witness. Our trading journal guide explains what's worth writing down.

Two guards that live at the broker

Some protection shouldn't depend on your bot staying alive.

What risk controls can't do

They keep losses to sizes you chose. They don't make a strategy good.

Prices can also jump past a stop on news or over a weekend — a Gap. Then a loss can be bigger than 1R. That's another reason to keep risk per trade small and to sit out big reports. And a bot needs watching: check the log every day.

Test the guards on purpose. Create the STOP file. Feed it an old timestamp. Make the fake broker fail. Each guard should stop the bot and say why in the log. If one doesn't, fix it before anything else.

Common mistakes

Questions

What is a kill switch in a trading bot?

A way to stop the bot at once without editing code — for example, a file, a button or a setting it checks before every trade. Once on, it stays on until a person turns it off.

How much should a trading bot risk per trade?

Many traders keep it around 1% of the account per trade, sized from the stop distance. Write the number into the code and cap the size too.

How do I stop my bot from placing duplicate orders?

Attach your own order ID, make it once, and reuse it on every retry. If the broker already has that ID, it treats it as the same order.

Do I need a daily loss limit if my bot uses stop losses?

Yes. Stops limit each trade. A daily limit stops a string of trades. Our rule is to stop for the day after 2 full losses.

Want the next step? We'll walk you through it.

Four free lessons by email, one every few days — each with a 10-minute exercise on a real chart.

Send Me the Free Lessons 4 short emails · No spam · Unsubscribe anytime

Or see the whole method, step by step:

See What’s Inside the Course Try everything free first. Decide when you're ready. · Open the free chart tool (works in the TradingView phone app)

One chart idea a day, in plain English. Follow along:

Follow on XFollow on TradingView