Article 18 — Cointegration Pairs Trading on NSE: Reliance-ONGC as a Worked Example
Article 18 — Cointegration Pairs Trading on NSE: Reliance-ONGC as a Worked Example
title: "Cointegration Pairs Trading on NSE — a Working Example with Reliance and ONGC"
description: "The full Engle-Granger cointegration workflow applied to an Indian energy pair, including entry/exit rules and Indian-market-specific execution constraints."
keyword: "cointegration pairs trading NSE"
stage: 4
The core idea. Two non-stationary price series can have a stationary linear combination (the spread). If the spread reverts to its mean, you can profit by shorting when it's high and longing when it's low.
The Engle-Granger 2-step procedure
Step 1: OLS regression of one series on the other
import pandas as pd
import numpy as np
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller
# Load 5 years of daily closes for RELIANCE and ONGC
df = pd.DataFrame({
"reliance": reliance_data["Close"],
"ongc": ongc_data["Close"]
}).dropna()
# Regress RELIANCE on ONGC
X = sm.add_constant(df["ongc"])
model = sm.OLS(df["reliance"], X).fit()
beta = model.params["ongc"] # hedge ratio
Step 2: ADF test on residuals
residuals = df["reliance"] - beta * df["ongc"] - model.params["const"]
adf_stat, p_value, *_ = adfuller(residuals)
print(f"ADF stat: {adf_stat:.3f}, p-value: {p_value:.3f}")
If p-value < 0.05, the spread is stationary → the pair is cointegrated → pairs trading is viable.
The z-score trading rule
# Compute z-score of spread
residuals = residuals.dropna()
mean = residuals.rolling(60).mean()
std = residuals.rolling(60).std()
z = (residuals - mean) / std
# Entry: |z| > 2. Exit: |z| < 0.5
# Long spread when z < -2: buy Reliance, short ONGC (in hedge ratio)
# Short spread when z > 2: short Reliance, buy ONGC
Indian-market-specific constraints
- Short selling. Indian retail can't short cash equity overnight. Use stock futures for the short leg.
- Hedge ratio and lot size. Your computed hedge ratio may not match F&O lot sizes. Round down; adjust size proportionally.
- Physical settlement risk on expiry. Don't hold pairs positions into expiry week.
Half-life of reversion
# AR(1) of spread
lag_spread = residuals.shift(1).dropna()
aligned = residuals.iloc[1:]
ar1_model = sm.OLS(aligned, sm.add_constant(lag_spread)).fit()
k = ar1_model.params[1] - 1
half_life = -np.log(2) / np.log(1 + k)
print(f"Half-life: {half_life:.1f} days")
A half-life of 10-30 days is ideal. Under 10 days: noise. Over 60 days: tie-up too much capital for too little return.
Stage 4 Volume 3 connection
Stage 4 Volume 3 (Time-Series Econometrics) covers cointegration with additional pairs examples, ARIMA, GARCH for volatility forecasting, and the 8 common time-series mistakes retail quants make.
Related reading
- The Wyckoff Method Applied to Indian Stocks — Accumulation, Distribution, and Springs
- VWAP Reversion Intraday on Indian Equities: The Institutional Reference, Applied to Retail
- ETF Arbitrage on NSE: How Premium-Discount Spreads Form and What Retail Traders Can Do About Them
Ready to go deeper than this article?
Bharath Shiksha is a 30-volume curriculum across 6 stages — from chart reading (Stage 1 at ₹2,999) through capital raising (Stage 6 at ₹18,999), or the full bundle at ₹39,999. Every volume has a 14-page companion worksheet, a 10-question gate quiz, and a 7-day money-back guarantee.
See the full curriculum →