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

  1. Short selling. Indian retail can't short cash equity overnight. Use stock futures for the short leg.
  2. Hedge ratio and lot size. Your computed hedge ratio may not match F&O lot sizes. Round down; adjust size proportionally.
  3. 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

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 →