VALYNOXSTRATEGY RESEARCH TECHNOLOGY
AlphaPrimeAlphaPrime
ACADEMY / Backtesting and validation

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 · Published · updated · Editorial policy

Contents
  1. What goes wrong when you backtest a futures strategy in plain Python?
  2. backtrader vs vectorbt: which Python backtesting library fits futures?
  3. Vectorized or event-driven: which style leaks the future?
  4. How do you handle contract rollover in a Python futures backtest?
  5. Which cost and fill assumptions does simple code get wrong?
  6. How large a search can Python handle before speed becomes the limit?
  7. Frequently asked questions
  8. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 five steps of a Python futures backtest and the failure each one lets in: loading bars brings raw joined contract files, stitching contracts brings roll gaps, computing signals brings same-bar fills, filling orders brings zero costs, and running the search brings one slow loop repeated thousands of times Where the harness breaks Each step and the failure it lets in 1 2 3 4 5 Load bars Stitch contracts Compute signals Fill orders Run the search Raw joined files Roll gaps Same-bar fill Zero costs One slow loop The idea is not the weak point. So: fix all five before ranking.
The five steps of a Python futures backtest and the step where each common failure enters.

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.

Questionbacktradervectorbt
Execution modelBar-by-bar loop with a simulated brokerArray operations over the whole history
Typical useOne strategy with order logic, position rules and several data feedsParameter sweeps where many variants are computed together
Look-ahead riskLower by design: an order placed on bar N fills on bar N+1 unless you switch that offDepends on you shifting the signal one bar before it is applied
Rollover and continuous contractYou supply the adjusted seriesYou supply the adjusted series
CostsCommission and slippage set by youFees and slippage set by you
Comparison of backtrader and vectorbt: backtrader runs bar by bar and suits one strategy with lower look-ahead risk, vectorbt runs on whole arrays and suits big sweeps but needs you to shift the signal, and in both you build the rollover series and set the costs yourself Both leave rollover to you backtrader vs vectorbt backtrader vectorbt Execution Best for Look-ahead Rollover Costs Bar by bar Whole arrays One strategy Big sweeps Lower Your shift You build You build You set You set Neither stitches your contracts. So: build the series first.
The two libraries differ in how they execute, and both leave the futures-specific work to 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 timelines of bars N minus 1, N and N plus 1 with the signal known at the close of bar N: in the leaking version the fill happens at that same close, in the fixed version the fill happens at the open of bar N plus 1 Fill on the next bar Gold line: signal known at close of N Leak N−1 N N+1 Fill at that same close Fixed N−1 N N+1 Fill at the next open Same-bar fills use unknown prices. So: shift the signal one bar.
A signal computed on the close of bar N and filled on that same close uses a price the trader did not have yet. Next-bar execution fixes it.

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.

Two price lines over the same weeks: the raw series joined by date jumps on roll day, a roll gap; the back-adjusted series shifts the earlier contract so both segments meet without a step Remove the roll gap Two contracts joined around roll day Roll day Raw join Adjusted Roll gap No step The step is a trade no one made. So: back-adjust with a set rule.
Joining two contracts by date leaves a step on roll day. Back-adjustment shifts the earlier contract so the two segments meet.

The procedure has three parts, and all three are decided before the test:

  1. 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.
  2. 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.
  3. 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.

SettingDefault of a plain scriptSetting that mirrors an account
CommissionZeroBroker commission plus exchange and clearing fees, per contract, per round trip
SlippageZeroAt least one tick per side for liquid contracts with market or stop orders; more at the open and around news
Fill barSame bar as the decisionNext bar after the decision
Stop ordersFilled at the trigger priceFilled at the trigger price or worse
SessionEvery bar in the fileThe session you would actually trade, with orders flat at its close if that is the plan
Hypothetical example: the same 30 dollar cost per round trip takes 75 percent of the edge per trade of a fast rule but only 3 percent of the edge of a slow rule, so zero-cost defaults flatter frequent trading most Defaults flatter fast rules Same cost per trip, share of edge Cost: $30 per trip Illustration Fast rule Cost 75% Kept Slow rule Kept 97% Defaults flatter frequent trading. So: costs go into the first run.
The defaults of a plain script against the settings that mirror a live account.

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.

NumberResult
Net profit (closed trades)$179,675
Max drawdown$134,405
Trades88
Win rate62.5%
Profit factor1.647
Average trade$2,041.76
Time in market91.48%
The summary block of the report for the kept NQ candidate: read the average trade against the cost per trade, then the max drawdown against the net profit, then the trade count.
The summary block of the report for the kept NQ candidate: read the average trade against the cost per trade, then the max drawdown against the net profit, then the trade count. NQ · daily · 2019-01-02 to 2025-06-30 · CPU engine backtest

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.

Hypothetical grid search at 30 seconds per run: three parameters with 10, 8 and 4 values give 320 runs or 2.7 hours, a fourth with 10 values gives ten times as many runs or 27 hours, a fifth with 10 values gives a hundred times as many runs or 11 days Each parameter multiplies Each run is a full pass over history Assumption: 30 s a run Illustration 10 × 8 × 4 × 10 × 10 320 runs 10× more 100× more 2.7 hours 27 hours 11 days Hours become days in two steps. So: multiply before you start.
The count of backtests in a grid search multiplies with every parameter added, and each one is a full pass over the history.

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