Algorithmic trading means expressing trading rules as code so they can be tested and applied consistently. With Python and a few open-source libraries, you can download historical data, turn a rule into code and test it — before risking any money.

This tutorial builds a simple rule — a 20/50 exponential moving average (EMA) crossover — and back-tests it on historical data. It deliberately stops at back-testing and paper trading.

Step 1: Set up your environment

Install the libraries from your terminal:

bash codebash
pip install yfinance pandas numpy matplotlib
  • yfinance — downloads historical market data from Yahoo Finance, including NSE- and BSE-listed symbols.
  • pandas — tables of data and time-series calculations.
  • numpy — numerical calculations.
  • matplotlib — charts.

Create a file called trading_bot.py and open it in your editor.

Step 2: Fetch historical data

For NSE symbols on yfinance, add .NS after the ticker; for BSE symbols, add .BO.

Download daily OHLCV data for one example symbol.python
import yfinance as yf
import pandas as pd

# Historical daily data for one NSE-listed stock.
# "RELIANCE.NS" is used only as an example ticker: use any liquid NSE symbol
# (".NS" for NSE, ".BO" for BSE).
ticker = "RELIANCE.NS"
data = yf.download(ticker, start="2023-01-01", end="2024-12-31")

print(data.head())
print(f"Total rows: {len(data)}")

This downloads daily open, high, low, close and volume data. Free data sources can have gaps or adjustments, so check the data before relying on it.

Step 3: Define the rule

The rule is a well-known teaching example:

  • Entry — when the 20-day EMA crosses above the 50-day EMA.
  • Exit — when the 20-day EMA crosses below the 50-day EMA.
Calculate the two exponential moving averages.python
# Exponential moving averages of the closing price
data["EMA_20"] = data["Close"].ewm(span=20, adjust=False).mean()
data["EMA_50"] = data["Close"].ewm(span=50, adjust=False).mean()

# Drop the first rows, where the averages are still warming up
data.dropna(inplace=True)

print(data[["Close", "EMA_20", "EMA_50"]].tail(10))

Step 4: Detect entries and exits

Find the days on which the averages cross.python
# State: 1 when the 20 EMA is above the 50 EMA, otherwise 0
data["State"] = 0
data.loc[data["EMA_20"] > data["EMA_50"], "State"] = 1

# A crossover is a change of state
data["Crossover"] = data["State"].diff()

entries = data[data["Crossover"] == 1]   # 20 EMA crosses above the 50 EMA
exits = data[data["Crossover"] == -1]    # 20 EMA crosses below the 50 EMA

print(f"Entries: {len(entries)}")
print(f"Exits:   {len(exits)}")

Step 5: Back-test the rule

A back-test applies the rule to historical data to see how it would have behaved. The key detail is shift(1): the rule can only act on information available the day before, which avoids look-ahead bias.

Compare the rule with buy-and-hold on the same historical data.python
# Daily returns of the stock
data["Market_Return"] = data["Close"].pct_change()

# The rule is invested only on days AFTER the state was 1 (shift avoids look-ahead bias)
data["Strategy_Return"] = data["Market_Return"] * data["State"].shift(1)

# Growth of 1 rupee
data["Cum_Market"] = (1 + data["Market_Return"]).cumprod()
data["Cum_Strategy"] = (1 + data["Strategy_Return"]).cumprod()

print(f"Buy-and-hold (historical): {(data['Cum_Market'].iloc[-1] - 1) * 100:.1f}%")
print(f"Rule (historical, before costs): {(data['Cum_Strategy'].iloc[-1] - 1) * 100:.1f}%")

Step 6: Test it properly

A single back-test proves very little. Before trusting any rule, test it more widely:

  • Several stocks and time periods — a rule that looked good on one stock in one period often fails elsewhere.
  • Realistic costs — brokerage, STT, GST, stamp duty, exchange fees and slippage can remove an apparent edge entirely.
  • Drawdown, not just return — measure the largest fall from a peak as well as the final number.
  • Out-of-sample testing — keep some data aside that you did not use while designing the rule.

Step 7: Paper trading next

If a rule survives wider testing, the next step is paper trading: running it forward in time with simulated orders and no real money. Paper trading shows how the rule behaves on data it has never seen, and how your code handles real-time data, errors and gaps.

Connecting to a broker's API is a separate step that needs a trading account, API access from that broker, and compliance with the current SEBI and exchange rules for retail algorithmic trading. Read each provider's terms carefully.

Where to go next

The Algorithmic Trading with Python course takes this further with an indicator library, event-driven back-testing, performance analytics, risk controls in code and a paper-trading system. The Strategy Backtester project is the structured version of this tutorial.