LEARN · EN · easyquanttrading.com
Backtest with Python: the cost assumptions that decide your result
Python makes backtesting tempting: a price series, a signal, a loop, and you have an equity curve. The curve will look good, and the reason is rarely the strategy. It is that the default assumptions of a hand-written backtest are optimistic in ways that are invisible until you write them down. This is a checklist for writing them down.
By the EasyQuant Research Team·Published 2026-09-26·We publish the tests our own strategies fail. Nothing here is a return promise.
- A hand-written backtest defaults to zero costs and perfect fills unless you intervene
- Intrabar order of stop and target is the single largest hidden assumption
- Trade on bar close means you cannot fill at that close in a live account
- Test the same code with costs doubled before you believe the result
Start from what the code does by default
The naive Python backtest looks like this: iterate over bars, when the signal fires record an entry at that bar's close, later record an exit at some bar's close, then compute the return from the difference. Every part of that is an assumption, and all of them point the same way.
The default assumptions are: the spread is zero, the commission is zero, you fill exactly at the close you observed, the stop and the target never conflict, and your size never varies. Under those assumptions almost any strategy with a mild trend bias produces a pretty curve. The work of a backtest is replacing them with something defensible.
Assumption one: the spread, applied on both sides
A price series from an API is usually a mid price or a last price. You buy at the ask and sell at the bid, so every round trip pays the spread once in full — not once per side as an extra, but as the gap you cross twice.
The code shape is to charge it at the moment of the fill, in the correct direction: an entry on the long side fills at price plus half the spread, an exit from a long fills at price minus half the spread. Applying it as a single deduction from profit at the end is equivalent in total but hides which trades were actually killed by it.
A detail worth getting right: spread is not constant. It is tighter in liquid sessions and much wider around news, at the daily rollover, and on weekends. Using a single average spread understates the cost of the trades that happen at the worst moments, which are often the ones your strategy is most eager to take.
Assumption two: commission per trade, not per strategy
Commission is charged per trade regardless of size (or per lot), so it is a fixed cost that hits high-frequency strategies hardest. If your backtest has 1,200 trades and your commission is 3.50 per round turn, that is 4,200 of cost before the first unit of profit.
The code shape is a signed deduction on each fill. The common mistake is to model it as a percentage of notional, which is how some brokers charge and not how others do — read your own broker's schedule and model that, not a generic one.
It is worth printing the total commission as a share of gross profit. If costs eat more than about a third of gross profit, the strategy's margin for error is thin, and a small deterioration in either the edge or the costs turns it negative.
Assumption three: what happened inside the bar
This is the one that changes results most and is easiest to get silently wrong. On a bar where the price touches both your stop and your target, the bar's OHLC data does not tell you which came first. Your code will do whatever your `if` statements say, and that is an assumption.
Three defensible choices, in increasing order of how hard they are on the strategy: resolve in favour of the stop (pessimistic), resolve by the bar's direction (open to close, a rough proxy), or use a finer data series than the one you are trading on.
The third is the honest one if you can afford it. If your strategy trades hourly bars, resolve intrabar conflicts on minute bars. The cost is data volume and runtime; the benefit is that the result stops depending on a coin flip you wrote into the code.
A useful test: run the same backtest with all intrabar conflicts resolved against you and again in your favour. If the results differ materially, your strategy's edge is partly a property of your `if` statement.
Assumption four: you cannot trade the close you just saw
If your signal is computed from a bar's close and you fill at that same close, you have given yourself something a live account does not get. The close is only known once the bar is finished, and by then that price is gone.
The fix is a one-bar delay: signal on bar i, fill at the open of bar i+1. This is the cheapest realism upgrade in the whole list, and it costs nothing but one line of index bookkeeping. It belongs in the code from the first draft, not added later, because adding it later changes every result you have already looked at.
Assumption five: slippage, and what it is not
Slippage is the difference between the price your strategy asked for and the price it got. It is not the spread — the spread is the known gap you cross; slippage is the unknown part on top. It is largest exactly when you most need a fill: fast markets, thin liquidity, and around scheduled news.
Modelling it as a fixed number of points per trade is better than zero and worse than nothing, because slippage is not constant. A more honest model makes it a function of volatility: slippage proportional to the recent average true range, applied on entry and exit. That way the backtest charges more in the conditions that actually cost more.
For stop orders specifically, model the fill at the stop price minus a slippage amount rather than exactly at the stop. A stop is an instruction to trade at market once a price is touched, not a guaranteed price.
Assumption six: size, and how it changes with equity
A backtest that trades a fixed number of units and a backtest that risks a fixed percentage of equity are different strategies with different drawdown profiles, even though the signals are identical. Which one did you write?
Fixed size is simpler but means your risk grows as a share of the account when the account shrinks, which is exactly backwards. Fixed fractional sizing means the position size depends on the equity curve, so the loop has to carry equity forward rather than computing returns as a simple sum of trade profits.
The reason this matters for a Python backtest in particular: it is easy to compute an equity curve by adding trade profits to a starting number, which silently implements fixed size. If you intended fixed fractional, you will not notice the difference until the drawdowns do not match the ones you expected.
Assumption seven: the warm-up, and the series you measure on
Any indicator with a lookback has no value for its first N bars. If your backtest starts producing signals from bar zero, the early trades were generated from an indicator fed with incomplete data — a look-ahead artefact in miniature.
The code shape is to compute indicators over the whole series, then start trading only after a warm-up period longer than the longest lookback, and to exclude that period from the performance statistics rather than counting it as a flat start.
The same care applies to the equity series itself. Maximum drawdown computed on per-bar equity is larger than the same measure computed on closed trades only, because it includes the unrealised loss while a position is open. Both are legitimate; they answer different questions, and a report that does not say which it used is not comparable to another.
The test that takes two minutes and tells you the most
Once your backtest runs, change one thing: double every cost assumption — spread, commission and slippage — and re-run. Do not change the signals.
If the strategy survives with a reduced but still positive result, you have learned something real: the edge is larger than the cost uncertainty. If it collapses, you have learned something more valuable and much cheaper than finding out live: the strategy was a bet on cost assumptions you cannot control.
Then compare the trade lists rather than the summary numbers. The final return can coincide by accident between two runs; the list of trades cannot. If doubling costs removed 300 trades and turned another 120 from wins into losses, the strategy depends on cheap execution, and that is a fact worth knowing before sizing it.
What a Python backtest cannot tell you
It cannot tell you the strategy will work. It can tell you whether the result you have is robust to the assumptions you were able to write down — a smaller and much more defensible claim.
It cannot model your own behaviour. The code follows the rules every time; you will not.
It cannot model execution you have not experienced: a rejected order, a partial fill, a broker that re-quotes in a fast market, or a terminal that disconnects at 3am with a position open. None of these appear in a price series, and all of them happen.
And it cannot tell you whether the edge persists. That question is not about code at all, and no amount of engineering rigour answers it.
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 library | 3674 |
|---|---|
| Flagged by the audit | 2013 |
| Flag rate | 54.8% |
| Checks still pending | 1651 |
| Passed the DSR overfitting check | 1 |
| Passed the significance check | 504 |
| DSR threshold used | 0.90 |
FAQ
- What is the best Python library for backtesting?
- The library matters far less than the assumptions you configure. A simple hand-written loop with explicit spread, commission, intrabar resolution and one-bar execution delay will give you a more honest answer than a feature-rich framework left on its defaults.
- Why does my Python backtest look better than live trading?
- Almost always because of assumptions rather than bugs: zero costs, filling at the close you just observed, intrabar conflicts resolved in your favour, and position sizing that does not shrink with the account. Each one is easy to leave out and each one flatters the result.
- How do I model slippage in a Python backtest?
- Better than a fixed number of points: make it proportional to recent volatility, such as a fraction of average true range, and apply it on both entry and exit. Fixed slippage understates cost in exactly the fast markets where it matters most.
- Should I fill at the close of the signal bar?
- No. The close is only known once the bar has finished, so a live account cannot trade at it. Signal on bar i and fill at the open of bar i+1. It costs one line of code and removes a systematic optimistic bias.
- How do I know if my backtest assumptions are too generous?
- Double every cost assumption, keep the signals identical, and re-run. If the strategy survives with a smaller positive result the edge exceeds the cost uncertainty; if it collapses, the result depended on assumptions you do not control.
More guides
- How EasyQuant validates strategies — evidence you can filter
- Honest backtesting, not pretty curves
- Gold strategy research that stays honest
- Overfitting detection: catch it before you deploy
- System Forge: design, then prove
- Walk-forward analysis: the only backtest that fights overfitting
- MT5 export without custody
- Glass box, not black box AI signals
Not investment advice. Historical results do not guarantee future performance. EasyQuant is a research factory — you execute on accounts you control.