Expiry rollovers, corporate actions, strike-chain survivorship and the look-ahead bugs that survive code review. Seven ways an Indian backtest lies to you.
A backtest is a claim about a counterfactual: had I run this, this is what would have happened. Most of the ways that claim fails are generic and well documented. A handful are specific to Indian market structure, and those are the ones that survive a careful code review because the code is correct — the data is what is lying.
1. Expiry rollover artefacts
Trend chart
How naive concatenation creates a phantom return
An illustrative indexed series shows the strategy-visible jump when the next contract has a different basis, even though neither contract moved that way.
The actual-contract series rises smoothly from 100 to 103 across four observations. A naively concatenated continuous series is 100 before the roll, jumps to 112 when the next contract is substituted, and then moves to 113. The artificial twelve-point level change can trigger a momentum signal that was never tradeable.
Illustrative index values, not market data. Back-adjustment removes the return discontinuity; trading actual contracts makes the roll and its cost explicit.
Indian index and stock futures expire monthly, and the near-month contract is the liquid one until a few days before expiry. To backtest a multi-month strategy you need a continuous series, and building one requires a decision about what happens at the roll.
Naive concatenation — near-month prices until expiry, then next-month prices — introduces a discontinuity equal to the basis between the two contracts. On a Bank Nifty future that can be tens of points. A momentum strategy reading that series sees a gap it can trade, twelve times a year, that never existed.
Method
Preserves
Distorts
Naive concatenation
Absolute price levels
Returns at every roll — creates phantom gaps
Back-adjusted (difference)
Return series
Absolute levels; old prices can go negative on long histories
Ratio-adjusted
Percentage returns
Absolute levels; better for long histories than difference-adjustment
Trade the actual contract
Everything
Nothing — but the strategy must handle the roll explicitly
Rollover methods and what they distort
2. Corporate actions in the cash series
Splits, bonuses, and dividends. A 1:1 bonus halves the price overnight, and an unadjusted series shows a −50% return. Any strategy with a stop-loss will be stopped out; any mean-reversion strategy will see the buying opportunity of the decade.
The subtle version: the series is adjusted, but the adjustment factor was applied from the ex-date rather than including it, or dividends were ignored while splits were handled. Verify against a known event rather than trusting the vendor. Pick a stock that had a 1:1 bonus, and check that the return on the ex-date is roughly flat.
verify_adjustment.pypython
import pandas as pddef check_corporate_action(series: pd.Series, ex_date: str, ratio: float) -> None: """ series: adjusted close, indexed by date ratio: price multiplier implied by the action (0.5 for a 1:1 bonus) """ window = series.loc[:ex_date].tail(2) overnight_return = window.iloc[-1] / window.iloc[0] - 1 # A correctly adjusted series shows a normal daily move here. # An unadjusted one shows something close to (ratio - 1). assert abs(overnight_return) < 0.15, ( f"Suspected unadjusted corporate action on {ex_date}: " f"overnight return {overnight_return:.1%}, expected ~0%, " f"unadjusted would be ~{ratio - 1:.0%}" )
A five-line sanity check that catches most adjustment errors.
3. Strike-chain survivorship
Decision funnel
How an options universe becomes hindsight-filtered
Every dataset filter preferentially retains strikes that turned out to be active and liquid.
The exchange contract master contains the full strike ladder. Quote availability removes never-quoted contracts. Trade-based historical data removes quoted but untraded strikes. Data cleaning removes sparse and stale series. A backtest then selects from the surviving liquid strikes, creating a universe that was not knowable at decision time.
Reconstruct eligibility from the contemporaneous contract master and quote state, then record strike-selection failures as outcomes rather than dropping them.
This is the one that invalidates the most options backtests, and it is largely invisible.
Historical options datasets typically contain the strikes that had activity. Strikes that were introduced and never traded may be absent entirely, and strikes that traded thinly may have sparse or stale prices. Your backtest, scanning for a strike at a given delta, only ever sees strikes that survived into the dataset — which correlates with the strikes that were liquid, which correlates with where the market actually went.
The practical effect: a strategy that selects strikes by some criterion will systematically select strikes that were tradeable in hindsight. When it runs live, the equivalent strike at decision time is often the one with a two-rupee-wide spread and no depth.
Mitigations
Require a minimum traded volume and a maximum spread at decision time, and record the failures as a metric.
Reconstruct the full strike ladder from the exchange contract master rather than inferring it from trade data.
Backtest strike selection and strategy P&L as two separate reports. If selection fails 15% of the time, you need to know that number.
4. Timestamp semantics and look-ahead bias
Timeline
When a 09:15 candle becomes knowable
The timestamp printed on a bar is not necessarily the time at which its close could be observed and traded.
At 09:15:00 the minute opens. Ticks arrive through 09:15:59. Only after the interval ends is the final high, low, close and volume known. Signal computation then runs, an order travels to the broker and exchange, and the earliest realistic fill is in the next interval after 09:16:00.
If the backtest both reads the completed bar and fills at that bar’s close, it has borrowed information from the future unless an executable quote was recorded there.
A minute candle labelled 09:15 might contain trades from 09:15:00 to 09:15:59, or it might be the candle ending at 09:15. Vendors differ. Some differ within the same dataset across instruments.
If your loop reads the 09:15 candle, computes a signal, and enters at the 09:15 close, you have used data up to 09:15:59 to place an order at 09:15:59 — which is fine only if you also assume zero latency and a fill at the last price. In practice the order reaches the exchange after 09:16:00 and fills somewhere in the next candle.
no_lookahead.pypython
# Wrong: decides and fills inside the same bar.for i in range(len(bars)): if signal(bars[:i + 1]): enter(price=bars[i].close) # <- knew the close before it closed# Right: the signal bar must be complete before the order exists.for i in range(len(bars) - 1): if signal(bars[:i + 1]): # The earliest observable price after the decision is the next bar's # open, and even that is optimistic — real orders arrive a few hundred # milliseconds in. If the edge only exists at bars[i].close, it is an # artefact of the timestamp convention, not a tradeable signal. enter(price=bars[i + 1].open)
Signal on the closed bar, execute on the next. The one-bar offset is not conservatism, it is correctness.
The test for whether this matters: run the backtest with the one-bar offset and without it. If the results are materially different, the strategy’s edge was in the look-ahead. Strategies with a real multi-bar edge lose a little; strategies without one collapse.
5. Circuit limits, halts and illiquid sessions
Indian equities have price bands. When a stock hits its circuit limit, it stops trading in that direction — there is no counterparty at a worse price. A backtest that places a market order at a limit-up price is trading against liquidity that did not exist.
The same applies to market-wide halts, to instruments in a ban period for F&O, and to the pre-open session, which has a different microstructure from continuous trading and should generally be excluded from a strategy designed for continuous trading.
Data hygiene rules worth encoding once
Exclude bars where the instrument was at a circuit limit
Or at minimum, mark them as unfillable.
Exclude F&O ban-period days for the affected underlying
New positions could not be opened; only reduction was allowed.
Handle the pre-open separately or exclude it
Call-auction price discovery is not the same process as continuous matching.
Drop or flag bars with zero volume
A carried-forward last price is not a tradeable price.
Verify the trading calendar rather than inferring it
Muhurat sessions, unscheduled holidays and special sessions all break date arithmetic.
6. Fill assumptions
The default fill model in most backtest frameworks is "you get the price you asked for". On liquid index futures during the middle of the day, that is roughly true. Everywhere else it is not.
Context
Reasonable fill assumption
Index futures, mid-session
Mid to touch, half a tick of slippage
Index futures, first/last 5 minutes
Touch or worse; widen materially
ATM weekly options
Touch; occasional worse fills on fast moves
OTM options beyond a few strikes
Full spread, and check depth before assuming any fill
Stock options
Full spread; many strikes are effectively untradeable in size
Any instrument on expiry afternoon
Assume the worst side and add a wide margin
Fill realism by context
The general rule: assume you cross the spread. If the strategy survives crossing the spread on every trade, it has a genuine buffer. If it only works on mid-price fills, it is a limit-order strategy, and limit-order strategies need a completely different backtest — one that models whether the order would have been filled at all.
7. Overfitting to a regime, not to noise
The standard warning about overfitting is about parameter count. The Indian-specific version is about regime: the structure of the market has changed repeatedly and materially in a short period. Weekly expiry availability changed. Contract sizes changed. STT changed twice. Upfront premium collection changed. Each of those altered the economics of entire strategy families.
A strategy validated on 2021–2023 data was validated in a different market from the one that exists now, in ways that have nothing to do with price behaviour. Walk-forward validation across those boundaries is more informative than any amount of in-sample parameter stability.
Out-of-sample testing protects against fitting to noise. Only testing across a structural break protects against fitting to a rulebook that no longer applies.
A minimum standard
Layered model
A validation stack for an honest backtest
A strategy is only as credible as the weakest realism layer it has not tested.
The base layer verifies contracts, corporate actions, calendars and timestamps. The next layer tests tradeable instrument selection. Execution adds next-observable prices, spreads, depth and partial fills. Economics adds every levy and slippage. Validation then separates regimes and out-of-sample periods. Finally live paper data checks that the same rules produce the expected decisions.
A smooth equity curve above a missing layer is not evidence. Record pass criteria and degradation at each layer separately.
Before a strategy is allowed near real money, the backtest should be able to answer all of these. If any answer is "the framework handles that", find out how.
What is the one-bar-offset version of these results?
What are the results with full-spread fills on every trade?
What are the results with the complete cost stack, applied on the correct bases?
How often does instrument or strike selection fail, and what does the strategy do when it does?
How does it perform on each side of the most recent structural change, tested separately?
What is the longest losing streak, and would the position sizing have survived it?
Frequently asked questions
How do I build a continuous futures series for backtesting Indian markets?
Either back-adjust or ratio-adjust the series at each monthly rollover, or backtest on the actual contracts and make the roll an explicit part of the strategy with its own cost. Naive concatenation of near-month prices creates a discontinuity equal to the basis at every roll, which momentum strategies will read as a tradeable gap that never existed.
What is survivorship bias in options backtesting?
Historical options datasets typically contain the strikes that had trading activity, so strikes that were introduced but never traded may be missing entirely. A strategy selecting strikes by some criterion therefore selects from a universe already filtered toward the strikes that turned out to be liquid. Live, the equivalent strike often has a wide spread and no depth. A backtest that never fails to find a matching strike is a warning sign.
How do I check whether my backtest has look-ahead bias?
Run it with a one-bar offset between signal and execution — decide on the closed bar, fill at the next bar's open — and compare. If the results change materially, the edge was in the look-ahead. This also exposes timestamp convention problems, where a bar labelled 09:15 actually contains data through 09:15:59.
Should I exclude the pre-open session from a backtest?
Usually yes, unless the strategy is specifically designed for it. The pre-open uses call-auction price discovery, which is a different matching process from continuous trading, so fills and price behaviour there are not comparable to the rest of the session.
Why do backtested results degrade so much when I add realistic costs?
Because costs scale with turnover while the edge scales per trade. A high-frequency strategy pays the cost stack on every round trip, and on Indian derivatives that stack has six components on two different bases. If your Sharpe barely moves when you add costs, the more likely explanation is that the cost model is wrong rather than that the strategy is unusually robust.