Continuous Futures Backtesting: Signals, Rolls, and P&L Without Look-Ahead
Use adjusted continuous prices for signals, dated contracts for P&L, causal roll decisions, and execution assumptions that survive scrutiny.
A continuous series is a research tool, not a tradable contract
A futures backtest needs two views of the market. The first is a long, comparable price series for calculating signals. The second is the dated contract that an order could actually have used. A continuous file supplies the first view. It does not eliminate the second.
This distinction matters most at contract rolls. A model can hold a signal position through a continuous join, but a real account cannot. It must close the expiring contract and open another one, usually at different prices and with two sets of transaction costs. Ignoring that operational step can make a strategy look smoother and cheaper than anything an account could reproduce.
The continuous datasets here use additive back adjustment:
price = price_raw + back_adjustment
price_raw is the original contract price. price is the adjusted research series. back_adjustment links their levels across rolls. Keep all three fields when inspecting a transition instead of reducing the file to close prices alone.
Verify the adjustment before using it
Start with a mechanical check. Floating-point prices need a tolerance, but the tolerance should be small enough to catch a real scale error.
import numpy as np
import pandas as pd
frame = pd.read_parquet("cme.mnq.tick.continuous.2026.parquet")
expected = frame["price_raw"] + frame["back_adjustment"]
assert np.allclose(frame["price"], expected, rtol=0, atol=1e-9)
assert frame["ts"].is_monotonic_increasing
Next, inspect the roll ledger. It records the source contract, destination contract, decision session, switch timestamp, and price difference. An adjustment change can help you locate candidate transitions in the continuous file, but it is not a complete execution model. Use the ledger as the roll mapping.
import json
with open("MNQ_rolls.json") as handle:
ledger = json.load(handle)
for roll in ledger["rolls"][:3]:
print(
roll["session"],
roll["from"],
roll["to"],
roll["switch_utc"],
roll["difference"],
)
Check that each destination contract exists in the dated dataset when your simulated order would trade. A continuous row after the switch does not prove that your assumed order type and size would have filled at that exact price.
Do not calculate ordinary percentage returns from adjusted levels
Additive adjustment preserves point differences within the adjusted framework. It does not preserve percentage returns. Dividing an adjusted price change by an adjusted historical level changes the denominator and can produce a return that never corresponded to account performance. Large adjustments make this distortion especially important.
# Avoid this as account P&L:
adjusted_return = frame["price"].pct_change()
# MNQ account P&L belongs to a dated contract:
pnl_usd = side * (exit_raw - entry_raw) * 2.00 * contracts
Adjusted differences can be useful for signal research. Account P&L still belongs to the dated position: direction, raw entry, raw exit, multiplier, and quantity. At a roll, calculate the old contract's closing P&L first, then open the new contract at its own raw price. Do not include the price gap between the two contracts as a windfall or loss.
A useful audit is to print every roll with the old raw exit, new raw entry, adjustment, gross point P&L, and estimated costs. If the backtest reports a large return exactly at the switch but no dated position can explain it, the return calculation is probably wrong.
Make the roll decision with information available at the time
A roll rule has a decision time and an execution time. Those are not interchangeable. Suppose the rule switches after the destination contract's volume exceeds the source contract's volume for a session. That condition is fully known only after the relevant observations exist. A backtest cannot use the completed session total to justify a fill earlier in the same session.
Write the rule in causal language:
- Compute the volume comparison using data available by timestamp T.
- Allow the roll decision at T or later, according to the strategy.
- Submit the exit and entry no earlier than the decision.
- Use the next eligible prices and charge both transactions.
Run the same strategy across several plausible roll windows. Do not select the window with the best final equity curve and present it as validation. The point of the sensitivity run is to discover whether the strategy depends on a fortunate transition, not to optimize the historical roll schedule.
Keep UTC ordering separate from session labels
UTC is the reliable ordering key. Exchange-local session labels require a timezone database and a product calendar. Many CME sessions turn over at 17:00 Chicago, so a UTC calendar date and a trading-session label are not the same thing. Daylight-saving changes make a fixed hour offset incorrect for part of the year.
frame["chicago_ts"] = frame["ts"].dt.tz_convert("America/Chicago")
# Keep the original UTC timestamp for sorting and joins.
# Use a session calendar to assign trading-day labels.
Do not infer daily bars with resample("1D") on UTC timestamps. The published daily bars use the Chicago session boundary. Weekends, holidays, maintenance breaks, and early closes also need explicit handling. A gap is not automatically missing data, and a carried-forward price during a closure is not an available fill.
Move execution at least one step beyond the signal
If a signal uses a bar's closing price, filling at that same close assumes knowledge and execution happened simultaneously. A cleaner default is to decide at the close and execute at the next eligible event. Test additional delay as a sensitivity case. If performance disappears with a small delay, the result depends on timing that may be difficult to reproduce.
Transaction costs include more than commissions. Include exchange and clearing charges where applicable, spread, slippage, and the two sides of each roll. Bars do not contain a complete order book or queue position, so do not claim a precise fill simulation from them. Use conservative scenarios and state the limitation.
gross_pnl = side * (exit_raw - entry_raw) * multiplier * contracts
net_pnl = gross_pnl - commissions - fees - estimated_slippage
Keep gross and net results side by side. High turnover can make a small signal look attractive before costs and unusable afterward. Report roll costs separately so a reader can see how much of the total drag comes from maintaining the position.
Test the result where you did not build it
Choose training, validation, and final evaluation periods in chronological order. Fit parameters on training data, make model choices on validation data, and evaluate the finished rule once on untouched data. If the final period inspires another parameter change, it has joined development and is no longer a clean final test.
Include roll episodes in the diagnostics. Compare performance by contract, year, volatility regime, and time around each switch. A strategy whose profits come from one mishandled roll or one exceptional quarter has not shown robust behavior. Simple benchmarks, delayed execution, higher costs, and alternative roll windows provide useful context.
Before accepting a continuous-futures backtest
- Verify
price = price_raw + back_adjustmentand inspect the roll ledger. - Use adjusted data for signals and dated raw prices for account P&L.
- Make every roll decision after its inputs become known.
- Model the old-contract exit and new-contract entry, including costs.
- Preserve UTC ordering and use an exchange calendar for sessions.
- Delay fills, raise costs, vary roll windows, and report the results.
- Keep a chronological final period untouched.
For the file-level workflow, see MNQ historical data in Parquet. The full backtesting checklist expands these checks into a reproducible research review.