⚠ Developer Disclaimer: Instacode.in is a software development company — we are NOT a SEBI-registered Investment Advisor (RIA) and do NOT provide investment, trading, or financial advice. All content below is purely educational / technical in nature. Strategies are mentioned only as coding patterns — outcomes, profits, or losses depend entirely on the user's own decisions, capital, and market conditions. Trading in markets carries risk of loss.

A backtest is the closest thing trading has to a time machine — you run today's rules against yesterday's prices and see what would have happened. The mechanics take an afternoon. Making the result trustworthy is the actual work, and most of this article is about that.

Step 1 — Get the data

from kiteconnect import KiteConnect
import pandas as pd

kite = KiteConnect(api_key="key")
kite.set_access_token("token")

candles = kite.historical_data(
    instrument_token=738561,   # RELIANCE
    from_date="2021-01-01",
    to_date="2026-04-01",
    interval="day"
)
df = pd.DataFrame(candles)
df.set_index('date', inplace=True)
df.to_csv('reliance_5y.csv')

Save it to disk on the first run. You will iterate on this strategy fifty times, and re-downloading five years of candles each time is slow and wastes your API quota for no reason.

Step 2 — Generate signals

df['ema20'] = df['close'].ewm(span=20).mean()
df['ema50'] = df['close'].ewm(span=50).mean()
df['signal'] = 0
df.loc[df['ema20'] > df['ema50'], 'signal'] = 1    # long
df.loc[df['ema20'] < df['ema50'], 'signal'] = -1   # short

Step 3 — Turn signals into returns

df['returns'] = df['close'].pct_change()
df['strategy_returns'] = df['signal'].shift(1) * df['returns']
df['cum_strategy'] = (1 + df['strategy_returns']).cumprod()
df['cum_buy_hold'] = (1 + df['returns']).cumprod()

That shift(1) is the single most important character in this entire article. Without it you are using today's closing price to decide today's trade — which is not a strategy, it is a look into the future. Backtests that produce absurdly good numbers are usually missing exactly this line.

Step 4 — Measure it

import numpy as np

total_return = df['cum_strategy'].iloc[-1] - 1
print(f"Total return: {total_return*100:.2f}%")

years = len(df) / 252
ann_return = (df['cum_strategy'].iloc[-1] ** (1/years)) - 1
print(f"Annualised: {ann_return*100:.2f}%")

sharpe = np.sqrt(252) * df['strategy_returns'].mean() / df['strategy_returns'].std()
print(f"Sharpe: {sharpe:.2f}")

cum = df['cum_strategy']
running_max = cum.cummax()
drawdown = (cum - running_max) / running_max
print(f"Max drawdown: {drawdown.min()*100:.2f}%")

Of these four numbers, maximum drawdown is the one to look at first. Total return tells you what the strategy earned; drawdown tells you whether you would have still been running it when it did. A curve that returns forty percent after a sixty percent drawdown is a strategy nobody actually holds through.

Step 5 — Plot it

import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.plot(df.index, df['cum_strategy'], label='Strategy')
plt.plot(df.index, df['cum_buy_hold'], label='Buy & Hold')
plt.legend()
plt.title('Equity Curve')
plt.show()

Always plot buy-and-hold alongside. A strategy that underperforms simply holding the index is a lot of work for a worse outcome, and the chart makes that obvious in a way the numbers do not.

The five ways a backtest lies

  1. Look-ahead bias. Future information leaking into the signal. Fixed by shift(1), and worth checking twice.
  2. Survivorship bias. Testing only on companies that still exist today quietly removes every failure from your sample.
  3. No costs. Brokerage and slippage take roughly 0.05 to 0.1 percent per trade. On a high-frequency strategy that is the difference between a winner and a loser.
  4. Over-fitting. Tuning parameters until the past looks perfect. You have described history, not found an edge.
  5. Single market regime. A trend-following rule tested only across a bull run tells you nothing about how it behaves sideways.

Adding real costs

BROKERAGE = 0.0003   # 0.03%
SLIPPAGE  = 0.0005   # 0.05%

df['trade'] = df['signal'].diff().abs()
df['costs'] = df['trade'] * (BROKERAGE + SLIPPAGE)
df['net_returns'] = df['strategy_returns'] - df['costs']
df['cum_net'] = (1 + df['net_returns']).cumprod()
print(f"Net total: {(df['cum_net'].iloc[-1]-1)*100:.2f}%")

Run this and compare the two curves. On a slow strategy the gap is small. On something that trades several times a day, watching a profitable equity curve turn flat is a genuinely useful experience — and better to have it here than with money.

Walk-forward validation

A single backtest across the whole dataset is weak evidence, because you tuned the parameters while looking at all of it.

Split the data by time instead. Tune on the first seventy percent, test on the remaining thirty, then roll the window forward and repeat. If the strategy only works when it can see the test period, walk-forward is where that shows up.

Libraries worth knowing

  • backtrader — event-driven and full featured, closest to how live execution actually behaves
  • vectorbt — very fast for parameter sweeps
  • zipline-reloaded — the maintained descendant of Quantopian's engine
  • bt — small and readable, good for portfolio-level tests

Write your own first anyway. Fifty lines of Pandas teaches you where the assumptions hide, and after that you will use a library knowing what it is doing for you.

The short version

A good backtest is an honest one. Include costs, shift your signals, test across different market conditions, and be suspicious of any result that looks wonderful. Historical performance is not a prediction — it is a sanity check on whether the idea is worth risking money on at all.

We backtest custom strategies for clients and deliver the full report, including the runs where the strategy did not work. Those are usually the more useful ones.

FAQs

Why do live results differ from the backtest?

Usually slippage and brokerage that were left out, parameters over-fitted to the past, or a change in market conditions. Add real costs first — that alone explains most of the gap. Past performance is not an indicator of future behaviour.

How many years of data do I need?

Three to five as a minimum, so the test spans more than one type of market. A strategy tested only across a strong trend tells you very little about how it handles a sideways year.

What is look-ahead bias in simple terms?

Using information in a decision that would not have existed at that moment — for example, using today's closing price to decide today's trade. Shifting the signal by one period fixes the common case.

Should I write my own backtester or use a library?

Write a simple one first. Fifty lines of Pandas shows you exactly where the assumptions live. Once you understand that, a library like backtrader or vectorbt saves time without hiding anything from you.

📚 More Algo Trading Guides

Build Your First Trading Bot with Zerodha and Python (2026)Common Algo Trading Strategies for Nifty &amp; BankNifty — A Developer's View (2026)Zerodha Kite Connect API — Step-by-step Tutorial (2026)Python Algo Trading in India — A Beginner's Guide (2026)Python vs VBA for Trading Automation — Which One Should You Pick?SEBI Algo Trading Rules — A Plain-English Guide (2026)
Found this useful? Share it:

Need a custom solution?

Instacode builds production-grade software — algo trading, ecommerce, web apps. Let's talk.

Get in Touch

💬 Comments (0)

Leave a comment

Apna sawaal ya feedback share karo — Sonali aur team padhte hain.

Be the first to comment 🚀