Skip to content

Risk Management for Algorithmic Trading

Position sizing formulas, maximum drawdown limits, correlation risk, and building risk controls into your EAs.

💡
Before you start

Python 3 and a terminal. No MetaTrader, no broker, no account and no money. macOS and Linux already include Python; on Windows install it from python.org with “Add python.exe to PATH” ticked, then check with python3 --version. No C++ compiler is needed — this page is arithmetic, not MQL5.

You need no maths beyond percentages. Every formula is written out in code you can read, and the probabilities in step 3 are computed exactly rather than simulated, so they are the same on every machine.

Nothing here is trading or financial advice. These are calculations about how losses compound and how often streaks occur; what you do with them is your decision, and the numbers used as examples are illustrative.

Why Risk Management is Everything

A strategy with a 90% win rate can still blow up an account if risk management is wrong. Conversely, a strategy with only a 40% win rate can be consistently profitable with proper position sizing. Risk management is not optional — it is the single most important factor in long-term trading survival.

As an algorithmic trader, you have an advantage: you can hardcode risk rules into your EA, making them impossible to override in the heat of the moment.

The 1-2% Rule

Never risk more than 1-2% of your account on a single trade. This ensures that a string of losses (which is inevitable) does not destroy your account:

// With 1% risk, you can survive 100 consecutive losses
// and still have 36.6% of your account left
// With 5% risk, 100 losses leaves you with 0.59%

input double RiskPercent = 1.0;  // Risk Per Trade (%)

Position Sizing Formula

Calculate lot size based on your risk percentage and stop-loss distance:

double CalculatePositionSize(double stopLossPoints)
{
    double balance = AccountInfoDouble(ACCOUNT_BALANCE);
    double riskAmount = balance * RiskPercent / 100.0;

    // Get tick value (profit per 1 point movement per 1 lot)
    double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
    double tickSize  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);

    if(tickValue == 0 || stopLossPoints == 0) return 0;

    double pointValue = tickValue / tickSize * _Point;
    double lotSize = riskAmount / (stopLossPoints * pointValue);

    // Normalize to broker's lot step
    double minLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
    double maxLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
    double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);

    lotSize = MathFloor(lotSize / lotStep) * lotStep;
    lotSize = MathMax(lotSize, minLot);
    lotSize = MathMin(lotSize, maxLot);

    return NormalizeDouble(lotSize, 2);
}

Maximum Drawdown Protection

Automatically stop trading if the account drawdown exceeds a threshold:

input double MaxDrawdownPercent = 15.0;  // Max Drawdown (%)

double peakBalance = 0;

bool IsDrawdownExceeded()
{
    double balance = AccountInfoDouble(ACCOUNT_BALANCE);
    double equity  = AccountInfoDouble(ACCOUNT_EQUITY);

    // Track peak balance
    if(balance > peakBalance) peakBalance = balance;

    // Calculate current drawdown
    double drawdown = (peakBalance - equity) / peakBalance * 100.0;

    if(drawdown >= MaxDrawdownPercent)
    {
        Print("MAX DRAWDOWN REACHED: ", DoubleToString(drawdown, 1),
              "% - Trading paused");
        return true;
    }
    return false;
}

void OnTick()
{
    if(IsDrawdownExceeded()) return;  // stop all trading

    // ... normal trading logic
}

Daily Loss Limit

input double MaxDailyLossPercent = 3.0;  // Max Daily Loss (%)

double dailyStartBalance = 0;
datetime lastDayChecked = 0;

bool IsDailyLossExceeded()
{
    // Reset at the start of each day
    MqlDateTime dt;
    TimeCurrent(dt);
    datetime today = StringToTime(IntegerToString(dt.year) + "." +
                     IntegerToString(dt.mon) + "." +
                     IntegerToString(dt.day));

    if(today != lastDayChecked)
    {
        dailyStartBalance = AccountInfoDouble(ACCOUNT_BALANCE);
        lastDayChecked = today;
    }

    double equity = AccountInfoDouble(ACCOUNT_EQUITY);
    double dailyLoss = (dailyStartBalance - equity) / dailyStartBalance * 100;

    return (dailyLoss >= MaxDailyLossPercent);
}

Correlation Risk

If your EA trades multiple correlated pairs (e.g., EURUSD and GBPUSD), a loss on one is likely to coincide with a loss on the other. This effectively doubles your risk.

  • Reduce position size when trading correlated instruments
  • Set a maximum total exposure across all positions
  • Consider using a portfolio-level risk budget rather than per-trade risk

Compute the Risk Rules Instead of Repeating Them, in Four Steps

“Never risk more than 1–2% per trade” is the most repeated advice in trading and one of the least often justified. It has a justification, it is arithmetic rather than opinion, and you can compute it in ten minutes. In the next half hour you will work out why large losses are different in kind from small ones rather than merely worse, calculate exactly how likely a painful losing streak is in a system that wins, find the exposure that hides behind five separate “small” positions, and write the four checks an Expert Advisor can enforce before it sends anything. Every line of output below came from running these files.

1
Work out why a big loss is different in kind

Go: open a terminal in a folder you can write to — cd ~/Desktop on macOS or Linux, cd %USERPROFILE%\Desktop on Windows.

Do: save this as recover.py and run python3 recover.py.

"""The arithmetic that makes large losses different in kind, not degree."""

print("%10s %20s" % ("LOSS", "GAIN NEEDED TO RECOVER"))
print("-" * 34)
for loss in (5, 10, 20, 30, 40, 50, 60, 75, 90):
    remaining = 1.0 - loss / 100.0
    needed = (1.0 / remaining - 1.0) * 100.0
    print("%9d%% %19.1f%%" % (loss, needed))

print()
print("A 50% loss needs a 100% gain. A 90% loss needs 900%.")
print()
print("This is not a psychological point. It is division: after losing half")
print("your money you are compounding from half as much, so the same")
print("percentage gain returns half as much money.")
print()
print("It is also why 'risk 2% per trade' is not timidity. Ten consecutive")
print("2% losses leave you needing:")
eq = 1.0
for i in range(10):
    eq *= 0.98
print("   equity after 10 x 2%% losses : %.1f%% of the start" % (eq * 100))
print("   gain needed to recover      : %.1f%%" % ((1.0 / eq - 1.0) * 100))
print()
eq = 1.0
for i in range(10):
    eq *= 0.90
print("and ten consecutive 10% losses:")
print("   equity after 10 x 10%% losses: %.1f%% of the start" % (eq * 100))
print("   gain needed to recover      : %.1f%%" % ((1.0 / eq - 1.0) * 100))
print()
print("Ten losing trades in a row is not unusual. The previous page measured")
print("an eight-trade losing run in an ordinary, profitable system.")

You should see: the recovery requirement rising far faster than the loss:

      LOSS GAIN NEEDED TO RECOVER
----------------------------------
        5%                 5.3%
       10%                11.1%
       20%                25.0%
       30%                42.9%
       40%                66.7%
       50%               100.0%
       60%               150.0%
       75%               300.0%
       90%               900.0%

A 50% loss needs a 100% gain. A 90% loss needs 900%.

This is not a psychological point. It is division: after losing half
your money you are compounding from half as much, so the same
percentage gain returns half as much money.

It is also why 'risk 2% per trade' is not timidity. Ten consecutive
2% losses leave you needing:
   equity after 10 x 2% losses : 81.7% of the start
   gain needed to recover      : 22.4%

and ten consecutive 10% losses:
   equity after 10 x 10% losses: 34.9% of the start
   gain needed to recover      : 186.8%

Ten losing trades in a row is not unusual. The previous page measured
an eight-trade losing run in an ordinary, profitable system.

A 50% loss needs a 100% gain; a 90% loss needs 900%. That is not a psychological observation, it is division — after losing half your money you compound from half as much, so the same percentage returns half the money.

The two blocks at the bottom are the actual argument for small position sizes. Ten consecutive 2% losses leave 81.7% of the account and need 22.4% to recover, which is a bad quarter. Ten consecutive 10% losses leave 34.9% and need 186.8%, which for most people is the end of the account — not because the strategy was worse, but because the same ten losses were sized differently.

And ten losses in a row is not a disaster scenario. It is the subject of the next step.

If not: python3: command not found on Windows means Python was installed without “Add python.exe to PATH”; try py recover.py. The arithmetic is exact, so your figures will match this page.

2
Calculate how likely a painful streak really is

Go: the same folder.

Do: save this as streak.py and run python3 streak.py.

"""How likely is a losing run long enough to hurt? Compute it."""
from math import comb

def p_streak_at_least(n_trades, k, p_loss):
    """Probability of at least one run of k losses in n trades (simulation-free bound
    via a recurrence -- exact, not an estimate)."""
    # dp[i] = probability that NO run of k has occurred in the first i trades,
    # tracked with the standard 'run-length' recurrence.
    # state: probability of ending with j consecutive losses, j = 0..k-1
    state = [0.0] * k
    state[0] = 1.0
    for _ in range(n_trades):
        nxt = [0.0] * k
        for j in range(k):
            if state[j] == 0.0:
                continue
            nxt[0] += state[j] * (1 - p_loss)          # a win resets the run
            if j + 1 < k:
                nxt[j + 1] += state[j] * p_loss        # another loss extends it
            # j + 1 == k means the run happened; that probability leaves `state`
        state = nxt
    return 1.0 - sum(state)

print("A system that wins 55 per cent of the time -- so loses 45 per cent.")
print()
print("%8s %10s %26s" % ("TRADES", "RUN OF", "CHANCE IT HAPPENS"))
print("-" * 48)
for n in (100, 500, 1000):
    for k in (5, 8, 10, 12):
        print("%8d %10d %25.1f%%" % (n, k, 100 * p_streak_at_least(n, k, 0.45)))
    print()

print("Read the 1000-trade block. A run of FIVE is certain. A run of EIGHT")
print("happens more often than not (60.4%). A run of TEN is 17.0% -- unlikely")
print("on any given account, and near-inevitable across many accounts or many")
print("years.")
print()
print("All of that in a system that WINS more often than it loses.")
print()
print("So the question is never 'will I have a losing streak'. It is 'what")
print("will my account look like when I do', and that is decided entirely by")
print("the size risked on each one -- which is the only part you control.")

You should see: a five-loss run being certain over a thousand trades:

A system that wins 55 per cent of the time -- so loses 45 per cent.

  TRADES     RUN OF          CHANCE IT HAPPENS
------------------------------------------------
     100          5                      64.7%
     100          8                       8.4%
     100         10                       1.7%
     100         12                       0.3%

     500          5                      99.5%
     500          8                      36.9%
     500         10                       8.8%
     500         12                       1.8%

    1000          5                     100.0%
    1000          8                      60.4%
    1000         10                      17.0%
    1000         12                       3.7%

Read the 1000-trade block. A run of FIVE is certain. A run of EIGHT
happens more often than not (60.4%). A run of TEN is 17.0% -- unlikely
on any given account, and near-inevitable across many accounts or many
years.

All of that in a system that WINS more often than it loses.

So the question is never 'will I have a losing streak'. It is 'what
will my account look like when I do', and that is decided entirely by
the size risked on each one -- which is the only part you control.

These are exact probabilities from a recurrence, not a simulation, so they are the same every run. Over a thousand trades in a system that wins 55% of the time: a run of five is certain, a run of eight happens more often than not, and a run of ten — 17% — is unlikely for you personally and close to inevitable across many traders or many years.

Notice the win rate. This is a profitable system. Losing streaks are not evidence that something has broken; they are a property of any process with randomness in it, and the only thing you decide is what your account looks like afterwards.

Which reframes the standard advice usefully. “Risk 2%” is not caution — it is the size at which the streak the arithmetic guarantees remains survivable.

If not: if every probability prints 0 or 100, the loss probability was set to 0 or 1. The recurrence tracks the chance of having no run yet, so the answer is one minus the sum of the remaining states.

3
Find the exposure hiding behind five small positions

Go: the same folder.

Do: save this as correlation.py and run python3 correlation.py.

"""Five 'independent' 2% risks that are really one 10% risk."""

POSITIONS = [
    ("EURUSD buy",  "long EUR, short USD", 2.0),
    ("GBPUSD buy",  "long GBP, short USD", 2.0),
    ("AUDUSD buy",  "long AUD, short USD", 2.0),
    ("NZDUSD buy",  "long NZD, short USD", 2.0),
    ("USDCHF sell", "long CHF, short USD", 2.0),
]

print("%-14s %-24s %8s" % ("POSITION", "WHAT IT REALLY IS", "RISK"))
print("-" * 50)
for name, what, risk in POSITIONS:
    print("%-14s %-24s %7.1f%%" % (name, what, risk))

total = sum(p[2] for p in POSITIONS)
print()
print("positions               : ", len(POSITIONS))
print("risk per position       :  2.0%")
print("stated total risk       : ", "%.1f%%" % total)
print()
print("Every one of them is short the US dollar.")
print()
print("If the dollar strengthens, all five lose together. The 'diversified'")
print("book is one trade in five costumes, and the real exposure is %.1f%%"
      % total)
print("of the account on a single macroeconomic event.")
print()
print("A rule of 2 per cent per position is meaningless without a second rule")
print("about correlated exposure. The usual form is a cap on total risk --")
print("say 6 per cent open at once -- and a cap per currency or per sector.")
print()
print("The uncomfortable part: correlations RISE in a crisis. Instruments")
print("that normally move independently move together precisely when several")
print("of your positions are already losing.")

You should see: five 2% positions that are one 10% bet:

POSITION       WHAT IT REALLY IS            RISK
--------------------------------------------------
EURUSD buy     long EUR, short USD          2.0%
GBPUSD buy     long GBP, short USD          2.0%
AUDUSD buy     long AUD, short USD          2.0%
NZDUSD buy     long NZD, short USD          2.0%
USDCHF sell    long CHF, short USD          2.0%

positions               :  5
risk per position       :  2.0%
stated total risk       :  10.0%

Every one of them is short the US dollar.

If the dollar strengthens, all five lose together. The 'diversified'
book is one trade in five costumes, and the real exposure is 10.0%
of the account on a single macroeconomic event.

A rule of 2 per cent per position is meaningless without a second rule
about correlated exposure. The usual form is a cap on total risk --
say 6 per cent open at once -- and a cap per currency or per sector.

The uncomfortable part: correlations RISE in a crisis. Instruments
that normally move independently move together precisely when several
of your positions are already losing.

Every position in that list is short the US dollar, whatever the ticker says. If the dollar strengthens they all lose together, so a book that looks diversified across five instruments carries 10% of the account on a single macroeconomic event.

A per-trade limit therefore means very little on its own. It needs a companion rule about total exposure — a cap on open risk overall, and a cap per currency, sector or theme.

And the last paragraph is the part that makes this urgent rather than academic. Correlations rise in a crisis: instruments that normally move independently move together precisely when several of your positions are already losing, so the diversification you were relying on disappears at the moment it was supposed to help.

If not: this prints a fixed table; the total is summed from it, so adding a position updates it correctly and is worth doing to see the number move.

4
Write the four checks an EA can enforce

Go: the same folder.

Do: save this as rules.py and run python3 rules.py.

"""Turn all of it into rules an EA can actually enforce."""

class RiskGuard:
    def __init__(self, per_trade=1.0, max_open=6.0, daily_loss=4.0, max_positions=3):
        self.per_trade, self.max_open = per_trade, max_open
        self.daily_loss, self.max_positions = daily_loss, max_positions
        self.open_risk, self.today_loss, self.positions = 0.0, 0.0, 0

    def may_open(self, risk):
        if risk > self.per_trade:
            return False, "risk %.1f%% exceeds the per-trade cap %.1f%%" % (risk, self.per_trade)
        if self.positions >= self.max_positions:
            return False, "already holding %d positions (cap %d)" % (self.positions, self.max_positions)
        if self.open_risk + risk > self.max_open:
            return False, "total open risk would be %.1f%% (cap %.1f%%)" % (self.open_risk + risk, self.max_open)
        if self.today_loss >= self.daily_loss:
            return False, "daily loss %.1f%% reached the stop-for-today cap %.1f%%" % (self.today_loss, self.daily_loss)
        self.open_risk += risk
        self.positions += 1
        return True, "allowed (open risk now %.1f%%)" % self.open_risk

g = RiskGuard()
ATTEMPTS = [1.0, 1.0, 1.0, 1.0, 2.5]
for i, r in enumerate(ATTEMPTS, 1):
    ok, why = g.may_open(r)
    print("attempt %d, risk %.1f%%  -> %-8s %s" % (i, r, "OPEN" if ok else "REFUSE", why))

print()
print("now the same guard after a bad morning:")
g2 = RiskGuard()
g2.today_loss = 4.2
ok, why = g2.may_open(1.0)
print("attempt with a fresh signal -> %-8s %s" % ("OPEN" if ok else "REFUSE", why))

print()
print("and with a looser position cap, so the OPEN-RISK cap is the one that binds:")
g3 = RiskGuard(per_trade=2.0, max_open=6.0, daily_loss=4.0, max_positions=10)
for i in range(1, 5):
    ok, why = g3.may_open(2.0)
    print("   attempt %d, risk 2.0%%  -> %-8s %s" % (i, "OPEN" if ok else "REFUSE", why))

print()
print("Four rules, and each one stops a different way of losing an account:")
print("   per-trade cap   -> one bad trade cannot matter much")
print("   position cap    -> you cannot accumulate quietly")
print("   open-risk cap   -> correlated positions cannot add up")
print("   daily-loss cap  -> a bad day cannot become a catastrophic one")
print()
print("Every one is a few lines of code, checked BEFORE the order is sent.")
print("None of them requires knowing anything about the market -- which is")
print("why they keep working when the strategy stops.")

You should see: each cap refusing a trade for a different reason:

attempt 1, risk 1.0%  -> OPEN     allowed (open risk now 1.0%)
attempt 2, risk 1.0%  -> OPEN     allowed (open risk now 2.0%)
attempt 3, risk 1.0%  -> OPEN     allowed (open risk now 3.0%)
attempt 4, risk 1.0%  -> REFUSE   already holding 3 positions (cap 3)
attempt 5, risk 2.5%  -> REFUSE   risk 2.5% exceeds the per-trade cap 1.0%

now the same guard after a bad morning:
attempt with a fresh signal -> REFUSE   daily loss 4.2% reached the stop-for-today cap 4.0%

and with a looser position cap, so the OPEN-RISK cap is the one that binds:
   attempt 1, risk 2.0%  -> OPEN     allowed (open risk now 2.0%)
   attempt 2, risk 2.0%  -> OPEN     allowed (open risk now 4.0%)
   attempt 3, risk 2.0%  -> OPEN     allowed (open risk now 6.0%)
   attempt 4, risk 2.0%  -> REFUSE   total open risk would be 8.0% (cap 6.0%)

Four rules, and each one stops a different way of losing an account:
   per-trade cap   -> one bad trade cannot matter much
   position cap    -> you cannot accumulate quietly
   open-risk cap   -> correlated positions cannot add up
   daily-loss cap  -> a bad day cannot become a catastrophic one

Every one is a few lines of code, checked BEFORE the order is sent.
None of them requires knowing anything about the market -- which is
why they keep working when the strategy stops.

Four rules, four distinct ways of losing an account, and every one of them a few lines checked before the order is sent:

  • Per-trade cap — one bad trade cannot matter much (step 2).
  • Position cap — you cannot accumulate quietly while distracted.
  • Open-risk cap — correlated positions cannot add up (step 4).
  • Daily-loss cap — a bad day cannot become a catastrophic one.

The two runs at the bottom exist because a guard that has never been seen to refuse anything has not been tested. With a tight position cap it is the position cap that binds; loosen it and the open-risk cap takes over. Both were made to fire deliberately.

The property that makes these worth having is that none of them knows anything about the market. They do not depend on the strategy being right, on volatility, or on your reading of conditions — so they keep working on the day the strategy stops working, which is the only day they matter.

The daily-loss cap deserves one extra note: it must reset on a schedule the EA can determine without your help, and it must survive a restart. Storing it in a global variable means a recompile clears it, and the EA cheerfully resumes trading on the worst day of the month.

If not: if the open-risk block never refuses, its cap is above what four attempts can reach — four at 2% is 8%, so a cap of 6% is what makes the fourth one fail.

🎉
Check yourself before moving on

Without scrolling up: a trader risks 5% per trade because their system wins 60% of the time, and reasons that with a 60% win rate a long losing streak is very unlikely. Show them the arithmetic that contradicts this, and say what you would suggest instead. Answer: two calculations settle it. First, streak likelihood: step 3 computed exact probabilities for a 55% system and found a run of eight happening more often than not over a thousand trades; at 60% the numbers are smaller but the same in kind, and a run of eight remains entirely ordinary rather than remarkable. Second, what such a run costs at 5%: eight consecutive 5% losses leave about 66% of the account, needing roughly 50% to recover, and ten leave about 60% needing 67% — which is a year of good trading spent getting back to level. The win rate does not protect against this, because streaks are a property of randomness rather than of edge. What to suggest is to separate the two decisions they have merged: the win rate tells you whether the system is worth trading, and the position size tells you whether you will still be trading it after the streak the arithmetic guarantees. Then add the caps from step 5, particularly the daily-loss limit, since the 5% habit does most of its damage on the day someone decides to trade their way out.

Now do it without the page: compute the recovery requirement for your own worst realistic case: take the largest percentage you have ever risked on one trade, multiply the streak length from step 3 that is more likely than not for your win rate, and work out what would be left. Then write the four caps from step 5 into whatever you trade with — as code if it is an EA, as a written rule with a number in it if it is not. A rule without a number is a preference, and preferences do not survive a losing week.

Risk Management Checklist for EAs

  • Every trade has a stop-loss — no exceptions
  • Position size is calculated from risk percentage, never hardcoded
  • Maximum drawdown kill switch is implemented
  • Daily loss limit is enforced
  • Maximum number of concurrent positions is limited
  • Maximum number of trades per day is limited (prevents overtrading)
  • Slippage protection: maximum deviation from requested price
  • Spread filter: do not trade when spread is abnormally wide
💡
Professional risk management

The risk controls shown here are the minimum. Production EAs used by professional traders include additional safeguards: equity curve tracking, regime detection, news filters, and dynamic risk adjustment. Building these systems is part of what makes professional EA development a specialized skill.