Article 16 — Pandas Backtesting for Indian Equities: A Working Tutorial

Article 16 — Pandas Backtesting for Indian Equities: A Working Tutorial


title: "Pandas Backtesting for Indian Equities: A Working Tutorial"

description: "A 120-line Python backtest that works on Indian equities, with honest transaction costs and the look-ahead-bias safety pattern."

keyword: "pandas backtesting indian equities tutorial"

stage: 4


What this produces. A vectorized backtest of a 20/50 EMA crossover on Nifty 50 daily data, with realistic Indian transaction costs, from 2015-2025. Runs in under 3 seconds. Produces Sharpe, max drawdown, win rate, and a parameter sweep heatmap.

Setup

pip install pandas numpy yfinance matplotlib

The 120-line backtest

import pandas as pd
import numpy as np
import yfinance as yf

# 1. Fetch data
ticker = "^NSEI"
df = yf.download(ticker, start="2015-01-01", end="2025-04-01")
df = df[["Close"]].dropna()

# 2. Compute indicators
fast = 20
slow = 50
df["ema_fast"] = df["Close"].ewm(span=fast, adjust=False).mean()
df["ema_slow"] = df["Close"].ewm(span=slow, adjust=False).mean()

# 3. Generate signal (crucial: shift by 1 to avoid look-ahead bias)
df["signal"] = (df["ema_fast"] > df["ema_slow"]).astype(int)
df["position"] = df["signal"].shift(1).fillna(0)

# 4. Compute returns
df["return"] = df["Close"].pct_change().fillna(0)
df["strategy_return"] = df["position"] * df["return"]

# 5. Apply realistic Indian transaction costs
df["trade"] = df["position"].diff().abs().fillna(0)
cost_per_round_trip = 0.0035  # 0.35% STT+brokerage+fees
df["cost"] = df["trade"] * cost_per_round_trip
df["net_return"] = df["strategy_return"] - df["cost"]

# 6. Compute equity curve
df["equity"] = (1 + df["net_return"]).cumprod()

# 7. Statistics
sharpe = df["net_return"].mean() / df["net_return"].std() * (252 ** 0.5)
max_dd = (df["equity"] / df["equity"].cummax() - 1).min()
win_rate = (df["strategy_return"] > 0).mean()
total_return = df["equity"].iloc[-1] - 1

print(f"Sharpe: {sharpe:.2f}")
print(f"Max drawdown: {max_dd:.2%}")
print(f"Win rate: {win_rate:.2%}")
print(f"Total return: {total_return:.2%}")

Parameter sweep for robustness

import matplotlib.pyplot as plt

def run_backtest(df, fast, slow):
    # ... same as above, parameterised ...
    return sharpe  # from the code above

results = []
for f in range(5, 50, 5):
    for s in range(50, 200, 10):
        sh = run_backtest(df, f, s)
        results.append({"fast": f, "slow": s, "sharpe": sh})

heatmap = pd.DataFrame(results).pivot(index="fast", columns="slow", values="sharpe")
plt.imshow(heatmap, cmap="RdYlGn", aspect="auto")
plt.colorbar(label="Sharpe")
plt.title("Parameter robustness")

Interpretation

  • If the heatmap shows a clear plateau (most cells Sharpe > 0.8), the strategy has robust edge
  • If one cell dominates surrounded by cliffs, the strategy is overfit to specific parameters
  • If all cells are near zero, the strategy has no edge, or the timeframe/regime is wrong

Stage 4 Volume 2 connection

Stage 4 Volume 2 (Backtesting Foundations) provides the full production-grade backtest template (pandas + Backtrader + vectorbt comparisons).


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 →