Guides /
Building a continuous NIFTY futures series from monthly contracts
How to stitch near, next and far month NIFTY futures into one continuous 1-minute series: choosing the roll day, back-adjusting the gaps, and checking the result.
Futures data arrives one contract at a time. On 14 October 2025, our free sample day, three NIFTY futures traded: the October contract expiring on 28 October, November and December. At the close they were at 25,185.50, 25,314.20 and 25,485.00, while the index closed at 25,123.35. Charting "NIFTY futures" means choosing which of those to follow on each day, and what to do at the joins.
Step 1: pick a contract for each day
The near month usually has almost all the volume. On the sample day it traded 6.6 million units against 0.53 million in November and 0.16 million in December. Two common roll rules:
- Fixed offset. Switch to the next contract a set number of trading days before expiry, for example three.
- Volume or open interest. Switch on the first day the next contract trades more than the current one. This tracks where the market actually is.
import re
from pathlib import Path
import pandas as pd
FUT = re.compile(r"NIFTY_FUT_1min_(\d{4}-\d{2}-\d{2})_exp(\d{4}-\d{2}-\d{2})")
rows = []
for f in Path("data").rglob("NIFTY_FUT_1min_*.parquet"):
m = FUT.search(f.name)
v = pd.read_parquet(f, columns=["volume"])["volume"].sum()
rows.append({"day": m[1], "expiry": m[2], "file": f, "volume": v})
fut = pd.DataFrame(rows).sort_values(["day", "expiry"])
# volume rule: each day, the contract with the most volume
pick = fut.loc[fut.groupby("day")["volume"].idxmax()].set_index("day")
Make the choice one-directional in practice: once you have rolled to a later expiry, do not roll back if the old contract has one busier day.
Step 2: join and back-adjust
On a roll day the new contract trades at a different price from the old one, mostly because of carry. Joining them raw leaves a jump that never happened in the market. Back-adjustment shifts all earlier history by the gap at each roll, so returns stay correct and the latest prices are real.
def load(f):
d = pd.read_parquet(f)
ts = "datetime" if "datetime" in d else "timestamp"
return d.set_index(ts)[["open", "high", "low", "close", "volume"]]
parts = []
for day, r in pick.iterrows():
d = load(r.file)
d["expiry"] = r.expiry
parts.append(d)
cont = pd.concat(parts).sort_index()
# gap at each roll = first close of new contract minus same-minute close of old one
rolls = cont["expiry"].ne(cont["expiry"].shift())
adj = pd.Series(0.0, index=cont.index)
for t in cont.index[rolls][1:]:
day = t.strftime("%Y-%m-%d")
old_exp = cont.loc[:t, "expiry"].iloc[-2]
old = fut[(fut.day == day) & (fut.expiry == old_exp)]
if old.empty:
continue
old_close = load(old.file.iloc[0])["close"].asof(t)
gap = cont.at[t, "close"] - old_close
adj[adj.index < t] += gap
for col in ["open", "high", "low", "close"]:
cont[col + "_adj"] = cont[col] + adj
Step 3: check it
- Plot the adjusted close against the index. The gap between them should shrink steadily toward each expiry and never jump at a roll.
- Compare daily returns of the adjusted series with the raw near-month series on non-roll days. They should match exactly.
- Keep the unadjusted price and the expiry column. Use adjusted prices for signals and returns, raw prices for anything involving the actual level, such as option strikes.