Python for Trading: Write Your First Backtest in 30 Lines

A 30-line pandas backtest of a moving-average crossover, explained step by step: loading prices, building the signal, avoiding look-ahead bias, adding fees and reading the result honestly.

By StockYatra Team
Python for trading: your first backtest cover

In short

  • You can test a trading idea in about 30 lines of Python with pandas.
  • The key step is shifting the signal, so you only trade on information you already had.
  • Always include fees and compare with buy and hold.
  • Our example lost money in testing. That is a useful, honest result.

Writing a backtest yourself is the fastest way to understand what quant traders and algo traders actually do. You do not need a framework. A CSV of prices and the pandas library are enough to test a simple idea and, more importantly, to see why most ideas fail.

Five steps: load prices, indicators, make a signal, shift it, compare with buy and hold
Your first backtest in five steps.

What you need

  • Python 3 and pandas (pip install pandas).
  • A CSV of candles with columns time, open, high, low, close, volume, where time is a Unix timestamp in seconds. You can export one from most charting tools or brokers. If your file uses dates instead, change the to_datetime line.

The code

import pandas as pd

FAST, SLOW, FEE = 20, 50, 0.001          # moving-average lengths, 0.1% fee per trade

df = pd.read_csv("AAPL.csv")             # columns: time, open, high, low, close, volume
df["date"] = pd.to_datetime(df["time"], unit="s")
df = df.set_index("date").sort_index()

df["fast"] = df["close"].rolling(FAST).mean()
df["slow"] = df["close"].rolling(SLOW).mean()

# 1 = hold the stock, 0 = stay in cash
df["signal"] = (df["fast"] > df["slow"]).astype(int)

# trade on the NEXT bar: today's signal can only use today's close
df["position"] = df["signal"].shift(1).fillna(0)

df["ret"] = df["close"].pct_change().fillna(0)
trades = df["position"].diff().abs().fillna(0)
df["strategy"] = df["position"] * df["ret"] - trades * FEE

equity = (1 + df["strategy"]).cumprod()
buy_hold = (1 + df["ret"]).cumprod()
drawdown = equity / equity.cummax() - 1

print(f"Bars tested:     {len(df)}")
print(f"Trades:          {int(trades.sum())}")
print(f"Strategy return: {equity.iloc[-1] - 1:+.1%}")
print(f"Buy and hold:    {buy_hold.iloc[-1] - 1:+.1%}")
print(f"Max drawdown:    {drawdown.min():.1%}")

Line by line

  1. Settings: a 20-bar and a 50-bar simple moving average, and a 0.1% cost each time the position changes.
  2. Load and sort the prices by time.
  3. Indicators: rolling(n).mean() is a simple moving average.
  4. Signal: 1 when the fast average is above the slow one, otherwise 0 (in cash).
  5. Shift: shift(1) means we act on the next bar. Without it, the backtest uses today's close to trade at today's close, which is look-ahead bias and makes almost anything look good.
  6. Returns: the strategy earns the stock's return only while holding, minus fees whenever it buys or sells.
  7. Results: final return, buy and hold, and maximum drawdown (the deepest fall from a peak).

Our result, honestly

We ran this exact code on 500 hourly AAPL candles from StockYatra's practice market:

Bars tested:     500
Trades:          24
Strategy return: -12.5%
Buy and hold:    +0.4%
Max drawdown:    -12.6%

The crossover lost money and did worse than simply holding. In a sideways, choppy market, moving-average crossovers flip in and out often, and the fees and late entries add up. Your numbers will differ with other data. The point is the process: a precise rule, no look-ahead, costs included, and an honest comparison.

Key idea: A backtest that makes you drop an idea has done its job.

Next steps to try

  • Change FAST and SLOW, but beware: trying many settings and keeping the best one is overfitting. Test the winner on data you did not use.
  • Add a filter, for example only take signals when the 200-bar average is rising.
  • Count winning and losing trades, and the average win versus average loss.
  • Try the same rule on a different stock or market.

No Python? Test it on the chart

StockYatra does not offer a public trading API, but you can test the same logic without code. The no-code builder in Pro Chart can do "SMA 20 crosses above SMA 50", and Pro Chart also runs Pine Script like this:

//@version=5
strategy("SMA 20/50 cross", overlay=true)
fast = ta.sma(close, 20)
slow = ta.sma(close, 50)
plot(fast, "SMA 20", color=color.green)
plot(slow, "SMA 50", color=color.red)
if ta.crossover(fast, slow)
    strategy.entry("Long", strategy.long)
if ta.crossunder(fast, slow)
    strategy.close("Long")

Every buy and sell is drawn on the NEPSE, NSE or US practice chart, with return, win rate, profit factor and drawdown. It is a quick way to sanity-check an idea before or after coding it.

Key takeaways

  • pandas can backtest a simple rule in about 30 lines.
  • Shift signals by one bar to avoid look-ahead bias.
  • Include fees and compare with buy and hold.
  • An idea that fails a fair test is a lesson, not a failure.

Test rules on the chart, no code needed

StockYatra is a free practice app for Nepal (NEPSE), India (NSE) and US stocks. Write rules, test 41 transparent strategies or your own Pine Script in Pro Chart, backtest them on the chart, and practise with virtual coins. Nothing is real money.

Get StockYatra See Pro Chart

More on algos, HFT and quant

StockYatra is an education app, not a broker, adviser or research analyst. Nothing here is a recommendation to buy or sell any security or to use any trading product. Case summaries are based on public orders and press reports; allegations are not findings, and outcomes can change on appeal. Practice prices are simulated and coins are virtual.

Frequently asked questions

Is Python good for trading?
Yes. Python, with libraries like pandas, is widely used for research and backtesting because it is readable and has strong data tools.
What is look-ahead bias in a backtest?
Using information that would not have been available when the trade was made, such as acting on a bar's close at that same close. Shifting the signal by one bar avoids it.
Why include fees in a backtest?
Fees, spreads and slippage reduce every trade. Strategies that trade often can turn from profitable to losing once costs are included.
Can I connect this code to StockYatra?
No. StockYatra has no public trading API. You can test the same rules without code, or in Pine Script, in Pro Chart on practice charts.