Walk-Forward & Out-of-Sample Backtesting for Nifty (No Lookahead)
By Shakti Tiwari — Nifty Option Trader, Research Analyst & XGBoost Expert
A strategy that looks perfect on ten years of Nifty 50 data and fails the moment you go live has one usual cause: it was overfit and never tested out-of-sample. Walk-forward analysis is the fix. This guide shows, in free Python, how to split ^NSEI history correctly, avoid lookahead bias, and get an honest estimate of how a strategy would have performed on data it never "saw." Code-first, India-focused, no fake stats.
Why In-Sample Backtests Lie
If you tune parameters on the same data you evaluate on, you're grading your own homework. With enough parameter combinations, something will look brilliant on the Nifty purely by luck. Out-of-sample testing separates the data used to choose parameters from the data used to judge them.
The Two Guardrails
-
No lookahead — never use information from time
t(or later) to make a decision at timet. In pandas this means.shift(1)on positions and never fitting on future rows. - Out-of-sample evaluation — optimize on a training window, then measure on the next, untouched window. Repeat, rolling forward. That's walk-forward.
Step 1: Free Nifty Data
pip install yfinance pandas numpy
import yfinance as yf, pandas as pd, numpy as np
df = yf.download("^NSEI", start="2010-01-01", end="2024-12-31", auto_adjust=True)
df = df.dropna()
df["ret"] = df["Close"].pct_change()
Step 2: A Parameterized Strategy
We'll optimize the lookback of a moving-average trend filter. The parameter is n; the goal is to choose it without peeking at the test period.
def run_strategy(data, n):
ma = data["Close"].rolling(n).mean()
signal = (data["Close"] > ma).astype(int)
position = signal.shift(1) # NO LOOKAHEAD
strat_ret = position * data["ret"]
return strat_ret
def sharpe(r, periods=252):
r = r.dropna()
if r.std() == 0 or len(r) < 20:
return -np.inf
return np.sqrt(periods) * r.mean() / r.std()
Step 3: Walk-Forward Loop
Split the history into consecutive blocks. On each training block, pick the n with the best Sharpe. Apply that n to the following test block and record the out-of-sample returns. Roll forward.
params = range(20, 210, 10) # candidate lookbacks
train_years, test_years = 4, 1
oos_returns = []
years = sorted(df.index.year.unique())
for start in range(0, len(years) - train_years - test_years + 1, test_years):
tr = years[start:start + train_years]
te = years[start + train_years: start + train_years + test_years]
train = df[df.index.year.isin(tr)]
test = df[df.index.year.isin(te)]
# choose best param on TRAIN only
best_n = max(params, key=lambda n: sharpe(run_strategy(train, n)))
# evaluate that param on TEST (unseen)
oos = run_strategy(test, best_n)
oos_returns.append(oos)
print(f"Train {tr[0]}-{tr[-1]} -> best n={best_n}, "
f"test {te[0]} Sharpe={sharpe(oos):.2f}")
oos_all = pd.concat(oos_returns).dropna()
Notice the chosen n often changes between windows — that instability is itself a warning that the "best" parameter is partly noise.
Step 4: Judge Only the Out-of-Sample Curve
equity = (1 + oos_all).cumprod()
cagr = equity.iloc[-1] ** (252 / len(oos_all)) - 1
sh = sharpe(oos_all)
maxdd = (equity / equity.cummax() - 1).min()
print(f"OOS CAGR {cagr:.3f} | Sharpe {sh:.2f} | MaxDD {maxdd:.3f}")
This out-of-sample equity curve is the only one worth showing anyone. The in-sample curve is marketing; the walk-forward curve is evidence. I'm deliberately not quoting the resulting numbers — they depend on your windows and dates, and a fixed figure would be a fabricated stat. Reproduce it yourself.
Step 5: Compare In-Sample vs Out-of-Sample
The honesty test: optimize n over the entire history (in-sample) and compare that Sharpe to the walk-forward Sharpe.
best_all = max(params, key=lambda n: sharpe(run_strategy(df, n)))
print("In-sample best n:", best_all, "Sharpe:", round(sharpe(run_strategy(df, best_all)), 2))
print("Walk-forward Sharpe:", round(sh, 2))
If the walk-forward Sharpe collapses versus the in-sample one, your edge was overfitting. That gap is the single most useful diagnostic in quant backtesting.
India-Specific Notes
- Works identically on the Sensex (
^BSESN) — swap the ticker. - For Nifty options / intraday walk-forward, source clean candles from Zerodha Kite Connect (paid) or Angel One SmartAPI (free) rather than Yahoo's short intraday window.
- Always net out Indian costs (brokerage, STT, slippage) inside
run_strategybefore optimizing — otherwise you'll pick over-trading parameters.
Common Mistakes
- Fitting the model or scaler on the full dataset before splitting → subtle lookahead. Fit on train only.
- Choosing the parameter after seeing test results → that's just in-sample with extra steps.
- Too-short test windows → noisy, unreliable out-of-sample estimates.
- Reporting the in-sample curve as if it were live-representative.
Takeaway
- Walk-forward analysis optimizes on a training window and judges on the next, unseen window — repeatedly rolling forward.
- Always
.shift(1)positions and fit only on training data to kill lookahead bias. - The out-of-sample equity curve is the only credible one; the in-sample curve flatters everything.
- A big gap between in-sample and walk-forward Sharpe means overfitting — treat it as a red flag.
- Apply the same framework to Sensex, Nifty options, or intraday with clean Kite/Angel data.
Related: My book Option Trading with AI: XGBoost, Transformers & Quantized Models for the Retail Nifty Trader applies walk-forward validation to ML models on Nifty.
Disclaimer: This article is for education only and is not financial, investment, or trading advice. SEBI-registered research rules apply — verify everything before acting.
🔗 Get the book on Amazon: https://www.amazon.in/dp/B0H9ZNTBPK
📢 Daily Nifty analysis on Telegram: https://t.me/shaktitrade
📧 Email: shaktitiwari715@gmail.com
💬 Need Help? (Free Support)
If you have any trouble building your own AI trading model, or questions about any article — I can help you for free.
📧 Email: shaktitiwari715@gmail.com
📢 Telegram: https://t.me/shaktitrade
No course, no upsell — just genuine help for retail traders.

Top comments (0)