Backtesting in Python: A Step-by-Step Tutorial with Real Market Data

  1. iTick
  2. Tutorial
Backtesting in Python: A Step-by-Step Tutorial with Real Market Data - iTick
Backtesting in Python: A Step-by-Step Tutorial with Real Market Data

Backtesting in Python: A Step-by-Step Tutorial with Real Market Data

Backtesting means simulating a trading strategy on historical data to see how it would have performed. It is the cheapest way to discover that an idea doesn't work - before real money finds out for you. In this tutorial you will build a complete, honest backtest in Python using pandas and real OHLCV data from the iTick API.

What you need

  • Python 3.9+ with pandas, numpy and requests
  • A free iTick API key for historical data
  • About 20 minutes
      pip install pandas numpy requests

    

Step 1: Download historical OHLCV data

      import requests
import numpy as np
import pandas as pd

TOKEN = "YOUR_API_TOKEN"
HEADERS = {"accept": "application/json", "token": TOKEN}

def load_bars(asset, region, code, ktype=8, limit=1000):
    url = f"https://api.itick.org/{asset}/kline"
    params = {"region": region, "code": code, "kType": ktype, "limit": limit}
    rows = requests.get(url, params=params, headers=HEADERS, timeout=10).json()["data"]
    df = pd.DataFrame(rows).rename(columns={"t": "time", "o": "open", "h": "high",
                                            "l": "low", "c": "close", "v": "volume"})
    df["time"] = pd.to_datetime(df["time"], unit="ms", utc=True)
    return df.set_index("time").sort_index().astype(float)

df = load_bars("stock", "US", "SPY")
print(df.tail())

    

kType selects the bar interval (see the docs); daily bars are a good starting point. New to the format? Read What is OHLCV data?

Step 2: Define the strategy

We will test a classic EMA crossover: hold the asset while the 20 EMA is above the 50 EMA, stay in cash otherwise. (Background: What is EMA?)

      fast, slow = 20, 50
df["ema_fast"] = df["close"].ewm(span=fast, adjust=False).mean()
df["ema_slow"] = df["close"].ewm(span=slow, adjust=False).mean()

# Signal computed on today's close...
df["signal"] = (df["ema_fast"] > df["ema_slow"]).astype(int)
# ...but we can only trade on the NEXT bar -> shift to avoid look-ahead bias
df["position"] = df["signal"].shift(1).fillna(0)

    

The shift(1) is the most important line in this tutorial. Without it, the backtest "knows" today's close before trading at it - a classic look-ahead bias that makes almost any strategy look brilliant.

Step 3: Calculate returns, including costs

      fee = 0.0005  # 5 bps per trade side - adjust for your broker and market

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

df["equity"] = (1 + df["strategy_ret"]).cumprod()
df["buy_hold"] = (1 + df["ret"]).cumprod()

    

Step 4: Measure performance

      def stats(returns, periods_per_year=252):
    equity = (1 + returns).cumprod()
    years = len(returns) / periods_per_year
    cagr = equity.iloc[-1] ** (1 / years) - 1
    vol = returns.std() * np.sqrt(periods_per_year)
    sharpe = returns.mean() / returns.std() * np.sqrt(periods_per_year) if returns.std() else 0
    drawdown = equity / equity.cummax() - 1
    return {"CAGR": f"{cagr:.2%}", "Volatility": f"{vol:.2%}",
            "Sharpe": round(sharpe, 2), "Max drawdown": f"{drawdown.min():.2%}"}

print("Strategy:", stats(df["strategy_ret"]))
print("Buy & hold:", stats(df["ret"]))
print("Number of trades:", int(df["trades"].sum()))

    
MetricWhat it tells you
CAGRCompound annual growth rate
VolatilityAnnualised standard deviation of returns
Sharpe ratioReturn per unit of risk (risk-free rate ignored here)
Max drawdownWorst peak-to-trough loss - can you live through it?

Use periods_per_year=252 for daily stock bars, about 365 for crypto, and scale accordingly for intraday bars.

Step 5: Test on other markets

Because iTick uses one schema for every asset class, the same backtest runs on forex, gold or Bitcoin by changing one line:

      eurusd = load_bars("forex", "GB", "EURUSD")
btc = load_bars("crypto", "BA", "BTCUSDT")

    

Common backtesting mistakes

  1. Look-ahead bias - using information not available at decision time. Always shift signals.
  2. Ignoring costs - commissions, spread and slippage can turn a winner into a loser.
  3. Overfitting - testing hundreds of parameter combinations and keeping the best. Hold out an out-of-sample period.
  4. Survivorship bias - testing only stocks that still exist today.
  5. Too little data - a strategy tested on one bull market hasn't been tested.

From backtest to live trading

Once a strategy survives out-of-sample testing, connect it to live prices. Use the iTick WebSocket feed for real-time quotes and ticks, keep computing your indicators incrementally, and paper-trade before going live.

FAQ

What is the best Python library for backtesting? For learning, plain pandas (as above) shows exactly what happens. For larger projects, frameworks such as Backtrader, vectorbt or Zipline-style engines add order simulation and reporting.

How much historical data do I need? Enough to cover different market regimes - typically several years of daily data, more trades rather than more years for intraday systems.

Where can I get historical data for backtesting? The iTick historical data API provides OHLCV for stocks, forex, crypto, indices and futures, with a free plan to start.

Start testing your ideas

Get a free API key, download historical data in minutes and find out whether your strategy really works.