LEARN · EN · easyquanttrading.com

Maximum drawdown in Python: the code and three traps

Computing maximum drawdown in Python takes about four lines, which is why it gets written quickly and wrongly. The mistakes are not syntax errors — they produce a number, the number looks reasonable, and it differs from what your platform reports. This page is the code plus the reasons two implementations disagree.

By the EasyQuant Research Team·Published 2026-09-26·We publish the tests our own strategies fail. Nothing here is a return promise.

The implementation

The algorithm is a running maximum and a division. In plain Python:

peak = equity[0]; mdd = 0.0; for value in equity: if value > peak: peak = value; dd = value / peak - 1.0; if dd < mdd: mdd = dd. At the end `mdd` is the maximum drawdown as a negative fraction.

That is the whole calculation. The running peak only ever increases — it never resets when the curve recovers, because a drawdown is measured from the highest equity ever reached, not from a recent peak.

The vectorised version with NumPy is shorter and faster: convert the series to an array, take `np.maximum.accumulate` to get the running peak at every point, divide the series by that peak elementwise, subtract one, and take the minimum. Same result, no loop, and it handles millions of bars without complaint.

A worked check with a small series: equity 100, 110, 120, 108, 102, 115, 123. The running peaks are 100, 110, 120, 120, 120, 120, 123 and the drawdowns are 0%, 0%, 0%, −10%, −15%, −4.17%, 0%. The answer is −15%, from the 120 peak to the 102 trough.

Trap one: resetting the peak at the wrong moment

The most common bug is a peak that resets too eagerly. If the peak is reset whenever the curve recovers above its starting value, or reset at the start of each period, the drawdown measured after that reset is smaller than the real one.

The classic form of this bug: computing the maximum drawdown per calendar month and reporting the worst month. That is the worst **monthly** drawdown, not the maximum drawdown. A decline that starts in March and bottoms in May is cut in two by the month boundary, and neither half is measured from the true peak — so both halves come out smaller than the real decline. A segmented maximum is always a lower bound on the unsegmented one.

The test for this bug: feed the function a series that declines steadily across several period boundaries and check it returns the full peak-to-trough fall, not the largest single-period slice.

Trap two: equity versus closed-trade balance

Balance is what you have after closed trades. Equity is balance plus the unrealised profit or loss on open positions. Maximum drawdown computed on balance ignores the drawdown that happens while a trade is open, which for a strategy that holds positions for days can be most of it.

The practical difference is large. A strategy that opens a position and holds it through a 20% adverse move shows no balance drawdown at all until the trade closes — and then shows the whole thing at once. If your risk was judged on balance drawdown, you were carrying a risk you never measured.

So the input matters as much as the code. If you have only closed-trade data, you can compute a drawdown, but it is a drawdown on a different object from the one you are exposed to. State which one you used whenever you quote the number.

Trap three: data resolution and the shape of the series

The same strategy produces different maximum drawdowns at different sampling frequencies, and the finer the data the larger the figure — usually. Tick-level equity contains intraday excursions that a daily series never sees, so a tick-based maximum drawdown can be several times the daily one.

Neither is wrong. They answer different questions: the tick figure is the worst moment you would have had to sit through; the daily figure is the worst day-end position. Pick the one that matches your monitoring and holding behaviour, then be consistent.

A related issue is what you feed in. Using cumulative profit instead of equity, or forgetting to start from the actual account balance, both shift the denominator. Since drawdown is a ratio, a wrong denominator changes the answer even when the numerator is right.

One more: a drawdown that begins at the very first bar has no prior peak, so a series that starts at its high and falls immediately will report correctly, while a series that starts mid-recovery needs its warm-up period included to be measured fairly. Excluding the warm-up from the statistics but including it in the peak tracking is the defensible choice.

What to compute alongside it

Maximum drawdown alone is thin. Four numbers together describe a drawdown properly, and all four come from the same running-peak series you already have.

**Depth** — the maximum drawdown itself. **Duration** — how many bars from the peak to the recovery back to that peak, which is often the number that decides whether a person can actually hold the strategy. **Time to recovery** from the trough rather than from the peak. And **the drawdown series** rather than its minimum, so you can see whether you have one deep event or a persistent inability to make new highs.

The recovery requirement is worth computing explicitly because it is the most under-appreciated number in the set: a 50% drawdown needs a 100% gain to get back to the peak, and a 75% drawdown needs 300%.

What the number does not tell you

It is a historical maximum over the window you supplied, so it is a lower bound on what can happen rather than an estimate of it. Extending the window can only make it larger or leave it unchanged.

It says nothing about probability. Two strategies with the same maximum drawdown are not equivalent if one reached it once in a decade and the other every eight months.

It is sensitive to every assumption upstream: the cost model, the position sizing, the data resolution and the period. A drawdown figure without those four qualifiers is not comparable to another one.

And it does not tell you whether you would have kept trading. That is the question the number is a proxy for, and it is not a question code can answer.

Current platform facts

Read live from the strategy library when this page was generated. These are the same counts published on our transparency page, and they change as strategies are added and rejected.

Strategies in the audited library3672
Flagged by the audit2011
Flag rate54.8%
Checks still pending1651
Passed the DSR overfitting check1
Passed the significance check504
DSR threshold used0.90

FAQ

How do I calculate maximum drawdown in Python?
Build a running maximum of the equity series, divide the series by that running maximum, subtract one, and take the minimum. With NumPy: peak = np.maximum.accumulate(equity); mdd = (equity / peak - 1).min().
Why is my maximum drawdown different from my platform's?
Usually one of four reasons: equity versus closed-trade balance, the sampling frequency of the series, whether the warm-up period was included, or a peak that was reset at period boundaries. All four change the answer while producing a plausible-looking number.
Should maximum drawdown be computed on equity or balance?
Equity, if the strategy holds positions open, because it includes unrealised losses you are actually exposed to. Balance drawdown can understate the risk of a strategy that holds losers for days.
Does maximum drawdown change with data frequency?
Yes, and finer data usually makes it larger, because intraday excursions never appear in a daily series. Neither figure is wrong; they measure the worst moment you would sit through versus the worst day-end position. Pick the one matching how you monitor and be consistent.
What is a good maximum drawdown?
It depends on the recovery arithmetic more than on a threshold: 50% needs a 100% gain to recover and 75% needs 300%. The practical test is whether you would keep trading through it, and it will last longer than you expect.

More guides

Not investment advice. Historical results do not guarantee future performance. EasyQuant is a research factory — you execute on accounts you control.

Maximum drawdown in Python: the code and three traps