Python backtesting for futures: 5 places simple code goes wrong
A plain Python backtest of a futures strategy fails at five points outside the idea itself: the continuous contract, the bar where the rule decides, the price of the fill, the cost per round trip, and the time a large search takes. Each one has a small fix; the five fixes together are the difference between a script result and a result you could have traded.
By Soren Huang · Published · updated · Editorial policy
Contents
- What goes wrong when you backtest a futures strategy in plain Python?
- backtrader vs vectorbt: which Python backtesting library fits futures?
- Vectorized or event-driven: which style leaks the future?
- How do you handle contract rollover in a Python futures backtest?
- Which cost and fill assumptions does simple code get wrong?
- How large a search can Python handle before speed becomes the limit?
- Frequently asked questions
- References
What goes wrong when you backtest a futures strategy in plain Python?
Python backtesting of a futures strategy breaks in five places outside the idea itself: the continuous contract (roll gaps), the bar on which the rule decides (look-ahead), the price at which the fill happens, the cost per round trip, and the time a search of thousands of variants takes. Fix these five and the idea gets a fair test.
A backtest is the replay of a fixed set of trading rules on old prices, with the costs an account would have paid, so that you can read how the rules behaved. The idea behind what backtesting in trading actually replays is simple. The harness around it is where a short script goes wrong. The claim of this article is that most of the gap between a Python backtest and a live futures account comes from that harness, not from the strategy idea. The five places are:
- Loading bars. Futures contracts expire. A script that reads one file per contract and joins them by date has built a series with price steps in it.
- Building the continuous contract. The step at each roll must be removed, and the roll date must follow a rule you wrote before the test.
- Computing signals. A rule that looks at the close of a bar must not act on that same close. This is the look-ahead leak.
- Filling orders. Zero commission, zero slippage and stop orders filled at the exact trigger price are the defaults of most scripts and of no account.
- Running the search. A loop that is fine for one run is multiplied by every parameter value you try, and the research stops when the clock does.
The sections below take the five in order. Each one names the failure, shows what it does to the result, and gives the fix in plain steps.
backtrader vs vectorbt: which Python backtesting library fits futures?
backtrader fits one strategy with explicit order logic, vectorbt fits sweeps over many parameter values, and neither builds a back-adjusted continuous futures contract for you. The two libraries are the ones most Python traders meet first, and they work in opposite ways.
An event-driven backtest is a loop that visits one bar at a time, runs the strategy code on that bar, and lets a simulated broker fill any order on a later bar. backtrader works this way: you write a strategy class, the engine calls it once per bar, and a market order placed on one bar is filled at the open of the next bar by default. A vectorized backtest is a calculation on whole columns of numbers at once: the signal for every bar is computed in one operation, the position for every bar in another, and the profit for every bar in a third. vectorbt works this way on top of pandas and NumPy, and it is built to run the same calculation for many parameter values side by side.
| Question | backtrader | vectorbt |
|---|---|---|
| Execution model | Bar-by-bar loop with a simulated broker | Array operations over the whole history |
| Typical use | One strategy with order logic, position rules and several data feeds | Parameter sweeps where many variants are computed together |
| Look-ahead risk | Lower by design: an order placed on bar N fills on bar N+1 unless you switch that off | Depends on you shifting the signal one bar before it is applied |
| Rollover and continuous contract | You supply the adjusted series | You supply the adjusted series |
| Costs | Commission and slippage set by you | Fees and slippage set by you |
The last two rows matter most for futures. Both libraries take a price series as input and do not ask where it came from. Whether the series has roll gaps, whether it covers the right session, and whether the costs match the contract are decisions made before the library sees a single bar. The six checks for comparing backtesting software apply to a library you write around as much as to a finished program; several of them are about exactly these inputs.
Vectorized or event-driven: which style leaks the future?
Vectorized code leaks the future more often, because the fill price sits in the same row as the decision unless you shift the signal by one bar. Look-ahead bias is any use of information in a backtest that a trader could not have had at the moment the order was placed. In a bar-by-bar loop the order of events is written out: the strategy reads bar N, places an order, and the broker fills it on bar N+1. In a vectorized script all of that is one line, and the line is easy to write wrong.
The classic mistake is this. You compute a signal from the close of each bar, for example "close above the 20-bar average". You then multiply the signal by the return of the same bar. That return runs from the previous close to this close, and the signal was not known until this close. The script has bought at a price that had already moved in its favour. The fix is one shift: the position on bar N+1 equals the signal from bar N. In pandas terms, shift the signal column down one row before you multiply.
Two more leaks hide in vectorized code. The first is an indicator that uses a centred window, so the value on bar N already contains bars N+1 to N+k; rolling functions with the centre option do this. The second is a stop or target checked against the high and low of the same bar that opened the position, so the exit price can be one the market printed before the entry. The one-bar test in look-ahead bias and the one-bar check catches all three: shift every input one bar later and rerun; a result that collapses was using information it did not have.
How do you handle contract rollover in a Python futures backtest?
Build the continuous series yourself with a written roll rule and a price adjustment at each roll, because joining contracts by date alone creates a step on roll day that the strategy trades as if it were real. Rollover is the move from the expiring futures contract to the next one, and a continuous contract is a single price series stitched from many contracts so that a rule can run across years without a gap.
The step exists because two contracts of the same future trade at different prices on the same day. The difference reflects financing, dividends or storage until each expiry. If the March contract closes at one price and the June contract trades higher on the same day, a series that switches from March to June overnight shows a jump that no trader experienced. A breakout rule fires on that jump. A position held across the roll books the jump as profit or loss. Over ten years of quarterly rolls, forty such steps can be a large part of a daily-bar result.
Hypothetical example
Suppose the June NQ contract trades 40 index points above the March contract on roll day. A raw joined series shows a 40-point step. At the published point value of $20 per index point, a long position held across that day shows an $800 gain per contract that no account received, and a short position shows an $800 loss that no account paid. A rule tested on ten years of such steps has forty of these phantom trades inside its result.
The procedure has three parts, and all three are decided before the test:
- Write the roll rule. Common rules are a fixed number of trading days before expiry, or the first day on which the next contract has higher volume or open interest than the current one. Pick one and keep it for every roll.
- Adjust the price. Back-adjustment measures the difference between the two contracts on roll day and shifts every earlier bar by that difference, so the series has no step. The most recent contract keeps its real prices. Ratio adjustment multiplies instead of adding; it suits long histories where prices have changed a lot.
- Keep the raw series next to the adjusted one. Rules that depend on price levels, such as a fixed-tick stop or a margin check, use the adjusted series for signals and the raw series for anything tied to the real contract.
Back-adjusted prices can become negative far back in history and will not match a quote screen; that is expected. The fuller treatment of roll rules, tick values and which contract to use for each purpose is in futures backtesting with contract specs and rollover done right.
Which cost and fill assumptions does simple code get wrong?
Adding a realistic round-trip cost and next-bar execution is the single change that moves a plain script closest to what an account would see. Most scripts start with zero commission, zero slippage, fills at the signal bar's close and stop orders filled at the exact trigger price. Each default is small per trade and large across a history, and together they favour strategies that trade often, because those have the most trades and the smallest move per trade.
| Setting | Default of a plain script | Setting that mirrors an account |
|---|---|---|
| Commission | Zero | Broker commission plus exchange and clearing fees, per contract, per round trip |
| Slippage | Zero | At least one tick per side for liquid contracts with market or stop orders; more at the open and around news |
| Fill bar | Same bar as the decision | Next bar after the decision |
| Stop orders | Filled at the trigger price | Filled at the trigger price or worse |
| Session | Every bar in the file | The session you would actually trade, with orders flat at its close if that is the plan |
For NQ the exchange specification gives a contract size of $20 times the index and a minimum tick of 0.25 index points, worth $5.00, so one tick of slippage on each side of a trade is about $10 per round trip. Exchanges can change these values; check the exchange's contract page, or read the current tick size and point value in the free futures contract calculator. Fills are the one input a backtest can only assume, which is why what a day trading simulator teaches that a backtest cannot is mostly about where your orders actually fill.
The run behind this article shows what a report looks like when every trade already carries these settings. AlphaPrime (Valynox) tested NQ daily bars from 2019-01-02 to 2025-06-30, long and short, one contract, with $20 commission and $10 slippage charged on every trade and next-bar execution. The rule is a Fair Value Gap entry and exit with no stop, target or filter: a stand-in rule built from standard indicators, used here to show the cost treatment, not a subject of this article. The search tried 1,024 variants and kept this one for its return against its drawdown; picking the kept one after the fact flatters it, so the numbers describe one past run and nothing a reader should expect from the rule.
Source: AlphaPrime product engine (CPU), run 2026-10-31. NQ daily bars, 2019-01-02 to 2025-06-30, long and short, 1 contract, $20 commission and $10 slippage per trade.
| Number | Result |
|---|---|
| Net profit (closed trades) | $179,675 |
| Max drawdown | $134,405 |
| Trades | 88 |
| Win rate | 62.5% |
| Profit factor | 1.647 |
| Average trade | $2,041.76 |
| Time in market | 91.48% |

Read the table the way the cost table above suggests. The average trade of $2,041.76 is far above the $30 charged per trade, so on daily bars the cost line is not what decides this result. The max drawdown of $134,405 against a net profit of $179,675 is the number that decides whether anyone could have held the position, and 88 trades over six and a half years is a small sample for any per-trade figure. A script that charged nothing and filled on the signal bar would have shown a different, friendlier table for the same rule. In AlphaPrime the fees, slippage and next-bar execution in backtesting are applied inside every evaluation of a search, so the ranking of candidates is already net of these settings before anyone reads it.
How large a search can Python handle before speed becomes the limit?
A parameter search grows by multiplication, so a bar-by-bar loop that takes half a minute per run becomes the limit at a few thousand variants. The number of backtests in a full grid search is the product of the number of values tried for each parameter:
number of backtests = v₁ × v₂ × … × vₖ
Here k is how many parameters the strategy has, and v₁, v₂ and so on are how many values you try for each one. Every backtest in that count is one complete pass over the bar history, with signals, fills and costs. Adding one parameter does not add to the count; it multiplies it.
Hypothetical example
Suppose one bar-by-bar run over ten years of hourly bars takes 30 seconds. Three parameters with 10, 8 and 4 values give 320 runs, about 2.7 hours. A fourth parameter with 10 values gives 3,200 runs, about 27 hours. A fifth with 10 values gives 32,000 runs, about 11 days of unattended computing. The run behind this article tried 1,024 variants of a one-rule strategy on daily bars; the same grid on hourly bars holds about 24 times as many bars per run.
Bar size multiplies the same way. High frequency data backtesting, on one-minute or tick bars, puts roughly sixty times as many bars into each run as hourly data does, so a grid that took a day on hourly bars takes weeks on one-minute bars. Vectorized code answers part of this: it runs many variants in one array operation, which is what vectorbt is designed for. It also brings back the shift problem of the look-ahead section, so the one-bar check belongs in every vectorized sweep. Whatever the style, the time depends on the processor or graphics card doing the work and on the length of the bar data, which is why the count of variants should be known before the first line of the loop is written.
The size of the search also changes how to read the winner. Bailey, Borwein, López de Prado and Zhu (2014) show that the best result out of many variants is inflated by the selection itself, and that the inflation grows with the number of variants tried. Harvey and Liu (2015) propose discounting a result for the number of tests behind it. The five-parameter grid in the example above, with a hundred times as many variants as the three-parameter grid, therefore needs a stronger out-of-sample check, not a weaker one. If writing and maintaining that harness is not where you want to spend your time, the options in free backtesting software for futures cover the same five steps with different trade-offs.
Before your next search, do one thing: write down each parameter and the number of values you plan to try, multiply them, time one run with costs and next-bar fills switched on, and multiply the two. If the answer is measured in days, cut the grid, or change the style of the harness, before you start it.
Frequently asked questions
Which Python backtesting library should a futures trader start with?
Choose by the job rather than by the name. A bar-by-bar library such as backtrader suits one strategy with explicit order rules, stops and several data feeds, and its default of filling an order on the next bar removes one common leak. An array-based library such as vectorbt suits sweeps over many parameter values. In both cases the continuous contract, the session and the cost settings are your work before the library sees the data.
Do backtrader and vectorbt handle futures rollover for you?
Neither builds a back-adjusted continuous series from raw contract files. You decide the roll rule, measure the price difference between contracts on each roll day, shift the earlier contract by that difference, and feed the finished series to the library. Keep the raw contract series next to it for anything tied to real price levels, such as a fixed-tick stop.
Why does my Python backtest look so different from live trading?
The usual causes are in the harness, not the idea: a signal filled on the bar that produced it, zero commission and slippage, stop orders filled at the exact trigger price, roll gaps traded as if they were real, and bars from a session you never trade. Rerun the backtest with every input shifted one bar later and with realistic costs; the amount the result falls tells you how much of it was harness.
Can a plain Python loop handle high-frequency data backtesting?
For one run, yes; one-minute bars over several years finish in minutes to hours in a bar-by-bar loop. For a parameter grid the count of runs multiplies by every parameter, and the bar count multiplies by about sixty when moving from hourly to one-minute data, so grids of a few thousand variants become a matter of days. Vectorized code shortens this, as long as the signal is shifted one bar before it is applied.
References
- Bailey, D. H., Borwein, J., López de Prado, M., & Zhu, Q. J. (2014). Pseudo-Mathematics and Financial Charlatanism: The Effects of Backtest Overfitting on Out-of-Sample Performance. Notices of the American Mathematical Society, 61(5), 458–471.
- Harvey, C. R., & Liu, Y. (2015). Backtesting. The Journal of Portfolio Management, 42(1), 13–28.
- CME Group. E-mini Nasdaq-100 Futures contract specifications (contract size $20 × index, minimum tick 0.25 index points = $5.00). cmegroup.com/markets/equities/nasdaq/e-mini-nasdaq-100.contractSpecs.html

